dotCMS / dotCMS/core

Story Block delivery renderers drop code, highlight and dotUnsupportedMark, and stop rendering nested marks

Open
#37,621 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

dotCMS: Block Editor OKR : Customer Support Team : Maintenance Type : Defect
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

Problem Statement

The Block Editor schema declares 10 marksbold, code, dotUnsupportedMark, highlight, italic, link, strike, subscript, superscript, underline (confirmed in the running app by QA on #37175; the legacy editor registers the same set).

There are five delivery renderers and every one of them knows 7:

Renderer File Coverage On an unknown mark
Angular SDK (classic) core-web/libs/sdk/angular/src/lib/components/dotcms-block-editor-renderer/blocks/text.component.ts:125 7/10 @default { {{ text }} }stops recursing
React SDK core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/blocks/Texts.tsx:128 7/10 if (!Component) { return text; }stops recursing
Vue SDK core-web/libs/sdk/vue/src/lib/components/DotCMSBlockEditorRenderer/components/blocks/TextBlock.vue:19-26,39-45 7/10 v-if="tag" -> falls to {{ text }}stops recursing
VTL macro dotCMS/src/main/webapp/WEB-INF/velocity/VM_global_library.vm:7 (#macro(renderMarks)) 7/10 ignores the mark (degrades correctly — flat loop, not recursive)
Angular SDK (semantic) .../dotcms-block-editor-renderer-semantic/dotcms-block-editor-renderer-native.component.html:355-362 7/10 skips the mark and keeps recursing — correct

Missing in all five: code, highlight, dotUnsupportedMark.

Three concrete consequences:

  1. The new editor ships an always-available inline-code button (toolbar.component.html:107 -> toggleCode(), ungated). An author applies it, sees it in the editor, and the front end does not paint it. Worse: on text carrying marks: [code, bold] the React, Vue and classic-Angular renderers also drop the bold, because the unknown mark ends the recursion.
  2. dotUnsupportedMark — the placeholder introduced by #37313 — can legitimately reach storage (documented in that PR's own footnote: when the preserved payload no longer parses, restoreUnknownMark keeps the placeholder so the raw payload still round-trips). When it does, those three renderers stop walking that node's marks. The fix that prevented editor-side data loss opened a formatting-loss path in delivery.
  3. The correct behaviour is already written. The semantic Angular renderer does exactly what the other three must do, with the reasoning in a comment ("Unknown mark type: skip it but keep rendering any marks beneath."). This is not a design question; it is an unpropagated fix.
Why VTL is the highest-risk of the five, and how not to break it

The other four are TypeScript with a test suite behind them. The Velocity path has neither.

The macro is two mirrored loops, not one. #renderMarks emits every opening tag in forward order ($range = [$start..$end], lines 13-44) and every closing tag in reverse ($range = [$end..$start], lines 46-70). A mark added to the opening loop and forgotten in the closing loop does not "render without the tag" — it produces malformed, unbalanced HTML on every published page carrying that mark. This is the single most likely way to break production while fixing this.

$start and $end are set in the first block and reused in the second; they are Velocity globals with no re-declaration. The current code works, but anything that moves or conditionalises the first #if($content.marks) silently breaks the second.

Enforce the parity textually (D3). Do not add a Velocity runtime or a Java integration test — both are out of scope and there is no .vm harness in core-web. Instead add a spec that reads the .vm file as a string and asserts that every mark name in the allowlist appears in both loops with its matching open and close tag. Crude, and exactly the right check here: it fails loudly on the one mistake that matters, needs no runtime, and lives beside the capability-key invariants added in #37609.

Steps to Reproduce

  1. In a Story Block field, type some text, select it, and apply inline code from the toolbar (always available).
  2. Optionally apply bold to the same selection so the node carries marks: [code, bold].
  3. Save and publish.
  4. Render the field on the front end through the React, Vue or classic Angular SDK renderer.

Expected: the text renders as <code> (and <strong> where bold was applied).

Actual: no <code> element, and on the combined case the <strong> is dropped too — the unknown mark ends mark recursion.

For highlight: author content in the legacy editor with a highlight mark, then render it through any of the five renderers — the <mark> never appears.

Acceptance Criteria

  • All five renderers handle code, highlight and dotUnsupportedMark
  • code renders as <code>
  • highlight renders as <mark> with no inline style — colour is the consuming site's CSS concern, keeping the output semantic and themeable
  • dotUnsupportedMark emits no element (it is visually neutral by design) but does not stop mark recursion
  • React, Vue and Angular-classic continue recursing through remaining marks when they meet a mark they do not know, matching the semantic Angular renderer's existing behaviour — marks: [code, bold] renders both
  • An unknown/future mark name still degrades to plain text without dropping sibling marks or aborting the node
  • VTL: each of the three marks is added to both the opening loop and the closing loop of #renderMarks, in correct nesting order
  • D3 — a spec reads VM_global_library.vm as a string and fails if any allowlisted mark is missing from either loop, or if its open and close tags do not correspond
  • Manual verification that published VTL pages carrying each mark emit balanced HTML (validator or DOM parse, not eyeballing)
  • Unit tests per renderer covering: each new mark alone, each new mark combined with a known mark, and an unknown mark between two known ones

dotCMS Version

main @ 667fc831ee (2026-09-17). Affects every consumer of the Angular / React / Vue SDK renderers and the VTL macro, regardless of FEATURE_FLAG_NEW_BLOCK_EDITOR.

Severity

Medium - Some functionality impacted

Links

  • Parent / umbrella: https://github.com/dotCMS/core/issues/37601 (Defects D and D3)
  • Related: #37313 (introduced dotUnsupportedMark), #37149 (registered Highlight in the editor — delivery was never updated to match)
  • NA — no Freshdesk ticket. Found by auditing the delivery layer against the editor schema.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the semantic Angular renderer at dotcms-block-editor-renderer-native.component.html:355-362 as the existing recursion reference, then inspect the React Texts.tsx, Vue TextBlock.vue, classic Angular text.component.ts, and VM_global_library.vm renderMarks macro. Run the existing renderer unit suites and add the requested VTL string-based spec beside the capability-key invariants. Done means all five renderers emit code and mark correctly, skip dotUnsupportedMark without stopping recursion, preserve unknown-mark siblings, and keep VTL output balanced.

Written by the indexing model from the issue text.

Assessment

Tech stack
angular, react, typescript
Domain
frontend, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.