fix(super-editor): render and round-trip w:smartTag content (SD-2647)#3546
Conversation
w:smartTag is a transparent inline wrapper around EG_PContent (ECMA-376 §17.5.1.9). Before this change it fell through to the v2 passthrough path, which wrapped the runs in a display:none span - so any content authored inside Word's smart tags (countries, places, dates, names) disappeared on render. Add a smartTag PM container node (non-atomic, content: inline*, transparent renderDOM) plus a v3 translator that preserves w:element, w:uri, and the optional w:smartTagPr child for round-trip. A small v2 importer entity intercepts w:smartTag before the passthrough catcher and delegates to the v3 translator. The pm-adapter inline converter mirrors structuredContent / bookmarkStart: walk children with inherited marks and forward inlineRunProperties so SD-2781 run-level bidi/script metadata survives. Sdt run-flattening recognises w:smartTag as a run-level wrapper so nested smartTags inside SDT content keep working. Container node, not a mark: smartTags nest in real documents (e.g. <w:smartTag element="place"><w:smartTag element="country-region">), and ProseMirror marks of the same type don't stack. Verified on the IT-945 customer file: 439 smartTag elements render (matches 439 XML opens), 22/22 sampled country names visible, 0 passthroughInline wrappers. Tests: 11 translator + 4 extension; full super-editor / pm-adapter / layout-engine suites green; workspace check:types clean.
|
The ecma-spec MCP tools keep getting denied — I'll proceed from ECMA-376 §17.5.1.9 directly. Status: PASS I reviewed the smartTag translator against ECMA-376 §17.5.1.9 (CT_SmartTagRun) and the PR is spec-compliant. What I checked:
No non-existent attributes, no missing required-attr handling that would break valid input, no incorrect defaults, no spec violations. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7c08072536
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
cubic analysis
2 issues found across 14 files
Linked issue analysis
Linked issue: SD-2647: Bug: Render and round-trip content wrapped in w:smartTag
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | Add smartTag ProseMirror node (inline non-atomic container with attrs element, uri, smartTagPr and transparent renderDOM) | The PM node implementation, attributes and renderDOM placeholder are present and unit-tested. |
| ✅ | Add w:smartTag v3 translator (encode/decode, preserve w:element/w:uri and capture/re-emit , route full EG_PContent) | Translator implemention includes encode/decode, attribute handlers, smartTagPr capture/re-emit and has unit tests covering attribute mapping and smartTagPr round-trip. |
| ✅ | Add pm-adapter inline converter and register it so smartTag children are visited into runs | A smart-tag inline converter was added and registered in the paragraph converter registry, mirroring existing structured-content handling. |
| ✅ | Add importer handler to capture before passthroughInline | Importer handler for w:smartTag was added and wired into the default node list handler so smartTag no longer falls through to passthroughInline. |
| ✅ | Exporter emits and re-emits preserved when present | Decoder emits w:smartTag with decoded attributes and re-inserts cloned smartTagPr when present; tests assert the smartTagPr is re-emitted and cloned (not same reference). |
| ✅ | Update SDT flatten helper to include w:smartTag so SDT-nested smartTags flatten correctly | RUN_LEVEL_WRAPPERS was extended to include 'w:smartTag'. |
| Visible content wrapped by w:smartTag renders correctly in paragraphs / tables / headers & footers / inside SDTs; nested smartTags preserve both wrappers | The code changes implement transparent rendering and pm-adapter/import pipeline changes needed for these contexts. Unit tests are present for node behavior and translator; however there are no end-to-end integration tests in the diff explicitly exercising paragraphs/tables/headers/footers or SDT nesting. The PR body reports manual verification (IT-945) showing sampled country names render and passthroughInline counts drop to 0. | |
| ✅ | Round-trip export/import preserves w:element, w:uri and | Translator encode/decode explicitly map attributes and preserve smartTagPr; unit tests validate attribute mapping and that smartTagPr is re-emitted as a clone, covering the round-trip logic at translator level. |
| ❌ | Exported .docx opens in Microsoft Word without a corruption prompt | No automated integration test or explicit export-into-.docx + Word-open verification is present in the diff. The PR description includes manual rendering checks but does not explicitly state Word-open results for exported files. |
| ✅ | No regression: passthroughInline remains atomic + hidden and unknown content is still preserved by passthroughInline | The PR leaves passthrough implementation untouched and adds a handler to avoid routing smartTag content into passthrough. The PR description reports passthroughInline wrapper count dropping to 0 in manual checks, indicating no regression and that visible content is no longer hidden behind passthroughInline. |
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…gPr in SDT flatten (SD-2647) The initial commit wired w:smartTag for import + render but left two holes on the export path: 1. exporter.js's exportSchemaToJson dispatch table had no smartTag entry, so the v3 decoder was unreachable. Any imported paragraph containing a smartTag would round-trip to null and the entire wrapped subtree would be dropped on save. 2. convertSdtContentToRuns added w:smartTag to RUN_LEVEL_WRAPPERS but then recursed on the full element list, so a w:smartTagPr nested inside an SDT-wrapped smartTag fell through the recursion's catch-all and was rewrapped as a fake <w:r>. That produces invalid OOXML and corrupts the smartTagPr round-trip in that nesting. Fixes both: - Import wSmartTagNodeTranslator and register it under `smartTag` in the export router. - Partition w:smartTagPr out of the recursive call when the wrapper is w:smartTag, and prepend it back to the wrapper's elements. Kept scoped to the w:smartTag + w:smartTagPr pair; other run-level wrappers are unaffected. New tests: - smartTag-export-routing.test.js exercises exportSchemaToJson directly so the routing wiring cannot regress. - convert-sdt-content-to-runs.test.js asserts w:smartTagPr stays as smartTag metadata instead of becoming a fake w:r. Both new tests fail on the previous commit and pass with this fix. super-editor: 13525 pass / 13 skip. Workspace check:types clean.
…trip (SD-2647) The previous export-routing tests proved the wrapper and attrs reach the XML, but didn't assert that the child runs survived. Add a tiny recursive text collector and assert the inner "Brazil" text is present in both the bare and smartTagPr-preserving cases, so a future regression that loses the smartTag content (without losing the wrapper) would still fail the test.
… (SD-2647) Two test gaps surfaced during PR review that were not blocking by themselves but together left the customer-bug surface unprotected. smartTagImporter.test.js pins the v2 importer bridge invariant: the passthrough handler refuses any node present in v3 registeredHandlers, so without the smartTagNodeEntityHandler claiming w:smartTag, the node silently falls off the end of the reducer chain. Cubic and Codex both read this as "duplicate registration"; the regression-guard test makes the bridge's necessity explicit and reproducible. The behavior round-trip spec exercises the full chain end-to-end with a sanitized real-Word fixture (OOXML surgery on the IT-945 customer .docx, not regenerated through SuperDoc's save path, so the input does not launder through the implementation under test). Asserts: - input fixture already carries w:tc / w:smartTag / w:element / w:smartTagPr (sanity gate before assertions), - table-cell country names render visibly (the customer bug surface, previously hidden as passthroughInline), - zero-edit exportDocx output still contains w:smartTag with the original w:element, the inner country text, and w:smartTagPr. Both tests run green against the SD-2647 fix; the round-trip spec takes ~4s, the importer unit ~2ms. Deferred per review: pm-adapter forwarding test (only common.test.ts currently asserts inlineRunProperties at the helper level; structured- content and other transparent wrappers have no forwarding tests, so a smartTag-only forwarder would set a new bar inconsistent with peers). Layout snapshot + visual pixel tests deferred too; smartTag is transparent metadata, the visible-text behavior assertion already covers what would change.
### Bug Fixes - copy-pasted text in suggestion mode (#3576) - center virtualized matches after mount in find nav (SD-3315) - stop find navigation jumping to the reverted caret (SD-3315) - honor the focus() contract + fix dangling docs reference (SD-3312) - don't re-center visible matches on find navigation (SD-3315) - report 'zoom' not 'mixed' for a zoom repaint (SD-3311) - wire pointer-source tracking on all init paths; update export snapshot - reset state on unload, dedupe + export payload types, add core tests - skip empty block SDT content selection - type modules.contentControls exactly (no pass-through index) - allow block SDT wrapper deletion to follow lock rules - promote image-bearing inline SDT wrappers to inline-block - keep block sdt fill behind content - hide block sdt fills in output modes - paint block SDT background on chrome layer - use logical inset for inline SDT label position - anchor inline SDT label to start of chrome - route smartTag through export and preserve smartTagPr in SDT flatten (SD-2647) - include inline SDT chrome width in block SDT bounds - keep block SDT chrome at full fragment width - render and round-trip w:smartTag content (SD-2647) - decouple base64 image helper imports - preserve block ids during metadata updates - ignore covered sdt label clicks - sync block sdt label selection updates - show empty SDT placeholder text in viewing and print modes - skip empty sdt scan on arrow right - preserve permission-only sdt placeholders - preserve comment-only sdt placeholders - preserve empty sdt bookmark placeholders - collapse hidden sdt placeholder text - keep sdt placeholder pm range atomic - trust empty sdt paragraph conversion - keep vanished sdt paragraph side effects - preserve vanished block sdt paragraphs - suppress hidden block sdt chrome - keep remeasured sdt placeholder atomic - transform sdt placeholder measure - remeasure sdt placeholders - hide sdt placeholders in print - hide sdt placeholders in viewing - ignore collapsed inline sdt cut - expose block sdt appearance - hide empty block sdt placeholder - align empty block sdt caret - use measured width for empty SDT placeholders - size SDT block labels to content width - collapse selection on sdtContentLocked delete - allow history transactions through sdt lock - drop unreachable move fallback - target marker-only textblock end - ignore empty block sdt key targets - cap block sdt label width - handle sdt marker gaps and block atoms - skip hidden field annotations in sdt navigation - skip hidden metadata sdt markers - skip hidden block sdt markers - handle marker-only sdt paragraphs - keep visible atoms in sdt navigation - skip hidden sdt navigation markers - target nearest sdt cursor position - respect inline atoms in sdt navigation - handle empty block sdt navigation - avoid restoring dragged block to its source position - exclude sdt chrome labels from caret position lookup - keep text-align enabled in locked SDT paragraphs - block disabled toolbar execution - guard unlisted locked toolbar commands - block locked sdt toolbar execution - reject malformed base64 image data URIs - reuse colliding data uri media targets - enforce upload byte cap for data uris - avoid non-image data uri extensions - reject raw raster data uri dimensions - validate oversized async svg images - register preset raster data uris in place - narrow sdt metadata overrides - normalize image data uri extensions - centralize image data url policy - reuse target image relationships - validate in-place svg payloads - validate field annotation data uri exports - reject malformed data uri files - avoid duplicate image rids - block raw raster data uri exports - reject malformed svg data uri payloads - warn on skipped image exports - reject separatorless data uri files - block non-image data uri exports - read svg data uri dimensions - share data uri media parsing - skip invalid data uri image targets - normalize svg data uri filenames - validate in-place svg image data - guard non-base64 data uri exports - export field annotation svgs as svg - allow non-base64 SVG data URLs in image rendering - mirror in-place image media to parent - decode non-base64 data URI exports - extract shared hash helpers - reuse data URI image exports - support non-base64 data URI images in registration - scope inline SDT placeholder to structuredContent metadata - register sized SVG data URI images without canvas processing - persist data URI images set via setPresetContent - share SDT lock predicates - version inline image metadata - align RTL SDT chrome to text - size SDT chrome for justified lines - honor ancestor image SDT locks - dirty inline image SDT changes - align SDT chrome within paragraph width - preserve SDT chrome continuation offsets - offset block SDT chrome for indents - suppress SDT pseudo hover in viewing mode - allow top-aligned inline images - fit block SDT chrome to actual content width - bottom-align text on lines with inline images - keep block SDT chrome and inline images out of paragraph geometry - disable image resize inside content-locked SDTs - detect inline image run changes in paragraph diff - mark block SDT selected when contained image is selected - select inline SDT content as text on Delete - intercept beforeinput insertText at inline SDT boundaries - delete contentLocked SDT wrapper in one step - bump sdBlockRev on ancestors of inline edits - select inline SDT content as text on Backspace - select inline SDT on Backspace at start of following run (SD-3165) - share structured content chrome label set - resolve block labels at node boundary - use contract label selectors - clear label gesture state on cancel - scope label clicks to owning editor - defer label selection to mouseup so native drag still fires - share structured content label classes - avoid deferred block label retry - select labels with active editor - focus editor after label selection - correct cursor placement and label interactions for structured content - handle cell-level SDT in vMerge column lookup (SD-3289) - preserve cell-level SDT wrapping table cells (SD-3289) - preserve recipient identity attrs on replay (SD-3279) - accept legacy fingerprints in compare/apply (SD-3279) - drop misleading pnpm run type-check hint from audit - structural-fail on missing dist + sync check-jsdoc header - strip session-local sdBlockId from diff fingerprint (SD-3279) - autoFit table width overflow from cell preferences (#3522) - flip public-method-coverage to strict-zero gate (SD-673) - flip jsdoc-hygiene-ts to strict-zero gate (SD-673) - rename jsdoc-hygiene-ts self-tests + wire into CI (fixes vitest discovery on #3511) - jsdoc-hygiene-ts handles private-identifier symbols + README - more bugs - add ui for overlapping delete, other fixes - replacement pair - remaining collab bugs - coalesce tracked inserts across run gaps - restore tracked change comment interactions - expose tracked mark predicate option - more cases - tc fixes - collab mode bug - floating comments fixes - make jsdoc-hygiene-ts baseline key line-independent + update wrapper docs - preserve tab underline via runProperties fallback in collab ### Changes - Merge branch 'main' into caio/sd-3315-find-replace-scroll - Merge pull request #3509 from superdoc-dev/artem/SD-3232 - Merge pull request #3555 from superdoc-dev/luccas/delete-image-content-locked-sdt - Merge branch 'main' into artem/SD-3159 - Merge pull request #3550 from superdoc-dev/luccas/sd-3302-bug-sdt-in-template-builder-shows-grey-background-highlight - Merge branch 'main' into artem/SD-3159 - SD-2676 - fix: table selection not providing a feedback (#3508) - Merge pull request #3549 from superdoc-dev/luccas/left-align-inline-sdt-label - Merge pull request #3546 from superdoc-dev/caio/sd-2647-bug-render-and-round-trip-content-wrapped-in-wsmarttag - Merge remote-tracking branch 'origin/stable' into sync/stable-to-main-20260527-230540 - Merge pull request #3539 from superdoc-dev/caio-pizzol/sd-3289-preserve-cell-level-sdt - Merge pull request #3527 from superdoc-dev/caio-pizzol/sd-3279-strip-sdblockid-from-diff-fingerprint - Merge branch 'main' into caio-pizzol/SD-js-contract-owner-audit - Merge pull request #3531 from superdoc-dev/caio-pizzol/SD-docs-snippet-typecheck-2 - Merge pull request #3526 from superdoc-dev/caio-pizzol/SD-runtime-payload-tests - Merge pull request #3521 from superdoc-dev/caio-pizzol/SD-public-method-coverage-strict-zero - Merge branch 'stable' - Merge pull request #3485 from superdoc-dev/artem/SD-3200 - Merge pull request #3517 from superdoc-dev/caio-pizzol/SD-jsdoc-hygiene-zero-flip - Merge pull request #3513 from superdoc-dev/caio-pizzol/SD-jsdoc-hygiene-cleanup - Merge branch 'stable' - Merge pull request #3511 from superdoc-dev/caio-pizzol/SD-jsdoc-hygiene-scanner - Merge pull request #3435 from superdoc-dev/nick/sd-3220-overlapping-suggestion-contract - Merge pull request #3429 from superdoc-dev/artem/underlined-tab-collab ### Documentation - note content controls in entityAt hit types (SD-3313) - clarify data uri buffer conversion - document image data uri helpers - clarify image registration comments - sync README exit semantics for js contract-owner audit - clarify wrapper-stages prose + add ts-jsdoc to check:public summary ### Features - add ui.contentControls.focus to place the caret in a control (SD-3312) - add ui.viewport.observe geometry-invalidation signal (SD-3311) - add ui.contentControls.scrollIntoView (SD-3310) - add activePath (full active stack) to content-control:active-change - expose public sdt events - select adjacent block SDT content at textblock boundaries - inherit run styles in empty block SDT placeholders - render placeholder text for empty SDTs - move caret into following block sdt on delete - move caret into preceding block sdt on backspace - disable mutation toolbar controls inside content-locked SDTs - render empty inline SDTs as a visible placeholder - add modules.contentControls.chrome - js contract-owner audit, report-only (SD-673) - add snippet typecheck for editor/superdoc/** + fix stale examples (SD-673) - add non-hover field color for sdt (#3506) - comments and tc on small screen (#3446) - overlapping tracked changes - add type-bearing JSDoc hygiene gate for .ts source (SD-673) - add anchored metadata orphan status ### Tests - resolve handleBase64 source path from package or repo root - cover locked block SDT Delete selection - cover nested block SDT boundary selection - update SDT keymap chain coverage - cover contentControlsChrome plumbing; clarify chrome-none comment - assert chrome-none hover suppression and cascade order - add v2 bridge unit + round-trip behavior coverage (SD-2647) - assert smartTag child text survives export round-trip (SD-2647) - cover nested block sdt navigation - roundtrip mixed image block sdts - cover image data uri length boundary - repaint saved sdt images through painter - cover structured content image edges - cover inline image diff fields - cover locked inline SDT beforeinput - cover inline SDT Cmd+X selection - cover inline SDT content Delete flow - clarify inline SDT boundary lock comment - cover two-step inline SDT Backspace - cover inline SDT selection meta escape - use real production payload for list-definitions-change bridge (SD-673) - tighten list-definitions-change bridge assertion (SD-673) - pin list-definitions-change bridge and DELETED comments shape (SD-673) - pin runtime event payload shapes (SD-673) ### Refactoring - consolidate type imports in dom painter - share block sdt navigation helpers - share box model between block and inline sdt labels - share structured content predicates - wrap shared tryDecodeDataUriText re-export - reuse shared data uri export policy - share image relationship export lookup - centralize image data uri parsing - share data uri text decoding - trim data uri metadata fields - extract CHECKED_FILES to shared module + drop audit from wrapper - drop list-item fragment renderer (SD-2851) (#3269) - drain 85 type-bearing JSDoc entries from .ts source (SD-673) ### Performance - avoid scanning data uri media ### Chores - 1.37.0 [skip ci] - fix import breaking delinstrtext orphans (#3535) - fix tests (#3533) - fixes - more fixes - run generate:all and fixes - more fixes - soec fixes - type fixes - ui and more - fix regression - type fixes - more fixes - more fixes - ci fixes - type fixes - add dispatch test for collab bug - review fix, type fix - add tests for metadata issue - tests for review issues
|
🎉 This PR is included in superdoc-cli v0.15.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.14.0 |
|
🎉 This PR is included in @superdoc-dev/mcp v0.10.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.38.0 The release is available on GitHub release |
|
🎉 This PR is included in @superdoc-dev/react v1.9.0 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.10.0 |
Summary
smartTagPM container node for<w:smartTag>so children render instead of falling into hiddenpassthroughInline.w:element,w:uri, andw:smartTagPr.Verification
pnpm check:types: cleanpassthroughInlinecount drops to 0Linear: SD-2647 (under SD-3298). Design rationale in the ticket.