Skip to content

Merge main into stable (conflicts need resolution)#2936

Open
superdoc-bot[bot] wants to merge 40 commits intostablefrom
merge/main-into-stable-2026-04-24
Open

Merge main into stable (conflicts need resolution)#2936
superdoc-bot[bot] wants to merge 40 commits intostablefrom
merge/main-into-stable-2026-04-24

Conversation

@superdoc-bot
Copy link
Copy Markdown
Contributor

@superdoc-bot superdoc-bot Bot commented Apr 24, 2026

Summary

  • creates merge/main-into-stable-2026-04-24 from stable
  • merges main into the candidate branch
  • commits the merge with conflict markers so a human can resolve the branch

Next Step

Resolve the conflicts on merge/main-into-stable-2026-04-24, push the fixes, and then merge this PR into stable.


Auto-created by promote-stable workflow.

chittolinag and others added 30 commits April 21, 2026 18:10
* fix: avoid scrolling back when dragging selection

* chore: removed references to tickets

* chore: removed references to ticket
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…es (SD-2417) (#2705)

* feat: add translator for w:pPrChange

* fix(pPrChange): preserve empty w:pPr on round-trip

An empty <w:pPr/> inside <w:pPrChange> means "the paragraph previously
had no properties" per the OOXML spec, but the encoder was dropping it:
pPrTranslator.encode() returns undefined for an empty pPr, so the
paragraphProperties key never landed in the encoded change. On decode,
the existing fallback couldn't fire because the key was absent, and the
element vanished on export.

Fall back to an empty object when the inner encode returns undefined,
so the key is always present when a <w:pPr> was in the source XML. The
decode path already reconstructs the empty <w:pPr/> from this shape.

* chore(super-converter): drop unused emitWhenAttributesOnly option

* fix(super-converter): order sectPr before pPrChange in w:pPr

CT_PPr requires sectPr to precede pPrChange, but the exporter always
appended sectPr as the last child. Before the w:pPrChange translator
existed, no export contained both children, so the ordering bug was
latent. With pPrChange now round-tripping, paragraphs that carry both a
section break and a tracked property change were emitted in an order
Word rejects.

Insert sectPr before any existing w:pPrChange child; otherwise append
as before.

* test(pPrChange): cover mixed sectPr and pPr routing (SD-2417)

- pPrChange-translator: add a round-trip that mixes sectPr with other
  paragraph properties so a regression dropping either half surfaces.
- pPr-translator: add two tests that exercise the pPrChange registration
  end-to-end through the real pPr translator, guarding against a silent
  SD-2417 regression if the registration is ever removed.

---------

Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
Co-authored-by: Caio Pizzol <caio@superdoc.dev>
* ci(behavior): parallelize tests by browser x shard matrix

Expands the behavior test matrix from 3 shards (each running all browsers)
to 12 jobs (3 browsers x 4 shards). Each job installs only the browser it
needs and uses a browser-scoped Playwright cache key. Local measurement
showed a 1/9 slice at ~4 min on 2 workers; 1/12 should land ~3 min of
tests + 2.5 min setup, well under the previous ~20 min wall time.

* ci(behavior): self-trigger on workflow file changes

---------

Co-authored-by: Caio Pizzol <caio@superdoc.dev>
* fix: improve toolbar responsiveness

* test(toolbar): fill round-3 coverage gaps (SD-2328)

Adds boundary and lifecycle coverage for the responsive toolbar fix:

- defaultItems.test.js: XL overflow cutoff (1429 vs 1430) and LG
  compact-class application (1280 vs 1281).
- Toolbar.test.js: positive ResizeObserver branch (constructor, observe,
  disconnect on unmount). Moves vi.unstubAllGlobals into afterEach so
  the stub doesn't leak on a thrown assertion.
- super-toolbar.test.js: getAvailableWidth branches on
  responsiveToContainer and falls back to 0 when no container is set.
- responsive-container-overflow.spec.ts: fixes a latent selector bug.
  The original descendant query never matched (class lives on the
  ButtonGroup root, not a descendant) so the minWidth assertion
  silently passed on null. Now uses a compound selector and asserts
  both side groups compact.

---------

Co-authored-by: Caio Pizzol <caio@superdoc.dev>
Co-authored-by: Artem Nistuley <artem@superdoc.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* ci(release): harden stable release orchestration and recovery

* fix: lint

* chore: fix dry run
…2905)

