Skip to content

36659 feat(block-editor): dotcms-* rich-node vocabulary on HTML input (parity with the Markdown leg) - #36841

Open
hassandotcms wants to merge 4 commits into
mainfrom
36659-tiptaphtml-accept-the-custom-editor-fields
Open

36659 feat(block-editor): dotcms-* rich-node vocabulary on HTML input (parity with the Markdown leg)#36841
hassandotcms wants to merge 4 commits into
mainfrom
36659-tiptaphtml-accept-the-custom-editor-fields

Conversation

@hassandotcms

Copy link
Copy Markdown
Member

What

Closes #36659. HTML input now expresses the same rich nodes the Markdown leg gained in #36658 (dotContent, dotImage, dotVideo, youtube, aiContent, gridBlock, custom blocks), as namespaced elements — HTML is no longer the lossier save-path format.

  <dotcms-content identifier="2d5d1c4c-…" language-id="1"></dotcms-content>
  <dotcms-grid>{"type":"gridBlock","attrs":{"columns":[6,6]},"content":[…]}</dotcms-grid>
  <!-- dotcms:attrs {"textAlign":"center"} -->
  • Markup form: the seven fence labels as element names. Scalars ride as attributes, hyphenated because HTML lowercases attribute names (language-idlanguageId); the verbatim labels and dotcms-ai take the fence-identical JSON as element body (attributes can't carry a subtree, and JSON in an attribute corrupts on the first quote). The editor's own HTML was rejected as a model: dotContent has no parseHTML, and dotVideo/youtube parse from <video>/<iframe> — both in DROP_SUBTREE, which stays.

  • Shared validator: both carriers normalize into the same TiptapMarkdown.DotcmsFences (now package-visible), so parity holds by construction — same schema, node type, 64KB/depth caps, doc rejection.

  • Degrade, never throw: invalid body labels keep their payload as a visible codeBlock like an invalid fence; scalar labels drop; unknown dotcms-* stays transparent.

  • Sanitization does not widen: no blanket attribute copy, every URL through sanitizeUrl.

Behavior change

Two narrow routing fixes in MapToContentletPopulator: a value opening with <dotcms-*> used to store as literal text (HTML_START misses hyphenated tags) and now routes to HTML via a <dotcms- carve-out; and routing is decided on what follows leading dotcms:attrs comments, so comment-then-HTML reaches the HTML converter instead of losing the decoration. Stored JSON still passes through untouched; same status codes, no new rejections.

Verification

204 unit (51 new, incl. HTML≡Markdown parity suite + hostile attrs on the new elements; existing sanitization suite unchanged), 20 integration (5 new). Live pass: the HTML-created doc opens in the Block Editor with every rich node rendering, takes a real edit, and re-saves intact; the Markdown-fence sibling renders identically. OpenAPI + MCP guidance updated.

@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @hassandotcms's task in 8m 22s —— View job


Review: dotcms-* rich-node vocabulary on HTML input

I read the full diff — TiptapHtml.java (the bulk of the change), the shared DotcmsFences refactor in TiptapMarkdown.java, the routing changes in MapToContentletPopulator.java, the doc-string changes, and both test suites.

This is careful, well-tested work. The shared-validator approach (routing HTML element payloads through the same DotcmsFences.parse) is the right call for parity-by-construction, the sanitization is conservative (no blanket attribute copy, every URL through sanitizeUrl), and the routing carve-out in the populator is correctly scoped to the known label set so nothing that routes today changes route. The parity suite and hostile-input tests are strong. Only two minor, non-blocking items:

New Issues

  • 🟡 Medium: dotCMS/src/main/java/com/dotcms/tiptap/TiptapHtml.java:310-319Pending dotcms:attrs decoration leaks to the next block when a scalar <dotcms-*> element fails validation. In the success path (node != null) the decoration is consumed via st.takePendingAttrs(); in the drop path (else branch, line 317) it is not, so a pending decoration set by a preceding `` comment survives and decorates whatever block comes next. Example: <dotcms-content></dotcms-content><p>x</p> — the `` drops (no `identifier`), and `textAlign` lands on the `

    x

    ` paragraph instead of being consumed. This diverges from the Markdown leg, where an invalid scalar fence degrades to a visible codeBlock that the decoration decorates (so it never leaks forward). Consuming the pending attrs in the drop branch would restore parity. Edge case (decoration + invalid scalar element), not covered by a test. Fix this →

  • 🟡 Medium: core-web/apps/mcp-server/src/tools/execute.ts:68MCP HTML note omits dotcms-ai from the body-carrier list. The note says only "dotcms-grid and dotcms-node take the same JSON object as the element's text body," but the code's DOTCMS_BODY_LABELS (TiptapHtml.java:104) is {dotcms-ai, dotcms-grid, dotcms-node}, and the sibling WorkflowResource.BLOCK_EDITOR_FIELD_NOTE correctly lists all three (WorkflowResource.java:263). An agent following the MCP guidance would try to author dotcms-ai as an attribute element and drop the payload. (dotcms-ai is also absent from the Markdown label list at lines 62-67 — a pre-existing gap, but this PR is the natural place to close it in both legs.) Fix this →

Nothing blocking. The routing logic (skipLeadingDotcmsAttrsComments + the unterminated-comment re-check), the depth/size caps inherited from DotcmsFences, the doc-smuggling rejection, and the self-closed-element child-recovery are all correct and covered by tests.

@hassandotcms
hassandotcms force-pushed the 36659-tiptaphtml-accept-the-custom-editor-fields branch from 3f5c5bd to 3317152 Compare July 31, 2026 15:49
…legs

Prepares the Markdown leg's #36658 vocabulary for reuse by the HTML leg
(#36659) so both save-path converters validate against the same code and
cannot drift into producing different nodes for the same rich content:

- DotcmsFences becomes package-visible, with a parse(label, JsonNode)
  overload for payloads assembled from element attributes (no raw body to
  size-cap; the String overload still caps before parsing).
- parseDecorationAttrs and its limits move out of the commonmark-specific
  TiptapBuilder to the class level, so the dotcms:attrs comment can be
  validated identically wherever it arrives.

No behavior change: the Markdown suites pass untouched.
…input

Closes the parity gap #36658 opened: HTML input could not express an
embedded contentlet, image, video, YouTube embed or grid, so a document
holding them could not be edited over the HTML leg (#36659).

The same seven labels the Markdown fences use are now namespaced custom
elements, validated by the very same DotcmsFences code, so both carriers
produce identical documents:

- Scalar payloads ride as attributes, hyphenated because HTML lowercases
  attribute names (language-id, mime-type, text-wrap) and mapped back to
  the camelCase attrs the editor expects; the flattened lowercase spelling
  is accepted too.
- dotcms-ai, dotcms-grid and dotcms-node take the fence-identical JSON as
  the element's text body, keeping the size cap, depth cap and doc
  rejection that the verbatim labels rely on.
- <!-- dotcms:attrs {...} --> comments decorate the next block with the
  Markdown leg's exact semantics: next-block-only, last-wins, structural
  attrs win on collision, consumed but never applied to a rich node.

Degrade, never throw: a body label keeps its payload as a visible
codeBlock exactly like an invalid fence, a scalar label drops (its
attributes carry no text to preserve), and an unknown dotcms-* element
stays transparent like any other unknown tag. A self-closed element does
not lose the siblings HTML parsing swallows into it.

Sanitization does not widen. DROP_SUBTREE is unchanged (YouTube and video
ride a sanitized src, never a real iframe/video element), attributes are
read by name per element rather than copied, and every URL-bearing field
passes sanitizeUrl.
…ML leg

The save path's HTML detection regex only matches spec-simple tag names,
so a value opening with a hyphenated custom element fell through to the
Markdown converter and was stored as literal text — the new vocabulary was
unreachable for exactly the values that lead with it.

Two routing corrections, both narrow:

- A leading <dotcms-*> element routes to the HTML converter, mirroring the
  startsWithDotcmsAttrsComment carve-out rather than broadening HTML_START
  to every hyphenated tag (which would silently reroute unrelated values).
- The decision is now made on what FOLLOWS any leading dotcms:attrs
  comments. Before Markdown they still reach the Markdown converter; before
  HTML they now reach the HTML converter, which honors the decoration
  instead of dropping it with the markup. An unterminated comment cannot be
  skipped, so it keeps the Markdown route it had.

Integration coverage: an HTML value opening with <dotcms-content> stores
and reads back hydrated through checkin; every label stores from one HTML
value; comment-then-HTML routes and decorates; an HTML update carrying the
stored rich block over produces no replacement warning; the unterminated
comment keeps its route.
… contract

The fire endpoints described the rich-node vocabulary as Markdown fences
only, so the HTML spelling was undiscoverable for the non-interactive
clients it exists for.

- BLOCK_EDITOR_FIELD_NOTE gains the element forms, the hyphenated-attribute
  rule, the body-JSON escaping rule, and the always-close-the-tag trap
  (HTML parsing ignores the / in <dotcms-video ... /> and swallows what
  follows).
- The note becomes public and is attached to ContentResource#saveDraft,
  which funnels through the same populator seam but documented none of it.
- openapi.yaml regenerated from the annotations.
- MCP execute guidance updated so agent clients can author either carrier.
@hassandotcms
hassandotcms marked this pull request as ready for review July 31, 2026 17:18
@hassandotcms
hassandotcms force-pushed the 36659-tiptaphtml-accept-the-custom-editor-fields branch from 3317152 to cce1039 Compare July 31, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

TiptapHtml: accept the same rich-node vocabulary on HTML input (parity with the Markdown leg)

1 participant