Story Block delivery renderers drop code, highlight and dotUnsupportedMark, and stop rendering nested marks
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Problem Statement
The Block Editor schema declares 10 marks — bold, 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:
- 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 carryingmarks: [code, bold]the React, Vue and classic-Angular renderers also drop the bold, because the unknown mark ends the recursion. dotUnsupportedMark— the placeholder introduced by #37313 — can legitimately reach storage (documented in that PR's own footnote: when the preserved payload no longer parses,restoreUnknownMarkkeeps 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.- 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
- In a Story Block field, type some text, select it, and apply inline code from the toolbar (always available).
- Optionally apply bold to the same selection so the node carries
marks: [code, bold]. - Save and publish.
- 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,highlightanddotUnsupportedMark -
coderenders as<code> -
highlightrenders as<mark>with no inline style — colour is the consuming site's CSS concern, keeping the output semantic and themeable -
dotUnsupportedMarkemits 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.vmas 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 (registeredHighlightin the editor — delivery was never updated to match) - NA — no Freshdesk ticket. Found by auditing the delivery layer against the editor schema.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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