Co-authored-by: Artem Nistuley <artem@superdoc.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…#2899)

* feat(super-editor): render imported heading/bookmark cross-references (SD-2495)

Wire the sd:crossReference v3 translator into the v2 importer entity list so
REF / NOTEREF / STYLEREF fields imported from DOCX actually produce PM
crossReference nodes instead of being silently dropped by the dispatch loop.
Walk nested run wrappers when extracting the field's cached display text, and
render the cross-reference as an internal hyperlink when the instruction
carries the \\h switch. Route clicks on internal #bookmark anchors through
goToAnchor so rendered cross-references navigate to their target in the
document.

Fixes IT-949 — Word cross-references (e.g. "Section 15") now appear in the
viewer and are searchable, matching Word's output.

* fix(presentation-editor): anchor nav writes scrollTop to the real scroll container

#scrollPageIntoView wrote scrollTop to #visibleHost, which is typically
overflow: visible and therefore not the actual scroll target. Anchor
navigation (TOC clicks and SD-2495 cross-reference click-to-navigate)
silently did nothing whenever the bookmark target was outside the
current viewport — the PM selection moved but the viewport never
scrolled.

Write to #scrollContainer (the resolved scrollable ancestor) as the
primary target, plus #visibleHost for backward compatibility with
legacy layouts and the existing test harness that mocks scrollTop on
the host element.

This unblocks SD-2495's cross-reference click-to-navigate on docs
where cross-references and their targets live on different pages.

* feat(layout): show-bookmarks bracket indicators (SD-2454)

Opt-in visual indicators for bookmark positions — mirrors Word's "Show
bookmarks" (File > Options > Advanced). Off by default.

- Pm-adapter bookmark-start and new bookmark-end converters emit gray `[`
  and `]` marker TextRuns when `layoutEngineOptions.showBookmarks` is
  true. Markers flow through pagination and line breaking as real
  characters, matching Word's own visual model.
- Auto-generated bookmarks (`_Toc…`, `_Ref…`, `_GoBack`) are hidden even
  when the feature is on — matching Word. A `renderedBookmarkIds` set on
  the converter context pairs suppression so closing brackets don't
  orphan open ones.
- PresentationEditor.setShowBookmarks toggles at runtime: clears the
  flow-block cache and schedules a re-render.
- SuperDoc.setShowBookmarks is the public API passthrough.
- Dev app gets a Show/Hide bookmarks toggle button in the header.
- CSS: subtle gray, non-selectable so users don't include brackets in
  copied text. Bookmark name surfaces via the native title tooltip on
  the opening bracket.

* test: close regression gaps for SD-2495 / SD-2454 / anchor-nav fix

Fills the test gaps surfaced by the testing-excellence review of this PR:

- crossReferenceImporter.integration.test.js (new, 4 tests): exercises the
  full v2 body pipeline (preprocessor -> dispatcher -> entity handler ->
  v3 translator). Asserts crossReferenceEntity is a member of the
  defaultNodeListHandler entities list, so the exact root cause that
  produced IT-949 ("Section 15" vanishing) fails loudly if a future
  refactor drops the wire-up. Unit tests of the translator alone cannot
  catch this — they bypass the dispatcher.

- EditorInputManager.anchorClick.test.ts (new, 4 tests): pins the
  SD-2537 click-to-navigate routing. Clicking #bookmark hrefs routes
  through goToAnchor (was TOC-only before). External and empty-fragment
  hrefs are explicitly NOT routed.

- cross-reference.test.ts: added marks-propagation test (node.marks flow
  into the emitted TextRun so italic/textStyle on xref text survives —
  SD-2537 "preserve surrounding run styling" AC).

- bookmark-markers.test.ts: converted the `for` loop over auto-generated
  bookmark names into `it.each`. Each input now reports per-case on
  failure, complies with testing-excellence's "no control flow inside
  test bodies" guideline.

- PresentationEditor.test.ts: documents why the scrollContainer-vs-
  visibleHost branch of the SD-2495 scrollPageIntoView fix isn't unit-
  testable here (happy-dom doesn't propagate inline overflow through
  getComputedStyle, which is what findScrollableAncestor uses).

* feat(pm-adapter): synthesize internal link for PAGEREF with \h switch

Mirrors the pattern added for crossReference in #2882. When a PAGEREF
field instruction carries the `\h` switch, attach a FlowRunLink via
`buildFlowRunLink({ anchor: bookmarkId })` so clicks on the rendered
page number navigate to the referenced bookmark through the existing
anchor-link routing (`EditorInputManager.#handleLinkClick` → `goToAnchor`).

Previously the PAGEREF inline converter emitted the `pageReference`
token run with `pageRefMetadata.bookmarkId` but no `link`, so the DOM
layer never produced a clickable element for PAGEREFs. TOC entries and
other hyperlinked page references imported from Word therefore failed
to navigate on click, even though Word honored the `\h` switch.

Depends on #2882 for `buildFlowRunLink` export and the generalized
anchor-click routing.

* fix(pm-adapter): match PAGEREF \h switch case-insensitively

Word field switches are case-insensitive per the field-code grammar, so
`\H` should produce the hyperlink the same as `\h`. Reviewer
(codex-connector) flagged that the original `/\\h\b/` regex skipped the
link synthesis for instructions like `PAGEREF _Toc123 \H`, leaving them
non-navigable even though the author had requested hyperlink behavior.

Adds the `i` flag and a regression test with an uppercase switch.

* test: add coverage for standalone PAGEREF \h + REF-family preprocessors

Behavior test (tests/behavior/tests/navigation/pageref-standalone-click.spec.ts):
Covers the PR's load-bearing case - a PAGEREF \h field NOT wrapped in a
<w:hyperlink>. The existing toc-anchor-scroll.spec.ts only exercises the
wrapped-in-hyperlink shape, where the outer link mark already propagates
via marksAsAttrs and the PR is a no-op. Fixtures exercise both \h and \H
(case-insensitivity per ECMA-376 17.16.1).

Preprocessor unit tests (ref/noteref/styleref):
These three importer modules were added in #2882 without tests. Each
verifies the preprocessor produces a sd:crossReference node with the
right fieldType and preserves the instruction text verbatim.

* test(superdoc): cover setShowBookmarks propagation and no-op guard

Closes the Codecov patch-coverage gap on SuperDoc.js flagged on PR #2899.
The uncovered setShowBookmarks method came in via the SD-2454 merge and
had no test for its config mutation, no-op short-circuit, or Boolean()
coercion. Models the existing setDisableContextMenu test pattern.

* refactor(pm-adapter): drop TextRun casts + case-insensitive \h in cross-reference

Addresses review feedback on #2899:

- page-reference.ts: remove redundant `as TextRun` casts — textNodeToRun
  already returns TextRun, so the casts are noise (cross-reference.ts:34
  already does this cleanly). Shorten the \h comment.
- cross-reference.ts: add the `i` flag to the \h switch regex to match
  ECMA-376 §17.16.1, same fix as 7f19358 for page-reference. Add a
  regression test covering `\H`.

* test: add unit coverage for scroll fan-out when ancestor != host

Stubs window.getComputedStyle to mark a wrapper element as scrollable so
#findScrollableAncestor returns the wrapper, then asserts both the wrapper
and the visibleHost receive scrollTop. This pins the SD-2495 fix: a revert
to the pre-fix one-liner (writing only to the visibleHost) now fails.

