richtext

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: MIT Imports: 14 Imported by: 0

README

Ridu rich-text plugin

github.com/riducms/ridu/plugins/richtext provides the rich-text field, portable Lexical document schema, server validation, reference metadata, and safe Go HTML renderer. Pair it with @riducms/plugin-richtext for the Svelte/Lexical editor.

Register the backend once, then use its field constructor:

ridu.Config{
	Plugins: []ridu.Plugin{richtext.New()},
	Collections: []ridu.Collection{{
		Slug: "posts",
		Fields: field.Fields{
			richtext.Field("content").Required(),
		},
	}},
}

richtext.Field("content") uses the current defaults. Pass one optional richtext.Config to customize the field; an explicit empty config keeps the same defaults. Supplying more than one configuration value panics immediately.

In an existing project, add both packages:

ridu add richtext \
  --go-package github.com/riducms/ridu/plugins/richtext \
  --admin-package @riducms/plugin-richtext

Then generate and create the selected adapter's migration. The generated registry imports the admin pair and rejects incompatible pairing metadata.

See the Rich text guide for installation, features, constraints, verification, SDK values, and server rendering.

Schema-backed block embeds

Configure a fixed set of ordinary reusable definitions. Omitting Features enables the usual features and blocks when definitions are present:

callout := field.Block{
    Key: "callout", Label: "Callout", TypeName: "ArticleCallout",
    Fields: field.Fields{
        field.Text("title").Required(),
        field.Textarea("message").Localized(),
        field.Relationship("related", "posts"),
    },
}

body := richtext.Field("body", richtext.Config{
    Blocks: []field.Block{callout},
})

Config.Blocks remains executable Go schema. It is excluded from serialized plugin settings and resolved through the field-owned EmbeddedTrees descriptor; all payload fields use ordinary defaults, validators, hooks, access, localization, references and transactions. Groups, arrays and finite nested rich-text fields work through that same boundary. An explicit Features list must include FeatureBlocks when definitions are supplied. FeatureBlocks by itself is a configuration error.

Ordinary relationship/upload fields in these payloads participate in target admission, population, reference indexing, and restrict/nullify actions. Standalone relationship/upload editor nodes retain their earlier shape/collection-allowlist validation only: they can reference missing targets and keep dangling IDs after deletion. Nesting an editor inside a block does not change that boundary.

The portable atomic node is:

{"type":"block","version":1,"fields":{"_key":"stable-occurrence","blockType":"callout","title":"Read first"}}

Only type, version and fields belong to the node envelope; no editor children or top-level payload properties are accepted. _key and blockType are reserved inside the schema payload. The server assigns an omitted key for a new occurrence and preserves valid existing keys.

Generation instantiates RichTextDocument<Payload> and RichTextDocumentInput<Payload> with field-specific unions. The portable TypeScript entry is @riducms/plugin-richtext/document. The Go equivalent is richtext.Document[generated.PostsBodyBlocksBlockPayload] (with InputPayload or UpdatePayload for writes). Its Node[T].Fields contains the generated scalar payload codec; switch on Fields.Value to access concrete generated variants. Unknown variants fail typed decoding; keep raw JSON in export/migration tools. Even plain fields have an empty allowlist: their TS payload is never, and their generated Go payload decoder rejects every block variant.

For typed read/edit/save, convert the envelope while retaining only payload identities:

update, err := richtext.MapDocumentBlocks(*article.Body,
    generated.PostsBodyBlocksBlockPayload.Retain)
// Handle err, then edit the relevant typed occurrence.
callout := update.Root.Children[1].Fields.Value.(*generated.ArticleCalloutUpdate)
title := "Updated title"
callout.Title = &title
// Save update through PostsUpdate.Body with the article's current revision.

Retain creates fresh update variants without copying redacted, localized or populated values. MapDocumentBlocks copies prose and editor structure, preserving occurrence identity and order. The typed article workflow demonstrates a populated read, localized children, nested rich text and revision-checked save without maps.

Read-only rendering

@riducms/plugin-richtext/render exports renderRichTextHTML without editor, DOM or Svelte imports. Pass a RichTextBlockRenderers<Payload, string> map for application-owned block HTML. Ordinary text is escaped; renderer HTML is trusted application code. Missing node/block renderers throw by default. The explicit fallback(node, error) option supports an application-owned visible recovery placeholder.

For Svelte, @riducms/plugin-richtext/svelte exports <RichText value={body} blocks={renderers} /> and RichTextBlockComponents<Payload>. Each component receives its exact { block } variant. The optional fallback and references snippets make unsupported nodes and reference presentation explicit. This read-only entry does not import Lexical or the admin editor.

Go applications use richtext.RenderDocument(document, blockRenderer, nodeRenderers). The block callback receives the generated payload codec for a normal concrete-type switch. RenderHTML remains the deliberate low-level raw renderer hook. None of these renderers fetches relationships: pass access-approved populated values, or resolve documented IDs in a bounded application query.

Experimental content and schema changes

This is a prelaunch replacement of the experimental placeholder, with unchanged version numbering. There is no automatic conversion or promise to accept arbitrary old payloads. Regenerate disposable development fixtures through the current API. For content worth retaining, export the raw document and revision JSON first, declare the intended block schemas, then use the adapter's existing compiled migration transform to move legacy top-level blockType/payload properties into fields, validate every declared variant and materialize stable identities. Inspect retained revisions and localized values too. Do not simply relabel an undeclared payload as a supported variant.

Unknown stored variants enter the generic block_recovery_required boundary: ordinary consumers receive recovery diagnostics, unredacted unknown payloads are withheld, and ordinary saves are blocked. Recovery uses explicit raw export and compiled transforms; reads never silently convert or drop historical blocks. Renaming/removing a variant or changing stored payload schemas uses the same migration inspection and transform contracts as every Phase 3 structured-content plugin.

Adding another variant to an already enabled block allowlist is additive on all three adapters. Enabling blocks for the first time on an existing plain rich-text field also changes its serialized feature settings: the conservative generic planners require an explicit compiled validation transform for that first opt-in, even if existing prose needs no rewriting. This is a migration limitation, not an automatic compatibility conversion. Keep the existing document and node versions.

Documentation

Overview

Package richtext provides Ridu's official versioned rich-text field plugin.

Index

Constants

View Source
const (
	Key                       = "richtext"
	DocumentVersion           = 1
	AdminPluginPairingVersion = 1
)

Variables

This section is empty.

Functions

func Field

func Field(name string, configs ...Config) field.PluginField

Field constructs a rich-text field with optional configuration. With no Config, it uses the recommended defaults. A supplied Config with omitted Features also inherits those defaults; an explicitly empty Features slice disables them. Embedded block fields retain their attached behavior and presentation. Field panics if more than one Config is supplied.

func New

func New() ridu.Plugin

func RenderDocument added in v0.2.0

func RenderDocument[T any](document Document[T], block func(T) (string, error), nodes map[string]func(store.Values) (string, error)) (string, error)

RenderDocument renders a typed document without loading an editor or fetching referenced records. The block callback receives the generated payload codec; applications dispatch its Value union by concrete generated type. Missing callbacks fail explicitly. Use RenderHTML for deliberate raw recovery tooling.

func RenderHTML

func RenderHTML(value store.Value, renderers map[string]func(store.Values) (string, error)) (string, error)

RenderHTML renders the portable core nodes. Custom feature nodes use a caller-owned renderer, keeping application presentation outside storage.

Types

type Config

type Config struct {
	// Blocks is the fixed executable schema allowlist. Fields are resolved
	// through the public embedded-fields contract, never serialized as settings.
	Blocks []field.Block `json:"-"`
	// BlockReferences selects Config.Blocks definitions; it is exclusive with Blocks.
	BlockReferences         []string  `json:"-"`
	Features                []Feature `json:"features"`
	UploadCollections       []string  `json:"uploadCollections,omitempty"`
	RelationshipCollections []string  `json:"relationshipCollections,omitempty"`
}

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a defensive copy of Ridu's recommended authoring defaults.

type Document

type Document[T any] struct {
	Version int     `json:"version"`
	Root    Node[T] `json:"root"`
}

Document is the portable rich-text envelope. T is the generated, field-specific block payload codec. It is not a map: configured variants retain their exact input/output types and discriminator through ordinary generated block codecs.

func MapDocumentBlocks added in v0.2.0

func MapDocumentBlocks[From, To any](document Document[From], convert func(From) (To, error)) (Document[To], error)

MapDocumentBlocks copies the editor tree while explicitly converting each typed block payload. Pass the generated output payload's Retain method to make an update document: only block identities are retained, so omitted, redacted, localized and populated fields are never copied into a write. Edit returned update payloads and save with the parent document's revision.

func (Document[T]) MarshalJSON added in v0.2.0

func (document Document[T]) MarshalJSON() ([]byte, error)

func (*Document[T]) UnmarshalJSON added in v0.2.0

func (document *Document[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON rejects unsupported documents rather than dropping historical content while decoding. Export/migration tools should retain raw JSON when a typed decoder returns an error.

type Feature

type Feature string
const (
	FeatureLinks          Feature = "links"
	FeatureLists          Feature = "lists"
	FeatureCode           Feature = "code"
	FeatureHorizontalRule Feature = "horizontal-rule"
	FeatureUploads        Feature = "uploads"
	FeatureRelationships  Feature = "relationships"
	FeatureBlocks         Feature = "blocks"
)

type Node added in v0.2.0

type Node[T any] struct {
	Type       string          `json:"type"`
	Version    int             `json:"version,omitempty"`
	Children   []Node[T]       `json:"children,omitempty"`
	Fields     *T              `json:"fields,omitempty"`
	Text       string          `json:"text,omitempty"`
	Format     json.RawMessage `json:"format,omitempty"`
	Detail     int             `json:"detail,omitempty"`
	Mode       string          `json:"mode,omitempty"`
	Style      string          `json:"style,omitempty"`
	TextFormat int             `json:"textFormat,omitempty"`
	TextStyle  string          `json:"textStyle,omitempty"`
	Direction  *string         `json:"direction,omitempty"`
	Indent     int             `json:"indent,omitempty"`
	Tag        string          `json:"tag,omitempty"`
	URL        string          `json:"url,omitempty"`
	Target     string          `json:"target,omitempty"`
	Rel        string          `json:"rel,omitempty"`
	Title      string          `json:"title,omitempty"`
	ListType   string          `json:"listType,omitempty"`
	Start      int             `json:"start,omitempty"`
	Value      int             `json:"value,omitempty"`
	Checked    *bool           `json:"checked,omitempty"`
	Language   string          `json:"language,omitempty"`
	Theme      string          `json:"theme,omitempty"`
	RelationTo string          `json:"relationTo,omitempty"`
	ID         string          `json:"id,omitempty"`
	Caption    string          `json:"caption,omitempty"`
}

Node contains portable editor properties. Fields is present only for atomic block nodes and holds the field-specific generated payload union.

func (Node[T]) MarshalJSON added in v0.2.0

func (node Node[T]) MarshalJSON() ([]byte, error)

func (*Node[T]) UnmarshalJSON added in v0.2.0

func (node *Node[T]) UnmarshalJSON(data []byte) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL