Merge main into stable#3228
Conversation
* ci: auto-resolve version conflicts in promote-stable workflow * ci: also match root-level release artifact files in auto-resolve
…#3200) * fix(mcp): fall back to z.unknown() for oneOf with non-object variants z.looseObject({}) emits type:"object" which is right for object-only unions but rejects arrays/booleans/etc. at runtime when the union includes non-object variants. Gate the looseObject path on "every variant is type:object" and fall back to z.unknown() otherwise. The only catalog field this affects today is superdoc_edit.content (oneOf object|array), where the array form was getting rejected before reaching DocumentApi. Adds a unit test that walks the catalog and checks the emitted type for both branches. * docs(mcp): tighten oneOf branch comment with AIDEV-NOTE anchor
Strengthens the existing reminder so agents look for comment-policy.md explicitly and run /comment-audit to validate changes, instead of relying on the bare 'follow comment-policy.md' wording.
* feat(contracts): add typed direction context types (SD-2775)
Introduces orthogonal direction context types so future RTL work cannot
accidentally collapse axes that ECMA-376 keeps separate:
- BaseDirection, WritingMode (enums)
- SectionDirectionContext (page direction, gutter — chrome only)
- TableDirectionContext (visual cell ordering only)
- CellDirectionContext (cell writing mode)
- ParagraphDirectionContext (paragraph inline base direction + writing mode)
- RunBidiContext, RunScriptContext (run-level signals; consumed in 1b/1c)
Adds `directionContext` field to ParagraphAttrs alongside the existing
`direction` scalar. Both are populated by pm-adapter from the same source;
consumers can migrate gradually.
Per ECMA-376 §17.6.1 / §17.3.1.6 / §17.4.1 / §17.3.1.41, each axis stays
separate: section bidi is chrome only, paragraph bidi is paragraph-local,
table visual direction is cell ordering, writing mode is the one
inheriting axis.
No behavior change. Resolver chain and migration follow in subsequent
commits.
* feat(pm-adapter): add direction resolver module (SD-2776)
New module pm-adapter/src/direction/ with:
- resolveSectionDirection / resolveTableDirection / resolveCellDirection /
resolveParagraphDirection — context propagation chain mirroring OOXML
containment hierarchy
- logicalSides helpers (resolveLogicalAlignment, resolveLogicalIndent,
physicalSide, isRtl, toBaseDirection) — direction-aware logical→physical
mapping
- 12 non-collapse tests enforcing the four ECMA spec rules:
1. Section w:bidi MUST NOT make paragraphs RTL (§17.6.1)
2. Table w:bidiVisual MUST NOT make cell paragraphs RTL (§17.4.1)
3. Run-level w:rtl MUST NOT bubble up to paragraph
4. Paragraph w:bidi DOES produce paragraph RTL (§17.3.1.6, including
style cascade through docDefaults per §17.7.2)
- Writing mode IS the one inheriting axis (§17.3.1.41) — paragraph→cell→
section→default
Co-located README documenting the spec rules, a worked-example for
downstream consumers, and explicit non-goals (script classifier and bidi
controls deferred to Wave 1b / 1c).
No production call sites consume the resolver yet; migration follows.
* refactor(pm-adapter): migrate computeParagraphAttrs to direction resolver
Replaces the cascade in resolveEffectiveParagraphDirection +
inferDirectionFromRuns with the typed resolver chain from
pm-adapter/src/direction/. The cascade had three issues identified by
the audit at .tmp/rtl-audit-findings.md:
1. Section→paragraph fallback (§17.6.1 violation) — section bidi
propagated to paragraph inline direction. Latin paragraphs in RTL
sections rendered right-aligned; Word renders them left-aligned.
2. Majority-of-runs heuristic (UAX #9 P2/P3 disagreement) — base
direction came from counting runs whose w:rtl flag was set, not
the first strong character of the text content.
3. docDefaultsDirection parameter (redundant) — the style-engine
cascade in style-engine/src/ooxml/index.ts:165 already resolves
docDefaults.paragraphProperties.rightToLeft into the paragraph's
resolved properties before this resolver runs.
Now: paragraph direction comes from paragraph w:bidi (or its style
cascade); when absent, inlineDirection is undefined and the browser
applies UBA via the missing dir attribute. Output corrected for
documents that today render incorrectly; unchanged for documents that
were already correct.
Tests updated:
- paragraph.test.ts: removed cascade/heuristic tests that codified
the spec violations
- paragraph.test.ts: section-fallback test flipped to assert no
inheritance
- index.test.ts: two integration tests flipped to expect undefined
paragraph direction when only section bidi is set
Validation:
- 1,765 pm-adapter unit tests pass
- 211 contracts unit tests pass
- 12,374 super-editor unit tests pass (incl. footer w:rtl roundtrip)
- 51 RTL Playwright behavior tests pass across Chromium/Firefox/WebKit
Closes SD-2776, SD-2778. The legacy attrs.direction scalar remains
populated for backwards compatibility; consumers should migrate to
attrs.directionContext over time.
* fix(direction): accept rightToLeft on TablePropertiesLike
The resolved TableProperties type from the style-engine uses
`rightToLeft` for the bidiVisual flag (matching the existing importer
convention). The resolver previously checked only `bidiVisual`, so
passing real resolved table properties would leave visualDirection
undefined for RTL tables.
Now accepts both `rightToLeft` (style-engine name) and `bidiVisual`
(OOXML name) for safety. Test added to cover the alias.
* fix(direction): map all ST_TextDirection values incl. V-suffix variants
Per ECMA-376 §17.18.93, ST_TextDirection has 12 enumeration values across
the strict and Word-transitional vocabularies. The V-suffix variants are
glyph rotation, which CSS expresses through text-orientation, so they share
the writing-mode of their non-V sibling.
Before this commit the three resolvers (paragraph/section/cell) handled 6
of the 12 values; lrTbV, tbRlV, tbV, lrV, rlV all fell through to undefined
and the resolver silently used the inherited/default writing-mode instead.
The repo's ST_TEXT_DIRECTION contract (registry.ts:18) publishes lrTbV and
tbRlV as accepted values, so this was a contract violation - documents that
imported one of these would lose their writing-mode override.
Adds an exhaustive test that exercises all 12 values on paragraph, section,
and cell.
* fix(layout-bridge): separate text direction from RTL hit testing
* fix(layout-bridge): harden isRtlBlock and anchor compat-fallback rule
Three review-driven nits on the SD-2780 hit-test fix:
1. The directionContext gate used `'inlineDirection' in directionContext`
which fires for keys with `undefined` values. The resolver can produce
`inlineDirection: undefined` when no paragraph w:bidi is set anywhere
in the cascade, and the function would then return false instead of
falling through to the legacy direction/dir field. Check the value, not
the key.
2. Anchor the legacy direction/dir fallback as compat-fallback per
comment-policy.md so future agents know what triggers it (no typed
directionContext) and when it can be retired (SD-2778 collapses the
duplicate field).
3. Document why `attrs.textDirection` is no longer in the chain. Per ECMA
§17.18.93, ST_TextDirection values are writing-mode (lrTb/tbRl/btLr/
lrTbV/tbRlV/tbLrV); none equal 'rtl'. The old check was always dead.
New test covers the precedence edge case.
---------
Co-authored-by: Caio Pizzol <caiopizzol@gmail.com>
…or (#3201) Extract resumePackagePublish switch into per-descriptor resumePublish functions and replace pkg.name === 'sdk' branches with capability checks (pkg.pythonPackages, pkg.preparePythonSnapshot). No behavior change: the recovery engine becomes generic so adding superdoc/react/vscode-ext in follow-up PRs only adds adapters, not new switch arms. The internal field state.sdkPythonPublished is renamed to state.pythonPublished and recovery's returned snapshot field sdkPythonSnapshot to pythonSnapshot. recordSdkPythonSnapshot keeps its name so it continues emitting the sdk_python_snapshot_* GITHUB_OUTPUT keys consumed by release-stable.yml. Existing helper tests updated to match the refactored structure (the intent - each package has its own explicit resume path - is preserved).
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
#3120) * chore: save * feat: update TOC entry in context menu * fix: update TOC creating extra spaces and changing fonts * test: created tests around TOC programmatic update * refactor: removed unused code * refactor: code style tweaks * refactor: removed duplicate functions * test(toc): add regression coverage for SD-2664 review findings - tabLeader: 'none' must round-trip via serialize/parse (currently lost because no \\p is emitted when separator is missing, and the parser has no way to disambiguate "default = dots" from "explicit none"). - toc.configure({ tabLeader: 'none' }) on a default-leader TOC must not silently no-op (areTocConfigsEqual reports identical serialized output). - toc.update mode: 'pageNumbers' must find tocPageNumber marks when the marked text is nested inside a run wrapper (the rebuild output shape). All three tests fail on the current branch and lock in the regressions flagged in code review. * fix: toc context menu update * refactor: simplified logic * refactor: removed unnecessary test suite * refactor: simplified tests * chore: small comment tweaks * test: added behavior test for multiple TOCs updates * fix: inline partial selection to produce inline text * fix: toc from empty to non-empty * fix: early return on TOC update --------- Co-authored-by: Gabriel Chittolina <gabrielchittolina1@gmail.com> Co-authored-by: Caio Pizzol <caio@superdoc.dev>
… paragraphs (SD-2973) (#3210) * fix(converter): preserve hyperlink mark on inserted text split across paragraphs (SD-2973) SD-2858's preserveRaw path keeps tracked-change-wrapped fields structurally intact when the field crosses paragraph or wrapper boundaries, avoiding the import crash. For destructive wrappers (w:del / w:moveFrom) this trade-off is invisible since the content disappears on accept, but for constructive wrappers (w:ins / w:moveTo) the user keeps the inserted text and Word shows it both as inserted AND as a clickable hyperlink — the previous behaviour rendered it with insertion styling alone. Add an `isConstructiveTrackChangeElement` predicate, propagate a `preserveRawConstructive` flag through the field collector, and run a post-pass that finds the visible runs (between separate and end fldChars) and wraps them in `w:hyperlink` in-place. The surrounding paragraph and tracked-change wrapper structure is left intact so the SD-2858 round-trip guarantee still holds. * refactor(converter): share hyperlink attribute resolution between field paths (SD-2973) Per Luccas's review on PR #3210: the URL/anchor parsing and rels-element construction in applyConstructiveFieldInterpretation duplicated the same logic in preProcessHyperlinkInstruction. Extract resolveHyperlinkAttributes(instruction, docx) as the single source of truth for parsing a HYPERLINK field instruction into the attribute set that belongs on a w:hyperlink element. Both preProcessHyperlinkInstruction (the standard field path) and applyConstructiveFieldInterpretation (the SD-2973 raw-preserved constructive-tracked-change path) call it. Net change: ~-5 lines, single source of truth, no behaviour change.
* fix: footer tcs in replacement generating one per character (#2965) * fix: footer tcs in replacement generating one per character * fix: sdt with "contentLocked" not removable * chore: removed console log * fix: allow single click to target whole field --------- Co-authored-by: Nick Bernal <117235294+harbournick@users.noreply.github.com> Co-authored-by: Gabriel Chittolina <gabrielchittolina1@gmail.com>
…#3212) Adds two color swatches to the toolbar that re-theme tracked changes and comment highlights by writing the public --sd-* CSS variables on :root, and centers the editor in its column with a max-width wrapper. - Native <input type="color"> hidden behind a tb-btn label keeps the no-UI-kit posture of the demo. - Icons inside each swatch use mix-blend-mode: difference so they stay legible across any picked color. - The editor sits inside an .editor-canvas wrapper (max-width 880px, margin auto) so the document is centered between the toolbar and the activity sidebar.
* feat(workflows): notify homepage repo on stable superdoc release Mirrors the promote-stable-docs.yml pattern: workflow_run on Release superdoc, gated on success + stable, with a tag-diff against the triggering head_sha to filter out semantic-release no-ops. Sends repository_dispatch to superdoc-dev/homepage so a receiver workflow there can open one bump PR per release. Kept out of release-superdoc.yml on purpose: that job sits in the release-stable concurrency group, and a homepage/token failure should not mark a successful npm publish as failed. * refactor(workflows): simplify notify workflow to a single step
* fix: clear transient hyperlink styleId on unlink * test: add unlink regression coverage for transient hyperlink style cleanup * fix(link): derive underline preservation at unlink time and add imported-link regression * fix(link): tag paste-added underline as autoAdded so unsetLink removes it When a user pastes a bare URL, handlePlainTextUrlPaste auto-converts it to a link and adds an underline mark. The PR's autoAdded mechanism in unsetLink only removes underline marks tagged autoAdded:true, so the paste-added underline (untagged) was left behind on Remove Link. Same one-line fix as setLink at link.js:247. Adds regression coverage to relationships.test.js for: paste-URL unlink, setLink with longer replacement text, mixed-underline selections, and re-setLink. * test(link): drop paraphrase comments to match local convention Existing tests in relationships.test.js use no inline comments; removing the four added in the previous commit so the new tests match local style and the comment policy. --------- Co-authored-by: Caio Pizzol <caio@harbourshare.com> Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
…ands (SD-3083) (#3217) * fix(super-editor): guard cached paragraph props lookup in indent commands (SD-3083) Increase/Decrease Indent crashed when fired before the rendering pass had populated the resolved-paragraph-properties cache (fresh load, freshly inserted paragraphs). Falls back to compute-on-miss so the style cascade is honored, instead of a bare guard which would stop the crash but apply the wrong delta on style-derived indents. Set/Unset commands keep the original code path - they don't read the current indent and don't need the resolve work. * test(super-editor): expand textIndent regression coverage (SD-3083) Unit tests: - decreaseTextIndent honors style-derived indent on cache miss (symmetric to the existing increase regression test) - Cache hit short-circuits the compute-on-miss fallback - inverse of the set/unset opt-out test, guards the production || short-circuit Behavior test (tests/behavior/tests/toolbar/paragraph-indent.spec.ts): - Increase Indent on a fresh paragraph adds indent without crashing - Decrease Indent removes the indent applied by Increase - Repeated Increase compounds the left indent * docs(super-editor): fix unsetTextIndentation @example typo Pre-existing typo - example used `unsetTextIndent()` but the function is `unsetTextIndentation`. Found during a comment audit on this branch.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(super-editor,painter): render images inside Word textboxes (SD-2804) ECMA-376 §20.4.2.38 (CT_TxbxContent) lets a textbox hold rich body-level content — paragraphs whose runs can carry inline w:drawing images. The text-only extractor used to silently skip those drawings, so the textbox rendered empty even though export round-tripped the image untouched. The fix surfaces the inline drawing as a textContent part with kind='image' so the existing shape painter can render it alongside text spans: - TextPart contract gains optional kind/src/width/height/alt fields. - extractTextFromTextBox.handleRun branches on w:drawing, reuses the v3 wp drawing handler (handleImageNode) to resolve rId, then upgrades the path-style src to a data URI from converter.media so the painter can drop it straight into <img>. - DomPainter's createFallbackTextElement renders image parts as inline <img> elements next to existing text spans. Linked: SD-2745 (header-anchored floating textboxes — positions the box where this content now renders). * fix(super-editor,pm-adapter,painter): address PR #3207 review (SD-2804) Per Luccas's review on PR #3207: - (C1) Skip hidden textbox images. handleImageNode flags wp:docPr hidden="1" via attrs.hidden, but the new image-part branch only checked attrs.src and emitted visible <img>s for them. Top-level hidden drawings are filtered later in the pipeline; image parts bypass that filtering. Gate the textParts.push on imagePm.attrs.hidden !== true so hidden textbox drawings stay hidden, matching the body-level behaviour. - (C2) Drop the duplicated resolveImagePartSrc helper in the importer (it rejected Uint8Array, breaking Y.js binary media). Store the raw path + extension + rId on the image part. pm-adapter's hydrateImageBlocks gains a vectorShape branch that hydrates textContent.parts alongside ImageRuns, so all media path candidates and the Uint8Array → TextDecoder decoding live in a single place. - (C3) Anchored drawings inside textboxes are out of scope — wrap / position / transform metadata isn't carried into the text-parts model. Restrict the textbox-image branch to wp:inline and document the limit in the code comment so a future fixture can extend it intentionally. - (C4) Align inserted images to the text baseline like body inline images do (vertical-align: bottom). ECMA-376 §20.4.2.8 specifies that an inline drawing behaves "like a character glyph of similar size", and the body inline image renderer defaults to vertical-align: bottom (renderer.ts ~L5770, L5847) — the textbox image part used vertical-align: middle, visibly misaligning text next to the image inside a textbox compared to outside it.
…stop) (#3204) Adds superdoc to the stable orchestrator so the v* tag drives docs-stable promotion in the same workflow that releases tools, removing the cross- workflow concurrency-eviction problem for stable superdoc releases. The orchestrator now groups packages by chain. Within a chain, fail-stop applies as before (CLI failure skips SDK/MCP). Across chains, failures are independent: a tools failure does not skip superdoc and vice versa. Workflow rewiring: - release-superdoc.yml stops auto-firing on stable; main pushes still publish prereleases, and workflow_dispatch is preserved for recovery. - promote-stable-docs.yml triggers off release-stable.yml. The conclusion gate now accepts both success and failure - a tools-chain failure that follows a successful superdoc release should still promote docs. The inner git-tag detection (compare tags merged at the run's head_sha vs origin/stable) remains the source of truth, so tools-only runs still leave docs-stable alone. - release-stable.yml header comments + step name updated to reflect the broader scope; the workflow's name field is unchanged so the existing workflow_run trigger and concurrency group continue to match. A rename is best as a follow-up cleanup PR. Stacked on #3201 (descriptor refactor).
Adds react to the core chain after superdoc. react consumes `superdoc` in dependencies, so releasing them in order through the orchestrator means consumers never see a react release that pins to an older superdoc than what just shipped. - Adds `resumeReactPublish` adapter (uses generic `npm-publish-package.cjs` helper, idempotent against npm via dist-tag updates). - Adds react descriptor to the `core` chain. A superdoc failure now skips react (fail-closed within chain); a tools failure still does not. - `release-react.yml` stops auto-firing on stable; main pushes still publish prereleases. Stacked on #3204 (superdoc orchestrator).
Completes the core chain: superdoc -> react -> vscode-ext. vscode-ext publishes to the VS Code Marketplace (not npm) and ships a .vsix asset on the GitHub release; the script's existing helpers already cover both, so this PR adds the descriptor, the resume adapter, and the workflow plumbing. - `resumeVscodeExtPublish` runs `pnpm run package` then `vsce publish --skip-duplicate`; idempotent against the marketplace. In a tagged snapshot it also runs `build:superdoc` first so esbuild can resolve the webview's `superdoc` and `superdoc/style.css` imports through packages/superdoc/dist (snapshot only ran `pnpm install`). - vscode-ext descriptor uses `vsCodeExtensionId` (no `npmPackages`), so `inspectPackageReleaseState` probes the marketplace, not npm. - release-stable.yml's orchestrator step gains `VSCE_PAT` env so vsce can authenticate. - release-vscode-ext.yml stops auto-firing on stable; main pushes still build .vsix attachments to the GitHub release. - Three remaining `pkg.name === 'vscode-ext'` branches in the script (`getExpectedReleaseAssets`, `isGitHubReleaseComplete`, `ensureGitHubRelease`) switched to `pkg.vsCodeExtensionId` capability checks for consistency with PR #3201's refactor pattern.
|
The MCP ecma-spec tools aren't granted in this session, so I'll verify against ECMA-376 from cross-referenced knowledge of the schema. Status: PASS The three OOXML-touching files are spec-compliant:
The inline comment cites "ECMA-376 §20.4.2.38" — that reads slightly off (WordprocessingML lives in clause 17 of Part 1/4; clause 20 is DrawingML), but it's a comment, not a spec violation, so not flagging.
No non-existent elements, no missing-required-attribute writes, no wrong defaults. Looks good. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f57130571a
ℹ️ 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! |
buildTocEntryParagraphs wraps the page-number text inside a run, so the tocPageNumber mark lives one level below the paragraph's direct children. sanitizeTocContentForSchema only filtered marks on direct children, so any schema that omits tocPageNumber (the case the sanitizer exists to support) would still receive the unknown nested mark and nodeFromJSON would fail when rebuilding the TOC. Extracts the per-node mark-stripping into a recursive helper that walks content arrays, so the sanitizer works regardless of how deep buildTocEntryParagraphs nests the run wrapper. Flagged by chatgpt-codex-connector in #3228.
buildTocEntryParagraphs wraps the page-number text inside a run, so the tocPageNumber mark lives one level below the paragraph's direct children. sanitizeTocContentForSchema only filtered marks on direct children, so any schema that omits tocPageNumber (the case the sanitizer exists to support) would still receive the unknown nested mark and nodeFromJSON would fail when rebuilding the TOC. Extracts the per-node mark-stripping into a recursive helper that walks content arrays, so the sanitizer works regardless of how deep buildTocEntryParagraphs nests the run wrapper. Verified empirically before fixing: - Reproduced the bug against a real prosemirror-model Schema lacking the tocPageNumber mark: unfixed sanitizer leaves the nested mark and nodeFromJSON throws 'There is no mark type tocPageNumber in this schema'; fixed sanitizer strips it and nodeFromJSON succeeds. - Codified as 4 unit tests in toc-wrappers.test.ts (sanitizer exported for testing). With the fix reverted, 2 tests fail with the exact surviving-mark assertion. With the fix applied, all 13 tests pass. Flagged by chatgpt-codex-connector in #3228.
Stable superdoc releases now ship via release-stable.yml (named '📦 Release stable tooling (CLI/SDK/MCP)'), so listening to '📦 Release superdoc' on workflow_run silently stopped firing for stable releases. Mirrors the trigger change already in place on promote-stable-docs.yml. - Trigger now matches the orchestrator workflow name. - Accept conclusion: failure too, so a tools-chain failure following a successful superdoc release still notifies (chain-independent orchestrator semantics). - Verify both 'superdoc' and '@harbour-enterprises/superdoc' on npm before dispatching; semantic-release's prepare phase pushes the v* tag before publish, so a publish failure that does not recover would otherwise notify on an unpublished version.
buildTocEntryParagraphs wraps the page-number text inside a run, so the tocPageNumber mark lives one level below the paragraph's direct children. sanitizeTocContentForSchema only filtered marks on direct children, so any schema that omits tocPageNumber (the case the sanitizer exists to support) would still receive the unknown nested mark and nodeFromJSON would fail when rebuilding the TOC. Extracts the per-node mark-stripping into a recursive helper that walks content arrays, so the sanitizer works regardless of how deep buildTocEntryParagraphs nests the run wrapper. Verified empirically before fixing: - Reproduced the bug against a real prosemirror-model Schema lacking the tocPageNumber mark: unfixed sanitizer leaves the nested mark and nodeFromJSON throws 'There is no mark type tocPageNumber in this schema'; fixed sanitizer strips it and nodeFromJSON succeeds. - Codified as 4 unit tests in toc-wrappers.test.ts (sanitizer exported for testing). With the fix reverted, 2 tests fail with the exact surviving-mark assertion. With the fix applied, all 13 tests pass. Flagged by chatgpt-codex-connector in #3228.
Summary
merge/main-into-stable-2026-05-11fromstablemaininto the candidate branch.github/workflows/release-stable.ymlby keeping the updated orchestrator docs-promotion comment frommainNote
The promote-stable workflow dispatch failed before opening a PR because the GitHub App token cannot push workflow-file changes without the Workflows permission. This branch was pushed manually with a token that has workflow scope.