---------

Co-authored-by: Tadeu Tupinamba <tadeu.tupiz@gmail.com>
Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
Co-authored-by: Caio Pizzol <caio@superdoc.dev>
* fix(paragraph): guard listRendering destructure against null

ParagraphNodeView destructured `{ suffix, justification }` from
`this.node.attrs.listRendering` without a null-check in #updateListStyles,
and accessed `listRendering.markerText`/`.suffix` directly in #initList.

When a paragraph node carried `listRendering: null` — which can happen
after certain editor mutations (e.g. dispatching a transaction that
combines `setDocAttribute('bodySectPr', …)` with a paragraph delete) —
the post-transaction list-styles re-pass threw:

    TypeError: Cannot destructure property 'suffix' of
    'this.node.attrs.listRendering' as it is null

Use `?? {}` and optional chaining so a null value falls back through
the existing defaults (`suffix ?? 'tab'` and the `suffix == null` branch
in #createSeparator).

Adds a regression test.

* fix(paragraph): no-op list style updates when listRendering is null

Previously the null-guarded path fell back to `suffix = 'tab'` and still
invoked `#calculateMarkerStyle`/`#calculateTabSeparatorStyle`. Reviewer
(codex-connector) flagged that in mixed-suffix updates — where a queued
RAF callback runs after a node transitions from `suffix: 'space'` to
`listRendering: null` — the separator may still be a Text node. Writing
`this.separator.style.cssText` on a Text node throws.

Change #updateListStyles and #initList to return early when
`listRendering` is null, leaving the existing marker/separator untouched.
Future updates (when the node gets a real listRendering or isList()
returns false) will clean up as before.

Adds a regression test covering the space→null transition.

* test(paragraph): cover constructor mount and null-to-tab recovery

Addresses review feedback on #2896:

- Update #initList JSDoc `@param` to include `| null`, matching the
  no-op-on-null behavior added in the previous commit.
- Add a test that mounts a ParagraphNodeView with `listRendering: null`
  (the constructor path, not just update()), confirming the null guards
  in #initList and #updateListStyles cover first-render too.
- Add a test for the space→null→tab transition that verifies the
  separator swaps from a text node back to a span when listRendering
  returns with a different suffix.

---------

Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…tems (#2811)

* refactor(layout): lift page metadata into ResolvedPage

* refactor(layout): lift fragment metadata into resolved paint items

Add pmStart, pmEnd, continuesFromPrev, continuesOnNext, markerWidth,
and metadata fields to resolved paint item types. Populate them in
the resolvers and update the painter to prefer resolved item data
over legacy Fragment reads with fallbacks.

* fix(layout): use resolved continuation state for paragraph first-line width
* chore: add initial scaffold for labs release check orchestration

* chore: tweak gha
…ayout (#2812)

* refactor(layout): lift page metadata into ResolvedPage

* refactor(layout): lift fragment metadata into resolved paint items

Add pmStart, pmEnd, continuesFromPrev, continuesOnNext, markerWidth,
and metadata fields to resolved paint item types. Populate them in
the resolvers and update the painter to prefer resolved item data
over legacy Fragment reads with fallbacks.

* refactor(layout): pre-compute SDT container keys in resolved layout

* refactor(layout): pre-compute paragraph border data in resolved layout (#2813)
…tage (#2814)

* refactor(layout): lift page metadata into ResolvedPage

* refactor(layout): lift fragment metadata into resolved paint items

Add pmStart, pmEnd, continuesFromPrev, continuesOnNext, markerWidth,
and metadata fields to resolved paint item types. Populate them in
the resolvers and update the painter to prefer resolved item data
over legacy Fragment reads with fallbacks.

* refactor(layout): pre-compute SDT container keys in resolved layout

* refactor(layout): pre-compute paragraph border data in resolved layout

* refactor(layout): move change detection into resolved layout stage

* fix: avoid duplicate block version hashing in DomPainter
…esolved items (#2818)

* refactor(layout): lift page metadata into ResolvedPage

* refactor(layout): lift fragment metadata into resolved paint items

Add pmStart, pmEnd, continuesFromPrev, continuesOnNext, markerWidth,
and metadata fields to resolved paint item types. Populate them in
the resolvers and update the painter to prefer resolved item data
over legacy Fragment reads with fallbacks.

* refactor(layout): pre-compute SDT container keys in resolved layout

* refactor(layout): pre-compute paragraph border data in resolved layout

* refactor(layout): move change detection into resolved layout stage

* refactor(layout): lift paragraph and list-item block/measure into resolved items
)

* refactor(layout): lift page metadata into ResolvedPage

* refactor(layout): lift fragment metadata into resolved paint items

Add pmStart, pmEnd, continuesFromPrev, continuesOnNext, markerWidth,
and metadata fields to resolved paint item types. Populate them in
the resolvers and update the painter to prefer resolved item data
over legacy Fragment reads with fallbacks.

* refactor(layout): pre-compute SDT container keys in resolved layout

* refactor(layout): pre-compute paragraph border data in resolved layout

* refactor(layout): move change detection into resolved layout stage

* refactor(layout): lift paragraph and list-item block/measure into resolved items

* refactor(painter): extract block/measure resolution helper
…Input (#2820)

* refactor(layout): lift page metadata into ResolvedPage

* refactor(layout): lift fragment metadata into resolved paint items

Add pmStart, pmEnd, continuesFromPrev, continuesOnNext, markerWidth,
and metadata fields to resolved paint item types. Populate them in
the resolvers and update the painter to prefer resolved item data
over legacy Fragment reads with fallbacks.

* refactor(layout): pre-compute SDT container keys in resolved layout

* refactor(layout): pre-compute paragraph border data in resolved layout

* refactor(layout): move change detection into resolved layout stage

* refactor(layout): lift paragraph and list-item block/measure into resolved items

* refactor(painter): extract block/measure resolution helper

* refactor(painter): remove body blocks/measures from DomPainterInput

Body block and measure data now flows exclusively through the resolved
layout. The painter only builds a blockLookup from header/footer data,
which is the last remaining fallback surface for fragments that do not
yet have a resolved path. Complex-transaction rebuild detection now
walks the resolved layout items directly instead of iterating the body
blockLookup.

The legacy createDomPainter wrapper derives a resolved layout from
its legacyState blocks/measures on the fly so the benchmark path and
direct createDomPainter(options).paint(Layout) callers keep working
without setResolvedLayout.

* fix: dompainter body input contract on first paint
…oration layouts (#2826)

* refactor(layout): lift page metadata into ResolvedPage

* refactor(layout): lift fragment metadata into resolved paint items

Add pmStart, pmEnd, continuesFromPrev, continuesOnNext, markerWidth,
and metadata fields to resolved paint item types. Populate them in
the resolvers and update the painter to prefer resolved item data
over legacy Fragment reads with fallbacks.

* refactor(layout): pre-compute SDT container keys in resolved layout

* refactor(layout): pre-compute paragraph border data in resolved layout

* refactor(layout): move change detection into resolved layout stage

* refactor(layout): lift paragraph and list-item block/measure into resolved items

* refactor(painter): extract block/measure resolution helper

* refactor(painter): remove body blocks/measures from DomPainterInput

Body block and measure data now flows exclusively through the resolved
layout. The painter only builds a blockLookup from header/footer data,
which is the last remaining fallback surface for fragments that do not
yet have a resolved path. Complex-transaction rebuild detection now
walks the resolved layout items directly instead of iterating the body
blockLookup.

The legacy createDomPainter wrapper derives a resolved layout from
its legacyState blocks/measures on the fly so the benchmark path and
direct createDomPainter(options).paint(Layout) callers keep working
without setResolvedLayout.

* refactor(layout): add resolveHeaderFooterLayout helper for decoration layouts

* chore: fix lock file

* fix: preserve per-page header/footer resolution data
* docs: enhance OpenAPI specification for sign API

- Updated the `document` object description to clarify the requirement of providing either `base64` or `url`.
- Expanded the `signer` object description to specify required fields and optional fields, including examples.
- Improved the `auditTrail` description to emphasize compliance requirements.
- Added a new `certificate` object to configure the audit trail certificate page.
- Updated the API documentation in `backend.mdx` to reflect the changes in the `signer` fields and their requirements.

* docs: update OpenAPI specification for event payload structure

- Replaced the `field` and `value` properties with a new `data` object in the event schema.
- The `data` object now supports an event-specific payload with detailed descriptions for different event types, enhancing clarity and usability.

* docs: update documentation for event payload and signer fields

- Enhanced the OpenAPI specification to clarify the structure of the event payload, including updates to the `field_change` type to support additional data types.
- Simplified the `signer` object in the backend documentation by removing unnecessary properties and added a warning about trusting browser-submitted `ip` and `userAgent` values.

---------

Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
harbournick and others added 10 commits April 23, 2026 08:33
* feat(layout): make tracked changes story-aware in resolved layout

* feat(editor): add story editing sessions and runtime plumbing

* feat(doc-api): expose story-aware tracked change operations

* feat(superdoc): surface story tracked changes in comments ui

* fix(notes): only strip separators after note reference runs

* fix(superdoc): restore tracked-change comment rendering and CI tests

* fix: sidebar activation state for tcs

* fix: footnote caret selection
* fix: image rendering

* chore: update test case
…der callback (#2827)

* refactor(painter): deliver resolved items to decoration provider callback

* fix: normalize header/footer resolved item coordinates before paint
…2925)

* fix(extract): return tables as paragraph-granular blocks (SD-2672)

doc.extract() was flattening tables into one joined string, which broke
RAG chunking and made table citations unreachable via scrollToElement.
Walk tables directly and emit one block per paragraph-like descendant
of each origin cell, tagged with tableContext so consumers can group
back to cell, row, or whole table.

- gridBefore/gridAfter placeholder cells are skipped via the
  __placeholder attr; they are layout artifacts with no user content.
- Block SDTs (structuredContentBlock) are transparent, so tables
  wrapped in content controls are not re-flattened through the
  wrapper's textContent.
- Cell paths use physical row-and-cell child indexes so deterministic
  fallback nodeIds agree with buildBlockIndex, keeping the
  scrollToElement round-trip stable for paragraphs that lack paraId
  and sdBlockId inside horizontally merged tables.

Tested: 13 behavior tests (7 existing SD-2525 + 6 new SD-2672),
5 new adapter unit tests, plus the full document-api-adapters suite
(3105 tests) and document-api bun suite (1362 tests).

* fix(extract): recurse through unrecognized block wrappers (SD-2672)

The new table walker only emitted blocks for recognized types and
silently dropped anything else, including their block children. That
regressed coverage versus the old textContent walk for `documentSection`,
`documentPartObject`, and `shapeContainer`, which all declare
block-level content but aren't in EMITTABLE_BLOCK_TYPES. Treat any
unrecognized block with block-level children as transparent and recurse
into it, so paragraphs nested inside these wrappers still surface with
their enclosing tableContext. Adds a unit test covering a
`documentSection` inside a table cell.

* test(extract): add DOCX-import-driven coverage for table edge cases (SD-2672)

The adapter unit tests hit the algorithm via schema-constructed PM docs,
which skips the importer entirely. This adds a second layer of tests
that load real Word-authored .docx files, run them through the full
import pipeline, and assert extract output. Closes the gap the code
review flagged for a customer-facing legal RAG contract.

Fixtures authored via Word COM + local OOXML patching:
- sd-2672-plain-3x3.docx: baseline table, no merges or placeholders
- sd-2672-merged-table.docx: colspan=2 and rowspan=2 anchors
- sd-2672-rtl-table.docx: bidiVisual RTL table
- sd-2672-gridbefore-vmerge.docx: w:gridBefore + w:vMerge=restart/continue
- sd-2672-sdt-table.docx: table wrapped in a w:sdt block (content control)
- sd-2672-nested-table.docx: 2x2 table inside cell (1,1) of outer table
- sd-2672-multipara-cell.docx: cell (0,0) with two paragraphs

The build-sd-2672-fixtures.mjs script regenerates the patched variants
from the Word-authored base, using JSZip + regex/XmlDocument surgery.

Tests assert: per-cell content lands at correct logical grid coords,
merged anchors carry rowspan/colspan, RTL tables still report columns
0..N-1, gridBefore placeholders don't emit phantom blocks, SDT wrappers
are transparent, nested tables get a fresh tableOrdinal with parent
coordinates, multi-paragraph cells emit one block per paragraph with
shared tableContext, and scrollToElement round-trips a merged-cell
paragraph nodeId.

* chore(tests): drop SD-2672 fixture build script

The script was added alongside the fixtures to regenerate the OOXML-patched
variants from a Word-authored base. It isn't carrying its weight: fixtures
are committed as static binaries, the regex-based XML patching is fragile
to Word COM output changes, and the commit history already documents how
each fixture was constructed. If we need a new edge-case fixture later,
hand-authoring it once is simpler than maintaining a generator.

* chore(tests): drop stale script reference in extract-docx error
* fix: footnote editing lag

* fix: flush active note sessions before DOCX export

* fix: context menu accept/reject in headers/footers

* fix: enable hover effect on header/footer regardless of active editing part

* feat: create duplicate-comment display for header/footer tcs
…s (SD-2557) (#2872)

* fix(pm-adapter): emit pending section break before TOC/docPartObj SDTs (SD-2557)

When a Word document stores a `w:sectPr` on a paragraph immediately before a
TOC (or other `docPartObj`) SDT, the section break was dropped. The TOC
ended up rendering on the same page as the prior section's content instead
of starting a new page as Word does.

Root cause in pm-adapter:

  - `findParagraphsWithSectPr` (sections/analysis.ts) recurses into `index`,
    `bibliography`, and `tableOfAuthorities` to count their children as
    paragraphs, but NOT into `documentPartObject` / `tableOfContents`.
  - As a result, section ranges treat a TOC SDT as a single opaque unit —
    its children don't occupy paragraph indices.
  - When processing flow, `handleParagraphNode` / `handleIndexNode` /
    `handleBibliographyNode` / `handleTableOfAuthoritiesNode` each emit a
    pending section break before the paragraph whose index matches
    `nextSection.startParagraphIndex`.
  - `handleDocumentPartObjectNode` and `handleTableOfContentsNode` did NOT
    run this check, so the deferred break only fired on the next body
    paragraph AFTER the SDT. The SDT's content rendered in the PREVIOUS
    section, with no page break before it.

Fix:

  - Add `emitPendingSectionBreakForParagraph(sectionState, nextBlockId,
    blocks, recordBlockKind)` helper in sections/breaks.ts that centralizes
    the "check, emit, advance" pattern.
  - Call the helper at the top of `handleDocumentPartObjectNode` and
    `handleTableOfContentsNode` — once per SDT. Since the SDT's children
    don't affect `currentParagraphIndex` (`findParagraphsWithSectPr` skips
    them), the check fires correctly at the SDT boundary: if the SDT sits
    at a section boundary, the nextPage break is emitted so the SDT renders
    on a new page.
  - No changes to section-range computation — counting stays consistent.

Verified against both fixtures from the issue (Highstreet Proposal Sample,
Heffernan Proposal Sample): cover stays on its own page, TOC starts on a
new page, matching Word.

Tests:

  - 3 new unit tests in document-part-object.test.ts covering:
    - Section break emitted at SDT boundary
    - No emission when SDT is not at a section boundary
    - No-op when sectionState is undefined
  - 1740 pm-adapter tests pass (up from 1737), 604 layout-engine tests pass.

* fix(pm-adapter): recurse into docPartObj for section ranges + per-child emission (SD-2557)

The initial fix only handled the sectPr BEFORE a TOC docPartObj (on the
empty paragraph that precedes the SDT). It missed the SECOND sectPr that
Word commonly stores on the trailing empty paragraph INSIDE the TOC SDT —
which signals "TOC section ends here, next section starts on new page".

Because `findParagraphsWithSectPr` did not recurse into `documentPartObject`
or `tableOfContents`, that inner sectPr was never discovered, so no
section-range boundary was built between the TOC and the following body
content. SuperDoc rendered the TOC and the first body section stacked on
the same page (just one page later than before the first fix).

Complete fix:

1. `findParagraphsWithSectPr` (sections/analysis.ts) now recurses into
   `documentPartObject` and `tableOfContents` in addition to `index` /
   `bibliography` / `tableOfAuthorities`. This lets section-range analysis
   see sectPrs stored anywhere inside a TOC SDT.

2. `handleDocumentPartObjectNode` (non-TOC branch) emits the pending
   section break before each child paragraph and advances
   `currentParagraphIndex` — matching the pattern in `handleIndexNode`,
   `handleBibliographyNode`, and `handleTableOfAuthoritiesNode`.

3. `processTocChildren` (toc.ts) accepts `sectionState` via its context
   arg and performs the same per-child emit + increment dance as the
   paragraph handlers. This handles the TOC branch of
   `handleDocumentPartObjectNode` and the nested `tableOfContents`
   recursion path.

With all three changes, the Highstreet fixture now renders exactly like
Word:

  - Page 1: cover
  - Page 2: TOC alone
  - Page 3: ABOUT US body
  - Page 4: ON BEHALF OF HIGHSTREET
  - Page 5: WORKERS COMPENSATION

Tests:
  - 4 new tests in document-part-object.test.ts (non-TOC emission,
    non-boundary no-op, undefined state, sectionState passthrough to
    processTocChildren)
  - 1741/1741 pm-adapter, 604/604 layout-engine, 11377/11377 super-editor

* refactor(pm-adapter): unify TOC handling via processTocChildren (SD-2557)

Collapse handleTableOfContentsNode into a delegation to processTocChildren
so direct `tableOfContents` nodes share the same code path as the
`documentPartObject` TOC gallery. Keeps the section-range counting contract
from findParagraphsWithSectPr consistent across both paths: every TOC child
paragraph advances sectionState.currentParagraphIndex, so deferred section
breaks fire at the correct boundary.

- Gate applyTocMetadata sdt fabrication on gallery — a direct
  tableOfContents PM node has no enclosing w:sdt, so we no longer invent
  a docPartObject SDT metadata entry on its entries.
- Forward themeColors through processTocChildren to the paragraph
  converter; also pass it in from handleDocumentPartObjectNode.
- Add regression tests for per-child index advancement, sdt gating, and
  themeColors pass-through.

Addresses PR #2872 review feedback.

---------

Co-authored-by: Nick Bernal <117235294+harbournick@users.noreply.github.com>
* fix: footer tc issus, selection issues

* chore: behavior tests

* chore: additional fixes
* fix: header/footer undo history across story sessions
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59921c3380

ℹ️ 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".

Comment on lines +2985 to +2987
if (resolvedItem?.block?.kind !== 'paragraph' || resolvedItem?.measure?.kind !== 'paragraph') {
throw new Error(`DomPainter: missing resolved paragraph block/measure for fragment ${fragment.blockId}`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow decoration fragments to render without resolved items

PageDecorationPayload.items is documented as optional (renderer.ts says omitted items should still render with reduced metadata), but paragraph rendering now hard-fails when resolvedItem is missing. In that case the code throws and falls back to an error placeholder, so header/footer providers that still return only { fragments, height } (a supported shape) will render broken content instead of text. This is a functional regression for legacy/third-party decoration providers and any path that drops misaligned items.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants