RTE bodies: TipTap-core Solid wrapper with markdown round-trip and configurable allowlist #7

Closed
opened 2026-07-11 12:59:48 +02:00 by dominik.polakovics · 2 comments

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 fields with body.type: rte render the TipTap editor inside the element form; typing produces styled text, never syntax
  • Content round-trips: markdown → editor → markdown is stable for allowlisted constructs (integrates with #2's serializer)
  • Sidecar allowlist config controls the toolbar and enforcement (e.g. a field without headings offers/accepts none)
  • Paste from Word/Google Docs strips to the allowlist; raw HTML in markdown source renders as visible text in the editor, is never emitted, and never executes
  • Links support internal page selection (by tree path) and external URLs
  • h1 is not offered anywhere (reserved for page title)

Blocked by

## 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 fields with `body.type: rte` render the TipTap editor inside the element form; typing produces styled text, never syntax - [ ] Content round-trips: markdown → editor → markdown is stable for allowlisted constructs (integrates with #2's serializer) - [ ] Sidecar allowlist config controls the toolbar and enforcement (e.g. a field without headings offers/accepts none) - [ ] Paste from Word/Google Docs strips to the allowlist; raw HTML in markdown source renders as visible text in the editor, is never emitted, and never executes - [ ] Links support internal page selection (by tree path) and external URLs - [ ] h1 is not offered anywhere (reserved for page title) ## Blocked by - #3
Author
Owner

This was generated by AI during triage.

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 D12 reference 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:

  • The Solid TipTap wrapper component: markdown value + allowlist config in, edited markdown out; generic across fields.
  • The sidecar body-policy shape from #3, extended with the rte variant and its optional per-field allowlist; schema validation rejects unknown allowlist entries with an actionable error, consistent with #3's sidecar error handling.
  • A markdown ⇄ editor-document conversion layer, allowlist-aware in both directions; this is where the no-raw-HTML and degradation rules live, and it should be testable without a browser where feasible.
  • The existing #3 save contract — the RTE feeds the same element-save path; no parallel write path.

Acceptance criteria:

  • A body field declared rich-text in its sidecar renders the TipTap editor inside the element form; typing produces styled text, never markdown syntax; non-rte bodies are unchanged
  • Markdown → editor → markdown is stable for allowlisted constructs, and saving goes through the engine from #2, rewriting only that page's file
  • The sidecar allowlist controls both the toolbar and enforcement (shortcuts and paste included); a field without headings offers and accepts none; omitting the allowlist yields the default set (bold, italic, links, bullet/numbered lists, h2–h4)
  • Paste from Word/Google Docs/web strips to the allowlist; raw HTML in the markdown source renders as visible text in the editor, never executes, and never appears in serialized output; the editor never emits raw HTML
  • Links support internal page selection (stored as tree path) and external URLs; links can be edited and removed
  • h1 is not offered or accepted anywhere, regardless of field configuration
  • A body containing inline shortcodes survives an edit-and-save without corrupting them (they remain shortcode nodes in the tree)
  • The sample site has at least one element with a rich-text body exercising the default allowlist and one field with a restricted allowlist; round-trip and sanitization behavior is covered by tests

Out of scope:

  • Inline shortcode chips with popover forms (#8) — this issue only guarantees non-corruption
  • Structural element operations (#6) and the page tree UI (#4) — a minimal page list for internal links suffices
  • Constructs beyond the defined vocabulary: images in the RTE (media is #12), tables, code blocks, blockquotes, text alignment/color
  • The full v1 element library (#14) and theme styling (#15)
  • Collaborative editing, comments, or multi-step undo beyond what TipTap provides in-session
  • Git commits on save (#10) and locks/identity/CSRF (#13)
> *This was generated by AI during triage.* ## 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 D12` reference 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:** - The Solid TipTap wrapper component: markdown value + allowlist config in, edited markdown out; generic across fields. - The sidecar body-policy shape from #3, extended with the rte variant and its optional per-field allowlist; schema validation rejects unknown allowlist entries with an actionable error, consistent with #3's sidecar error handling. - A markdown ⇄ editor-document conversion layer, allowlist-aware in both directions; this is where the no-raw-HTML and degradation rules live, and it should be testable without a browser where feasible. - The existing #3 save contract — the RTE feeds the same element-save path; no parallel write path. **Acceptance criteria:** - [ ] A body field declared rich-text in its sidecar renders the TipTap editor inside the element form; typing produces styled text, never markdown syntax; non-rte bodies are unchanged - [ ] Markdown → editor → markdown is stable for allowlisted constructs, and saving goes through the engine from #2, rewriting only that page's file - [ ] The sidecar allowlist controls both the toolbar and enforcement (shortcuts and paste included); a field without headings offers and accepts none; omitting the allowlist yields the default set (bold, italic, links, bullet/numbered lists, h2–h4) - [ ] Paste from Word/Google Docs/web strips to the allowlist; raw HTML in the markdown source renders as visible text in the editor, never executes, and never appears in serialized output; the editor never emits raw HTML - [ ] Links support internal page selection (stored as tree path) and external URLs; links can be edited and removed - [ ] h1 is not offered or accepted anywhere, regardless of field configuration - [ ] A body containing inline shortcodes survives an edit-and-save without corrupting them (they remain shortcode nodes in the tree) - [ ] The sample site has at least one element with a rich-text body exercising the default allowlist and one field with a restricted allowlist; round-trip and sanitization behavior is covered by tests **Out of scope:** - Inline shortcode chips with popover forms (#8) — this issue only guarantees non-corruption - Structural element operations (#6) and the page tree UI (#4) — a minimal page list for internal links suffices - Constructs beyond the defined vocabulary: images in the RTE (media is #12), tables, code blocks, blockquotes, text alignment/color - The full v1 element library (#14) and theme styling (#15) - Collaborative editing, comments, or multi-step undo beyond what TipTap provides in-session - Git commits on save (#10) and locks/identity/CSRF (#13)
Author
Owner

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.

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.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#7
No description provided.