RTE bodies: TipTap-core Solid wrapper with markdown round-trip and configurable allowlist #7
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle#7
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What to build
Rich-text editing for element bodies (docs/DECISIONS.md D12): a thin SolidJS wrapper around
@tiptap/core(official bindings are React/Vue only). Serializes to/from the markdown body of an element. Formatting allowlist is developer-configurable per richtext field via the sidecar; default: bold, italic, links, bullet/numbered lists, headings h2–h4. No raw HTML ever; paste from Word/Docs/web is sanitized to the allowlist.Acceptance criteria
body.type: rterender the TipTap editor inside the element form; typing produces styled text, never syntaxBlocked by
Agent Brief
Category: enhancement
Summary: Rich-text editing for element bodies: a thin SolidJS wrapper around
@tiptap/core(the official TipTap bindings are React/Vue only) that round-trips an element's markdown body through the editor, enforces a developer-configurable per-field formatting allowlist from the sidecar (default: bold, italic, links, bullet/numbered lists, headings h2–h4), sanitizes paste, and never produces or executes raw HTML.Dependency note: this issue is hard-blocked by #3 (schema-generated element form and canonical save path); the scheduler handles ordering — do not start until #3 is merged. #3 provides the sidecar schema loader with a body policy field, the form the editor mounts inside, and the save path through the round-trip engine from #2 — the RTE replaces the plain-textarea body treatment #3 allowed itself. The issue body's
docs/DECISIONS.md D12reference points at a file that is not committed — this brief is self-contained.Current behavior:
After #3, an element's params are edited through a schema-generated form and saved canonically; the element's markdown body, if editable at all, is a plain textarea showing raw markdown syntax. There is no rich-text editing anywhere: authors see and type
**bold**,##and[text](url)literally. Nothing constrains what markdown constructs a body may contain, and nothing sanitizes pasted content. The frontend is SolidJS; no TipTap dependency exists yet.Desired behavior:
Solid wrapper. A thin, reusable SolidJS integration of
@tiptap/core: create the editor instance on mount, destroy it on cleanup, and bridge editor state to Solid reactivity (value in, change notifications out). No React or Vue packages. The wrapper takes the field's current markdown body and its allowlist configuration, and reports the edited body back for the normal #3 save flow. It is a generic component — no per-element or per-field special cases.Body policy. The sidecar body policy from #3 gains a rich-text variant: a body declared as rich text (e.g.
body.type: rte) renders the TipTap editor inside the element form in place of a textarea. Non-rte body policies keep their existing treatment. The rte declaration optionally carries the field's formatting allowlist; when omitted, the default allowlist applies: bold, italic, links, bullet lists, numbered lists, headings h2–h4. The configurable vocabulary is exactly that construct set — each item can be granted or withheld per field (heading levels individually or as a range, within h2–h4).Editing. Typing produces styled text, never visible markdown syntax. The toolbar (and any keyboard shortcuts/input rules) offers exactly the field's allowlisted constructs; a construct outside the allowlist is not offered, not reachable by shortcut, and not accepted from paste — e.g. a field configured without headings shows no heading control and pasting a heading yields its text content. h1 is never offered or accepted on any field regardless of configuration; it is reserved for the page title.
Round-trip. The editor loads from the element's markdown body and serializes back to markdown; for allowlisted constructs, markdown → editor → markdown is stable (loading and immediately saving an already-canonical body changes nothing). Saving goes through the element tree and the canonical serializer from #2, rewriting only that page's file, exactly like a param edit.
No raw HTML, ever. Raw HTML appearing in the markdown source renders in the editor as visible literal text — it is never interpreted, never executes (no script/style/event-handler injection through body content), and never appears in the serialized output as markup. The editor's markdown output never contains raw HTML under any input. Other markdown constructs outside the field's allowlist (e.g. a table or an h1 in pre-existing content) degrade to their plain-text reading in the editor rather than rendering or being silently dropped.
Paste. Pasting from Word, Google Docs, or web pages maps recognizable formatting to the field's allowlist (bold stays bold if allowed) and strips everything else to plain text — no styles, classes, spans, comments, or embedded markup survive.
Links. The link affordance supports two modes: an external URL entered by hand, and an internal page chosen from the site's content tree, stored as its tree path. A minimal page list sourced from the content directory is sufficient for internal selection (the real tree UI is #4). Existing links can be edited and removed.
Inline shortcodes. A body that already contains inline shortcodes must survive rich-text editing uncorrupted. Note the engine's contract: text nodes containing raw
{{</{{%are a serialize error, so inline shortcodes cannot be flattened into body text — they must remain shortcode nodes in the saved tree. Their editor representation may be an inert placeholder in this issue; making them first-class chips is #8.Key interfaces:
Acceptance criteria:
Out of scope:
Closing as superseded: the maintainer has decided on a clean-slate reset of the project (issue #29, PR #30). The v1 implementation this issue builds on is being removed and the plan will be re-drawn from scratch; this spec is retired rather than actioned.