feat(ui): metadata-id geometry on ui.metadata (SD-3204)#3379
Conversation
Adds `ui.metadata.getRect({ id })` and `ui.metadata.scrollIntoView({ id })`
keyed on the metadata id (= the value passed to
`editor.doc.metadata.attach`, or the SDT's w:tag underneath). The
handle hides the metadata-id β SDT-node-id β painter-geometry bridge
that the SD-3208 demo had to compose by hand from
`useSuperDocContentControls` + a tagβnodeId map +
`ui.contentControls.getRect`.
Failure mapping reuses the existing ViewportRectResult union:
empty id β `invalid-target`; unknown id (no matching
`properties.tag` in cc.items) β `unresolved`; SDT present but
unpainted β whatever `contentControls.getRect` returns
(`not-mounted` / `not-ready`) propagates as-is.
`scrollIntoView` resolves the id via
`editor.doc.metadata.resolve`, converts the SelectionTarget into a
TextTarget (same-block is one segment, cross-block is two collapsed
endpoints β defensive, since metadata v1 anchors are same-block),
and forwards to `ui.viewport.scrollIntoView`. nodeEdge endpoints
fail with `{ success: false }` rather than approximating.
No `getRects` (`ViewportRectResult.success.rects[]` already
exposes the per-line array), no namespace param (`attach` enforces
globally unique ids), no React hook, no mutation helpers β those
wait for second-customer signal.
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c762e597bf
βΉοΈ 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".
Codecov Reportβ All modified and coverable lines are covered by tests. π’ Thoughts on this report? Let us know! |
β¦data.* (SD-3204)
Anchored-metadata uses an inline SDT's `w:tag` to mark anchors in
the body, but `w:tag` is not reserved for metadata β an imported
DOCX can carry Word-authored content controls whose tag happens to
match a metadata id. `editor.doc.metadata.resolve` previously
matched on tag alone, so foreign controls would resolve as if they
were metadata anchors and any consumer (including `ui.metadata.*`)
would be steered at an unrelated control.
Fix at the source: `metadataResolveWrapper` now requires both
halves of the anchor β the SDT in the body AND a payload entry in a
customXml part β to agree before returning a non-null result.
Mirrors what `metadata.get` already does for payload reads.
Defensive UI-layer gate: `ui.metadata.getRect` and
`ui.metadata.scrollIntoView` both call `editor.doc.metadata.get`
first and short-circuit on null. Keeps the UI handle symmetrical
for direct callers that bypass `resolve` and protects against the
same class of bug if a future source-side change widens `resolve`.
Tests:
- anchored-metadata-wrappers: foreign SDT with matching w:tag and
no payload β `metadata.resolve` returns null.
- ui.metadata.getRect / scrollIntoView: same scenario β reports
`unresolved` / `{ success: false }` without delegating to
viewport.
`hasMetadataPayload` used `!== null` against an `unknown | null` structural return type. Production `metadata.get` always returns null on miss, so this was correct for the runtime path, but a stub or adapter returning `undefined` would have slipped through. Switched to `!= null` so both shapes gate the same way.
|
π This PR is included in superdoc-cli v0.12.0 The release is available on GitHub release |
|
π This PR is included in superdoc-sdk v1.11.0 |
|
π This PR is included in @superdoc-dev/mcp v0.7.0 The release is available on GitHub release |
|
π This PR is included in superdoc v1.35.0 The release is available on GitHub release |
|
π This PR is included in @superdoc-dev/react v1.6.0 The release is available on GitHub release |
|
π This PR is included in vscode-ext v2.7.0 |
Adds
ui.metadata.getRect({ id })andui.metadata.scrollIntoView({ id })keyed on the metadata id β the value the consumer passed toeditor.doc.metadata.attach. Hides the metadata-id β SDT-node-id β painter-geometry bridge that the SD-3208 demo'sCitationHighlightshad to compose by hand fromuseSuperDocContentControls+ a tagβnodeId map +ui.contentControls.getRect.What the handle does
getRect({ id })reads the cached content-controls slice, finds the item whoseproperties.tag === id, and delegates toui.contentControls.getRect({ id: <SDT node id> }). Return shape isViewportRectResult, identical to the rest of theui.*.getRectfamily.scrollIntoView({ id, block?, behavior? })callseditor.doc.metadata.resolvefor theSelectionTarget, converts it to aTextTarget(the shapeui.viewport.scrollIntoViewaccepts), and forwardsblock/behaviorunchanged.Failure mapping
Reuses the existing
ViewportRectResultunion β consumers learn one error model acrossui.viewport,ui.contentControls, andui.metadata.id: ''{ success: false, reason: 'invalid-target' }idnot incc.items(no matchingproperties.tag){ success: false, reason: 'unresolved' }contentControls.getRect('not-mounted'/'not-ready')scrollIntoViewreturns{ success: false }for unknown ids and fornodeEdgeendpoints (no cleanTextTargetrepresentation) rather than scrolling to an approximation.What's out
getRectsβViewportRectResult.success.rects[]already exposes the per-line array; a sibling method with the same return shape would just add API noise.namespaceparam βmetadata.attachenforces globally unique ids within a document.ui.metadata({ namespace })handle, no React hook, no attach helpers. Those wait for second-customer signal that composing from primitives is too awkward.Cross-block scroll
Anchored metadata v1 attaches over same-block text ranges only, so the cross-block branch of the local
SelectionTarget β TextTargetconverter is defensive. It returns two collapsed segments at start and end points;scrollRangeIntoViewwalks segments in document order and scrolls to the first, so the effect is "scroll to the start endpoint." If a future metadata path produces a real cross-block anchor we revisit this β likely by returningnulland surfacing the failure to the caller. Comment increate-super-doc-ui.tsflags the revisit condition.Public facade
MetadataHandlere-exported throughsuperdoc/uivia the four facade files (ui.d.ts,public/ui.ts,verify-public-facade-emit.cjsexpectedNames,ui.barrel.test.tsregression). Facade count goes 70 β 71 (3 runtime + 68 types).Verified:
src/ui/metadata.test.tsβ 6/6 (3 getRect including the bridge-boundary test, 3 scrollIntoView)src/ui/*full suite β 312/312src/ui.barrel.test.tsβ 7/7verify-public-facade-emit.cjsβ ui: 71 exportspnpm --filter superdoc run build:esβ clean