Conversation
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Word does not render w:bar borders on display (only preserves them on save). SuperDoc currently matches that behavior by omitting 'bar' from the normalized sides list. Add a note at the source of truth so future contributors don't re-add it without a real use case.
* feat: support column line separators
- Extracts 'w:sep' tag according to OOXML spec
- Renders one separator for each page column ('w:col' / 'w:num') in DOM painter
Closes #2067
* test: add docx file with line separator between columns enabled
* fix: rendering color of separators
* fix: reuse ColumnLayout and SINGLE_COLUMN_DEFAULT
* fix: ensure column width larger than separator width
* chore: lint
* chore: fix imports and missing SINGLE_COLUMN_DEFAULT usage
* chore: reuse type
* fix: render column separators per region on pages with continuous breaks
Two related fixes so the new column-line-separator feature works correctly
on pages where a continuous section break changes column layout mid-page.
1. isColumnConfigChanging now compares withSeparator. Before, a sep-only
toggle (count+gap unchanged) returned false, so no mid-page region was
created and the toggle was silently dropped. Applied in both
section-breaks.ts and the inline fallback in index.ts.
2. constraintBoundaries captured during layout are serialized onto a new
page.columnRegions contract field. renderColumnSeparators now iterates
regions and draws each separator bounded by its yStart/yEnd instead of
painting a single full-page overlay. When no mid-page change occurs,
columnRegions is omitted and the renderer falls back to page.columns
(unchanged behavior).
Verified by loading a fixture with 7 scenarios (2-col, 3-col, unequal
widths, separator on/off, continuous breaks toggling the separator).
Pages now show per-region separators tiled correctly; a 3-col region
followed by a 2-col region no longer paints a shared full-page line.
Out of scope here, tracked for follow-up: widths/equalWidth are still
dropped at pm-adapter extractColumns, so unequal-width separators render
at the equal-width midpoint; body-level w:sep is dropped at v2
docxImporter; there is no w:sep export.
* test: cover DomPainter renderColumnSeparators
13 unit tests over the separator renderer. Splits coverage into the
fallback path (page.columns only) and the region-aware path
(page.columnRegions).
Fallback path: pins the 2-col and 3-col geometry, and each early-return
guard (withSeparator false/undefined, single column, missing margins,
no columns at all, pathologically-small columnWidth).
Region path: verifies per-region yStart/yEnd bounding, mixed regions
(some draw, some skip for withSeparator=false or count<=1), zero-height
regions, and that columnRegions wins when both it and page.columns are
present.
Previously renderColumnSeparators had zero DOM-level coverage — the
region-aware refactor in the prior commit relied entirely on
layout-engine tests that never exercised the DOM output.
---------
Co-authored-by: Caio Pizzol <caio@harbourshare.com>
…ort (#2760) * fix(sdk): raise asyncio StreamReader buffer in Python AsyncHostTransport The Python async transport spawned the host CLI without passing a `limit=` to `asyncio.create_subprocess_exec`, so its stdout `StreamReader` inherited asyncio's default 64 KiB buffer. Every host response is written as a single newline-delimited JSON line, so any `cli.invoke` whose serialized result exceeds 64 KiB (e.g. `superdoc_get_content` on larger documents) caused `readline()` to raise `ValueError: Separator is not found, and chunk exceed the limit` inside `_reader_loop`. The exception was caught by the generic reader-loop handler and pending requests were rejected with the misleading `HOST_DISCONNECTED` error — even though the host process was still alive and healthy. Pass `limit=` to `create_subprocess_exec` and expose it as a new `stdout_buffer_limit_bytes` constructor option on `AsyncHostTransport`, threaded through `SuperDocAsyncRuntime` and `AsyncSuperDocClient`. The default of 64 MiB safely covers the host's own 32 MiB `DEFAULT_MAX_STDIN_BYTES` input cap with room for ~2x JSON expansion. `SyncHostTransport` is unaffected — it uses raw blocking `subprocess.Popen` which has no asyncio buffer limit. Adds a `TestAsyncLargeResponse` regression suite that: 1. Round-trips a 200 KB response through the default-configured transport. 2. Pins that an explicitly tightened `stdout_buffer_limit_bytes` still reproduces the original failure mode, guaranteeing the option is wired through to `create_subprocess_exec`. * fix(sdk): tear down host process on async reader-loop failure AsyncHostTransport._reader_loop caught reader exceptions by rejecting pending futures and flipping state to DISCONNECTED, but never killed self._process. Because dispose() early-returns on DISCONNECTED, any reader-loop failure left an orphaned host subprocess running with no public API to reap it. This is a pre-existing bug, but the previous commit made it easier to trip by exposing stdout_buffer_limit_bytes: any caller who sets it below their real response size hits the orphan path. Route both the buffer-overflow and generic-error branches through a new _schedule_cleanup helper that fires _cleanup() as a separate task (it can't be awaited inline — _cleanup cancels and awaits the reader task itself). _cleanup kills the process, waits on it, rejects pending, and only then transitions to DISCONNECTED, so a subsequent dispose() is a safe no-op instead of leaking the host. Also catch asyncio.LimitOverrunError / ValueError separately and surface HOST_PROTOCOL_ERROR with a "raise stdout_buffer_limit_bytes" hint plus the current limit in details. The previous HOST_DISCONNECTED code pointed users at the wrong problem since the host was still alive. Extends TestAsyncLargeResponse to assert HOST_PROTOCOL_ERROR, verify the hint is in the message, confirm the subprocess is actually reaped (returncode set, _process cleared), and that dispose() after an overflow is a safe no-op. * refactor(sdk): dedupe stdout_buffer_limit default and add wiring test Address review follow-ups on the async transport buffer-limit option. - Hoist DEFAULT_STDOUT_BUFFER_LIMIT_BYTES (64 MiB) to module scope in transport.py and reference it from AsyncHostTransport, the async runtime, and AsyncSuperDocClient so the default lives in one place instead of three copies of 64 * 1024 * 1024. - Add a short "raise if a single host response can exceed this size" comment on the client.py parameter so callers see the guidance at the public API boundary, not buried in transport.py. - Rename test_response_above_default_64kb_buffer to test_response_above_asyncio_default_streamreader_limit. 64 KiB is asyncio's default, not the SDK's (which is now 64 MiB), so the old name read backwards after this PR. - Add test_client_threads_stdout_buffer_limit_to_transport: builds AsyncSuperDocClient with a custom limit and asserts the value reaches AsyncHostTransport. Without this, a silent drop of the arg in client.py or runtime.py would leave the existing overflow test passing while the public API reverts to the asyncio 64 KiB default. * fix(sdk): mark transport DISPOSING synchronously on reader teardown Round-2 review follow-ups: - _schedule_cleanup now flips state to DISPOSING before scheduling the cleanup task. Previously, between the reader returning and the async _cleanup running, _ensure_connected's CONNECTED fast path would still accept invoke() calls; they then blocked on a future the dead reader could never resolve until watchdog_timeout_ms (default 30s). - Narrow the buffer-overflow catch to readline() only and drop asyncio.LimitOverrunError from the tuple. readline() re-raises LimitOverrunError as ValueError (it is not a ValueError subclass on any supported CPython), so the previous broad except could reclassify unrelated ValueErrors from dispatch as a buffer-limit error with a misleading remediation hint. Comment corrected to match. - Re-export DEFAULT_STDOUT_BUFFER_LIMIT_BYTES from superdoc/__init__.py so consumers tuning the option don't import from the implementation module. - Tighten test_host_crash to assert HOST_DISCONNECTED specifically and verify process teardown via the new _schedule_cleanup path. - Strengthen the dispose-after-overflow assertion to actually verify the no-op claim (state stays DISCONNECTED, _process stays None, a second dispose is also safe). Replace the timing-sensitive process.returncode read with await process.wait(). * fix(sdk): serialize teardown across reader, _kill_and_reset, and dispose Round-2 follow-up — addresses the residual race that the synchronous DISPOSING flip didn't cover. Before: `_kill_and_reset()` (called from `_send_request` on stdin write failure or watchdog timeout) `await`ed `_cleanup` directly. If a reader-triggered `_schedule_cleanup` was in flight, both ran concurrently and raced on `_reject_all_pending`'s read-then-clear of `self._pending` (futures added between snapshot and clear were leaked) and on `process.kill()`/`reader_task.cancel()`. `dispose()` similarly short-circuited on DISPOSING without waiting for the in-flight cleanup to finish — the caller saw "disposed" before the host was fully torn down. Now: - `_kill_and_reset` and `dispose` both check the cleanup-task slot and `await` an in-flight cleanup rather than starting a parallel one. Single-flight teardown across all three entry points. - `_cleanup` clears `self._cleanup_task` in `finally` when it owns the slot, so introspection doesn't surface a stale done handle and the next teardown gets a fresh slot. - `dispose()` after a reader-triggered cleanup now blocks until that cleanup finishes, restoring the "host fully torn down on return" contract. Tests: - `test_schedule_cleanup_dedupe_guard_drops_reentrant_call` — second `_schedule_cleanup` does not replace the in-flight task slot. - `test_overflow_during_dispose_does_not_schedule_cleanup` — `_stopping` suppression is honored. - `test_kill_and_reset_awaits_in_flight_cleanup` — `_kill_and_reset` observes the existing task instead of running a parallel `_cleanup`. - `test_dispose_waits_for_in_flight_cleanup` — `dispose()` blocks until reader-triggered cleanup completes before returning. 95 transport tests pass; 5 consecutive runs with PYTHONASYNCIODEBUG=1 show no flakes. * fix(sdk): close residual races in async transport teardown Two correctness regressions and three test gaps surfaced in the final-pass review of the cleanup-task lifecycle. **1. _ensure_connected race (HIGH).** The synchronous DISPOSING flip in _schedule_cleanup did not gate _ensure_connected, so a concurrent connect()/invoke() reaching _start_host during the DISPOSING window would reassign self._process and self._reader_task. The pending cleanup task then read those slots after its first await and killed the freshly-spawned process. Fix: drain self._cleanup_task at the top of _ensure_connected via asyncio.shield (so a cancelled caller doesn't abort the in-flight cleanup). **2. Cancellation propagation race (HIGH).** _kill_and_reset and dispose() awaited the cleanup task without asyncio.shield. When the caller (e.g. an invoke task at the watchdog branch) was cancelled, asyncio cancelled the awaited cleanup task too — _cleanup did not catch CancelledError around process.wait(), so teardown stopped before clearing _process / setting state. dispose() then saw DISPOSING with _cleanup_task=None and returned without finishing teardown, leaking the host process. Fix: wrap the awaited cleanup in asyncio.shield in both call sites; restructure _cleanup so it captures handles and sets state synchronously up-front, before any awaits, so observable state is always consistent. **3. Move _stopping guard into _schedule_cleanup.** The previous test_overflow_during_dispose_does_not_schedule_cleanup was tautological — it set _stopping=True and then re-checked the same condition in the test body before calling _schedule_cleanup, so the call never ran and the assertion passed trivially. Move the guard into _schedule_cleanup itself (it's the correct authoritative location anyway), remove the now-redundant call-site checks in _reader_loop, and rewrite the test to call _schedule_cleanup unconditionally with _stopping=True. The test now actually exercises the production guard. **4. Multi-pending-invoke overflow test.** Codex round-2 gap that remained open. Locks down that _reject_all_pending fails ALL pending futures with HOST_PROTOCOL_ERROR plus the actionable hint, not just the one whose response overflowed. **5. Async reconnect-after-buffer-overflow test.** Sync transport already had test_reconnect_after_failure; async only covered reconnect after explicit dispose. Validates that reader-triggered cleanup leaves the transport reusable for a fresh invoke without wedging _cleanup_task / _connecting / _process. Plus: replaced asyncio.sleep(0) with asyncio.Event-based synchronization in lifecycle tests (Codex/Opus medium — sleep(0) is implementation-defined under uvloop / Python scheduling changes); two new tests directly cover the round-3 races (test_ensure_connected_drains_in_flight_cleanup_before_spawn, test_kill_and_reset_caller_cancellation_does_not_cancel_cleanup). 99 transport tests pass; 5 consecutive runs with PYTHONASYNCIODEBUG=1 show no flakes; new tests pass under -W error::ResourceWarning. --------- Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com> Co-authored-by: Caio Pizzol <caio@harbourshare.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> Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
…869) (#2676) * fix: use actual font size for tab runs instead of hardcoded 12pt default The tab measurement code hardcoded `maxFontSize: 12` for tab-created lines and tab runs, causing incorrect line height calculations when the surrounding text used a different font size. This also adds `fontSize: '0'` to line styles to eliminate the CSS strut that caused baseline misalignment between normal-flow and absolutely-positioned (tab-aligned) children. * fix: prevent text overlap when multi-column section spills across pages When a multi-column section's content spans multiple pages and columns have unequal heights, the next section's content was positioned at the shorter column's cursor Y instead of below all column content. This caused visible text overlap on page 2. Add maxCursorY to PageState to track the deepest Y reached across all columns. startMidPageRegion now uses this value so the next section begins below all column content. * fix: eliminate CSS strut misalignment between tab-segmented and normal lines Lines with tab stops use absolute positioning for text segments, which bypasses the CSS strut. Normal-flow lines inherit the browser's default 16px font-size, creating a strut that shifts text down ~1px via baseline alignment. This made tab-indented first lines appear shifted up relative to continuation lines. Zero the line container's font-size to remove the strut, and restore explicit font-size on the three child elements that inherit rather than set their own: empty-run caret placeholder (lineHeight), math run wrapper (run height), and field annotation wrapper (16px fallback). * test: add regression test for maxCursorY overlap fix (SD-1869) Verifies that mid-page column transitions start below the tallest column when columns have unequal heights. Uses a 3-col → 2-col transition to exercise the maxCursorY tracking without triggering the new-page guard (columnIndexBefore >= newColumns.count). * fix: use browser default font size for math run fallback run.height is a layout heuristic that can reach 80–100px for tall expressions (fractions, equation arrays). Using it as the wrapper's font-size made the plain-text fallback render at that size. Swap to BROWSER_DEFAULT_FONT_SIZE (16px) — MathML has its own scaling, so the value only affects the textContent fallback path. --------- Co-authored-by: Caio Pizzol <caio@harbourshare.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
…2867) Pulling the full corpus (~450 .docx files, ~100 MB) on every new worktree is wasteful: the bytes are identical to the primary repo already sitting on disk. Detect when we're in a git worktree via `git rev-parse --git-common-dir`, then hardlink any files the primary has into the worktree's corpus dir before hitting R2. Only files the primary is missing go over the network. --force still re-downloads from R2 (we unlink the destination before writing so the primary's inodes never get clobbered). --no-seed opts out for users who want the old behaviour.
* feat(math): implement m:box and m:borderBox converters (closes #2605) Made-with: Cursor * fix(math): parse full ST_OnOff values in borderBox converter Made-with: Cursor * fix(math): fall back to mrow when borderBox hides all sides with no strikes Made-with: Cursor * fix(math): address review findings for m:box/m:borderBox - isOn now checks m:val === undefined instead of !el.attributes so elements with namespace-only attributes are still treated as on, matching the ST_OnOff default per §22.9.2.7. - convertBorderBox returns null for empty m:e, consistent with convertBox and convertFunction (no empty <menclose> wrappers). - m:box JSDoc now reflects that boxPr semantics (opEmu, noBreak, aln, diff, argSz) are silently dropped — not "purely a grouping mechanism". - Registry comment drift fixed: m:box and m:borderBox moved into the Implemented block. - Tests: strike direction mapping (BLTR→up, TLBR→down, V), full ST_OnOff matrix (1/true/on/bare/0/false), tightened assertions to exact-string equality, pinned the current boxPr-drop behavior. * feat(math): polyfill MathML <menclose> via CSS MathML Core (Chrome 109+, 2023) dropped <menclose> — no browser paints it natively. Without this, m:borderBox content imports correctly but renders invisibly. Ship a small CSS polyfill that maps every notation token to borders or pseudo-element strike overlays: - box / top / bottom / left / right → CSS border sides - horizontalstrike / verticalstrike → ::after gradient layer (H or V) - updiagonalstrike / downdiagonalstrike → layered gradients via CSS custom properties so X patterns stack correctly Wired through the existing ensure*Styles pattern in renderer.ts. Zero bundle cost, no runtime polling, fully semantic (the DOM still says <menclose notation="box">). * fix(math): correct diagonal strike directions in menclose polyfill CSS linear-gradient direction keywords confusingly produce stripes perpendicular to the direction vector: - "to top right" progresses toward the top-right corner, which makes the visible color stripe run top-left to bottom-right ("\") - "to bottom right" progresses toward the bottom-right corner, which makes the stripe run bottom-left to top-right ("/") The polyfill had them swapped, so updiagonalstrike rendered as "\" and downdiagonalstrike as "/" — the opposite of what Word shows and what MathML 3 specifies. Swap the direction keywords and add a comment so the next reader doesn't re-flip them. * fix(math): wrap borderBox content in <mrow> for horizontal row layout MathML Core does not define <menclose>, so Chrome treats it as an unknown element and does not run the row-layout algorithm on its children. Each child rendered with display: block math and stacked vertically — a multi-element expression inside a borderBox (e.g. Annex L.6.1.3's a² = b² + c²) became a column of letters. Wrap the content in an inner <mrow> before appending to <menclose>. <mrow> is in MathML Core, so the row layout runs on its children and everything stays inline. The outer <menclose> remains the polyfill target for borders and strikes. * test(behavior): cover m:borderBox + menclose polyfill end-to-end Loads the 30-scenario fixture (sd-2750-borderbox.docx) and asserts: - every scenario produces the expected notation attribute in DOM order - multi-child content (Annex L.6.1.3: a² = b² + c²) renders as a horizontal row — width > 1.5× height, inner <mrow> present, 5 children - ST_OnOff variants (1/true/on/bare/0/false) resolve correctly through the full import path, not just the unit converter - m:box silently drops boxPr (opEmu/noBreak/aln/diff) and emits <mrow> - the menclose CSS polyfill stylesheet is injected into the document Runs across chromium/firefox/webkit. Complements the 53 unit tests by exercising the cross-package path: OMML import → pm-adapter → painter-dom → rendered MathML. --------- Co-authored-by: Caio Pizzol <caio@harbourshare.com>
* fix(super-editor): remove toolbar window listeners on unmount * fix: setup window listeners on onMounted and onActivated * test: assert Toolbar window listener cleanup * fix(super-editor): unmount toolbar Vue app on SuperToolbar.destroy Without this, the Toolbar component's onBeforeUnmount hook never runs when SuperDoc is destroyed, so the window resize/keydown listeners this PR registers are never removed. Verified in the dev app: dispatching a resize event after destroy still fired the toolbar handler; with the unmount call the handlers are cleanly removed. Also: - drop dead teardownWindowListeners() call inside setupWindowListeners (addEventListener is idempotent for the same type+listener pair) - add afterEach(vi.restoreAllMocks) to Toolbar.test.js so spies don't leak across tests if an assertion throws --------- Co-authored-by: Caio Pizzol <caio@harbourshare.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* docs: uptade Angular example to use modern angular features, like input and viewChild signals * docs: update Angular documentation to instruct how to use in version prior to 17.1 * docs(angular): correct signal-API version requirements viewChild() landed in 17.2 (not 17.1) and is stable only from Angular 19; input() is 17.1+ and stable from 19. Verified via tsc against @angular/core@17.1.0: 'has no exported member named viewChild'. Also aligns onReady log string with React/Vue docs. --------- Co-authored-by: Caio Pizzol <caio@harbourshare.com> Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
….114 (#2862) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
…s (SD-2538) (#2871) * fix(math): preserve italic on variables inside function-wrapped limits (SD-2538) Word wraps lim_(n→∞) as m:func > m:fName > m:limLow. convertFunction was calling forceNormalMathVariant with querySelectorAll('mi'), which recursed into the <munder> produced by m:limLow and stripped italic from every variable inside m:lim. Scope forceNormalMathVariant so it walks only non-structural children and never overwrites an existing mathvariant — preserves m:sty/m:scr authored styling on the function name and leaves nested limit, subscript, superscript, matrix cells, etc. untouched. * docs(llms): mention native math equation rendering * test(math): add behavior-level regression for SD-2538 Pins the end-to-end shape of the fix by loading math-limit-tests.docx and asserting that every <mi> inside a lim-based <munder>'s limit expression has no mathvariant attribute — i.e. variables in m:lim stay italic when m:limLow is wrapped in m:func > m:fName. Matches the canonical shape produced by Word's own OMML2MML.XSL. * refactor(math): apply review feedback on SD-2538 fix - Rename STRUCTURAL_MATHML → MATH_VARIANT_BOUNDARY_ELEMENTS; the intent is "where the walk stops", not "which elements are structural". - Drop the inner walk closure and the Array.from snapshot — the outer function already recurses cleanly; neither earned its keep. Tests: - Fold the SD-2538 unit test into the adjacent "m:limLow in m:func" test; the fixture shape was identical, only the assertions differed. - Extend "m:limUpp in m:func" with the symmetric mathvariant assertions so the 'mover' entry of the boundary set is pinned, not just 'munder'. - Add a preserve-branch test (m:sty=i on function name) to pin the !hasAttribute guard — previously no test would have failed if we'd removed it. - Behavior test: assert exact count (5 munder + 1 mover) instead of >0, and cover both limLow and limUpp in the same sweep. Review context: Word's own OMML2MML.XSL confirmed as parity target. The subtree-opaque concern from Codex was checked against the XSL output for m:func > m:fName > m:sSub (f_i(x)) and matches our post-fix behavior. * fix(math): restore Array.from on HTMLCollection iteration The review-driven removal of Array.from broke type-check in CI: HTMLCollection is not iterable under the default lib.dom.d.ts (needs `dom.iterable`), so `for…of root.children` fails TS2488. Bun's looser runtime type-check let it slide locally. Restoring Array.from with a comment explaining why.
…#2874) * feat(toolbar): auto-derive font-option style from label/key (SD-2611) Consumers passing { label, key } to modules.toolbar.fonts got a dropdown where every row rendered in the toolbar's UI font — rows only render in their own typeface when each option carries props.style.fontFamily, and the API silently expected callers to know that shape. This adds a small normalizer that fills in props.style.fontFamily (from key or label) and data-item, so the minimal { label, key } shape just works. * docs(toolbar): align fonts API docs and FontConfig type with runtime behavior The docs said `key` is 'font-family CSS value applied to text', but the runtime applies `option.label` — the font dropdown doesn't set `dropdownValueKey`, so ButtonGroup falls back to `option.label` when emitting setFontFamily. The sample `{ label: 'Times', key: 'Times New Roman' }` also violated the active-state matcher (which compares `label` to the first segment of the document's font-family), so the 'active' chip never lit up. Rewrites the field descriptions to match what each value actually does, fixes the example to follow the label-equals-first-key-segment convention, and surfaces the optional `props.style.fontFamily` preview override.
* fix: toc not loading on paragraphs * fix: tab stop computation and alignment for TOC styles * fix: tests * fix: don't decode every child twice Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com> * fix: build * refactor: removed unused code * chore: small code tweaks * chore: small code tweaks * chore: removed unused code * fix(layout-engine): tab-alignment parity, TOC schema invariant, regression tests Follow-ups from PR review: - measuring/dom: sync sequentialTabIndex with explicit tabIndex values (mirror remeasure.ts). - Document getAlignmentStopForOrdinal as a Word-compat heuristic, not ECMA 17.3.3.32 behavior. - tableOfContents encoder: normalize every non-paragraph child into its own paragraph so the schema invariant (paragraph*) holds for mixed inline + paragraph input, not just the all-inline case. - Add regression tests: 3-tabs + 2-alignment-stops asymmetric case, mixed start+end stops preserving legacy defaults-after-rightmost behavior, tightened TOC leader assertion. * test(behavior): add TOC tab-alignment regression for SD-2447 Playwright spec that loads a TOC1 fixture and asserts leader endpoints align, page numbers right-justify, and leader start positions vary with title length (proving the first tab falls on the default grid, not the right stop). Fixture is a Word-native 14KB DOCX with a right-aligned dot-leader tab stop on TOC1 style — the same paragraph shape as the reported Keyper document. --------- Co-authored-by: Gabriel Chittolina <gabrielchittolina1@gmail.com> Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com> Co-authored-by: Caio Pizzol <caio@harbourshare.com>
…kChanges (SD-2608) (#2847) * refactor(config): consolidate track-changes config under modules.trackChanges (SD-2608) track-changes config lived in two places (config.trackChanges for visibility, config.layoutEngineOptions.trackedChanges for mode/enabled), unlike every other module which sits under config.modules.*. consumers had to learn three keys for one module. this adds modules.trackChanges as the canonical path and keeps both legacy keys working as deprecated aliases with one-time warnings. - new normalizer in core/helpers resolves canonical + legacy with precedence new > legacy > derived default, and mirrors values back to legacy paths so the ~14 internal reads keep working untouched - accepts all four TrackedChangesMode values (review | original | final | off) to preserve existing pass-through of layoutEngineOptions.trackedChanges - suppresses re-warning on a second normalization of the same config object so write-through values don't look like new legacy usage - JSDoc, Mintlify docs, and the one SuperDoc.test.js legacy use-site updated * feat(track-changes): add pairReplacements mode matching Word/ECMA-376 (SD-2607) (#2849) * feat(track-changes): add pairReplacements mode matching Word/ECMA-376 (SD-2607) when track changes are on and a user replaces text, SuperDoc groups the insertion and deletion under one shared id so accepting or rejecting takes one click (Google-Docs-like). Microsoft Word and ECMA-376 §17.13.5 treat every <w:ins> and <w:del> as an independent revision with its own required w:id. a consumer wants their UI to match Word. this adds modules.trackChanges.pairReplacements (default true preserves current behaviour). when false, each insertion and each deletion is an independent change, addressable and resolvable on its own. - importer: buildTrackedChangeIdMap accepts { pairReplacements }; when false, skips the adjacent w:del+w:ins pairing so each Word w:id maps to its own UUID - insertTrackedChange: no shared id on replacements in unpaired mode - getChangesByIdToResolve: returns just the single matching mark in unpaired mode (no neighbor walk) - wiring: SuperDoc.vue -> editor.options.trackedChanges -> Editor.ts -> SuperConverter.trackedChangesOptions -> docxImporter no exporter change needed — <w:ins>/<w:del> are already written per-mark with their own w:id in both modes. no public API shape change. * fix(track-changes): address review findings on pairReplacements (SD-2607) - always walk adjacent same-id marks in getChangesByIdToResolve so a single logical revision split across multiple segments resolves as one unit; the unpaired case is handled implicitly because ins/del now have distinct ids - align changeId with the insertion mark's id so comment threads and the optional comment reply attach to the same thread in unpaired mode - simplify id-minting: one primary id anchors the operation; the deletion mints its own fresh id only when unpaired AND it's a replacement. the Document API write adapter now gets unpaired revisions when the flag is off without any adapter-level change - add trackedChanges?: {...} to EditorOptions so consumers don't need casts - add an unpaired-mode example snippet to the docs - extension test now covers the headline guarantee: in unpaired mode, acceptTrackedChangeById(insertionId) resolves only the insertion, and the deletion is still independently rejectable by its own id * fix(types): tighten JSDoc signatures so type-check passes (SD-2608) the normalizer and tracked-change mapper emit strict typedefs but several helper signatures were relying on implicit any, and the NORMALIZED_MARKER symbol indexed an under-typed config object. CI type-check rejected these once @ts-check saw the full types. - normalize-track-changes-config.js: explicit JSDoc param types on helpers, Record<string, any> on config so module/track-chains resolve, symbol cast for the NORMALIZED_MARKER access - trackedChangeIdMapper.js: nested walk now propagates pairReplacements instead of emitting an under-typed WalkContext * refactor(track-changes): rename pairReplacements → replacements enum (SD-2608) pairReplacements leaked editor-speak and forced consumers to mentally invert a boolean to get the Word-style model. rename to a self-documenting enum that sits alongside future per-behavior siblings under modules.trackChanges. - public surface: `modules.trackChanges.replacements: 'paired' | 'independent'` default `'paired'` (no behavior change for existing consumers) - plumbing: Editor.options.trackedChanges.replacements → SuperConverter trackedChangesOptions → buildTrackedChangeIdMap; parallel path through trackedTransaction → replaceStep so user-driven replacements (the Firefox coalescing path) also honor the flag - replaceStep now passes id: undefined to markDeletion in 'independent' mode so the deletion mints its own id — closing the UI-typing gap - docs: configuration.mdx ParamField + example use the enum; track-changes extension doc gets a new "Revision model" section explaining paired vs independent with snippets - tests: normalizer, mapper, extension, and a new behavior spec (tracked-change-independent-replacement.spec.ts) exercise the enum end-to-end across chromium/firefox/webkit; harness + fixture accept a `replacements` URL param so tests can flip the flag * docs(track-changes): migrate from extensions to modules (SD-2608) with SuperDoc moving away from ProseMirror and extensions no longer appearing in the public nav, track changes reads better as a first-class module page alongside comments. the new modules/track-changes.mdx consolidates what consumers actually need: configuration, revision model (paired vs independent), the Document API (editor.doc.trackChanges.*), editor commands, events, permissions, and DOCX import/export. - new: apps/docs/modules/track-changes.mdx modeled on modules/comments.mdx - nav: docs.json lists modules/track-changes after modules/comments - redirect: /extensions/track-changes → /modules/track-changes so bookmarked URLs and external links keep working - deleted: apps/docs/extensions/track-changes.mdx (was hidden anyway) - updated: modules/overview, extensions/overview, and superdoc/configuration cross-links point at the new page * docs(track-changes): correct docs against codebase + lead with Document API (SD-2608) audited every claim on modules/track-changes.mdx against packages/ and fixed the drift. the Document API is now the primary surface; legacy editor commands are grouped under a single deprecated section. corrections: - event payload is flat ({ type: 'trackedChange', event, changeId, ... }), not nested under { type, comment }. fields trackedChangeType, trackedChangeText, deletedText, trackedChangeDisplayType, author, authorEmail, date, importedAuthor come straight from comments-plugin.js:1134 - event values pulled from shared/common/event-types.ts: add, update, deleted, resolved, selected, change-accepted, change-rejected - permissions include RESOLVE_OWN / RESOLVE_OTHER (accept) alongside REJECT_OWN / REJECT_OTHER (permission-helpers.js:3-12) - trackChanges.list() returns .items (not .changes) per DiscoveryOutput - removed disableTrackChangesShowFinal() — not implemented - mark names confirmed: trackInsert / trackDelete / trackFormat restructuring: - Document API section moved up and gets the full TrackChangeInfo shape - editor.commands usage moved to a "Legacy editor commands" section at the bottom, wrapped in a Warning banner pointing at the Document API - voice aligned with brand.md: shorter sentences, concrete examples, "Import it. Edit it. Export it. Nothing lost." over abstract claims no code changes. * docs(track-changes): use Lucide play icon for the example card (SD-2608) * docs(track-changes): use git-compare icon to match module theming (SD-2608) * feat(dev-app): add tracked-replacements mode toggle (SD-2608) expose modules.trackChanges.replacements in the SuperDoc dev app so developers can test both 'paired' (Google Docs) and 'independent' (Word) revision models without editing code. - reads the initial value from ?replacements=independent in the URL so test links can pin a specific mode - switching via the header <select> updates the URL param and reloads, same pattern used by the Layout Engine and View Layout toggles - moved the legacy top-level trackChanges: { visible: true } config to modules.trackChanges.{visible, replacements} so the dev app stops triggering its own deprecation warning
…2632) (#2875) * fix(math): split multi-char m:r text per character to match Word (SD-2632) Word's OMML2MML.XSL classifies each character in an m:r run individually — digits group into a single <mn> (with one optional decimal point between digits), operator characters each become their own <mo>, and everything else becomes its own <mi>. SuperDoc was emitting the entire run text as one element, so runs like "→∞" or "x+1" rendered as a single <mi>, losing operator spacing and semantics. tokenizeMathText implements the per-character classification. convertMathRun returns a single Element for one-atom runs and a DocumentFragment when multiple atoms are emitted, so siblings flow directly into the parent's <mrow> without an extra wrapper. m:fName is the documented exception — Word keeps multi-letter function names like "sin" or "lim" as one <mi> inside the function-name slot. convertFunction routes m:r children through convertMathRunWhole (no splitting), and a new collapseFunctionNameBases pass re-merges the base slot of any structural MathML element (munder/mover/msub/…) that Word nested inside m:fName — without this, "lim" inside m:limLow would incorrectly split to three <mi>. Also drops U+221E (∞) from OPERATOR_CHARS — it's a mathematical constant per Word's XSL, not an operator. MathObjectConverter's return type widens from Element|null to Node|null so convertMathRun can return a DocumentFragment. All other converters already return Element, which is assignable to Node — no other changes. Verified against real Word-native fixtures: `→∞` in the limit-tests fixture case 1 now renders as <mi>n</mi><mo>→</mo><mi>∞</mi> (matches Word OMML2MML.XSL byte-for-byte), and nested limits keep their function names intact. Ref ECMA-376 §22.1.2.116, Annex L.6.1.13, §22.1.2.58. * fix(math): group letters inside m:fName mixed runs + cover mmultiscripts (SD-2632) Follow-up to /review feedback backed by fresh Word OMML2MML.XSL evidence: - `convertMathRunAsFunctionName` (renamed from `convertMathRunWhole` since "whole" no longer fits) now groups consecutive non-digit / non-operator characters into one <mi> while still splitting digits and operators. Word's XSL for `<m:fName><m:r>log_2</m:r></m:fName>` produces `<mi>log</mi><mo>_</mo><mn>2</mn>` — not `<mi>l</mi><mi>o</mi><mi>g</mi>…`. - `BASE_BEARING_ELEMENTS` gains `mmultiscripts` — Word emits it when an `m:sPre` sits inside `m:fName`; our base-collapse pass needs to know to merge the first-child <mi> run. - CONTRIBUTING.md now documents the widened `Node | null` return type. Tests added: - Direct `tokenizeMathText` edge cases: `.5` / `5.` / `1.2.3` / `2x+1` / consecutive operators / empty / standalone ∞. - m:fName mixed-content: `log_2` stays `<mi>log</mi><mo>_</mo><mn>2</mn>`. - Base collapse inside nested `m:sSub` under `m:fName`. - Base collapse inside nested `m:sPre` (mmultiscripts) under `m:fName`. - Behavior test tightened to pin the full 3-atom sequence for `n→∞`. Disputed during review and deferred with evidence: - Opus claim that standalone `m:limLow` with "lim" base regresses to italic: Word XSL itself splits "lim" per-char in that shape (with or without m:sty=p), so our output matches Word. - Codex claim that Arabic-Indic digits should be `<mn>`: Word XSL also classifies them as `<mi>`, so our behavior matches. - Non-BMP surrogate-pair support: edge case in extreme mathematical alphanumerics; Word XSL itself errored on U+1D465. Separate ticket worth. * fix(math): iterate tokenizer by code point to preserve surrogate pairs Addresses Codex bot review on PR #2875. Astral-plane mathematical alphanumerics (e.g. U+1D465 mathematical italic x, U+1D7D9 mathematical double-struck 1) are UTF-16 surrogate pairs. Walking text by code unit split them into two half-pair <mi> atoms with invalid content. `codePointUnitLength` returns 2 when the current position starts a surrogate pair so tokenizeMathText and tokenizeFunctionNameText step across the full code point.
…cker (SD-2635) (#2876) * fix(react): stabilize user/users/modules props to prevent re-init flicker (SD-2635) Consumers passing inline object literals (the idiomatic React pattern) caused a full SuperDoc destroy + rebuild on every parent re-render because the main useEffect compared these props by reference identity. When a consumer stored the SuperDoc instance in state from onReady, the resulting re-render supplied fresh references and triggered another cycle — observed as 4 full destroy/re-init cycles in ~100ms with visible display:none/block flicker. Wrap user, users, and modules in a new useStableValue hook that returns a reference-stable version only changing identity when the structural content actually changes (via JSON.stringify compare, run only on reference-change). Semantics are strictly a superset of the prior behavior — value changes still rebuild; reference-only changes no longer do. * fix(react): drop modules from structural memo; harden tests (SD-2635 review round 2) Review feedback addressed: - Correctness: `modules` carries function-valued options (`permissionResolver`, `popoverResolver`) and live objects (`collaboration.ydoc`/`provider`) that JSON.stringify silently drops or collapses. Keeping it on structural compare would silently ignore real config changes. Reverted to reference identity for `modules`; `user` and `users` stay memoized since they are plain data. - Naming: renamed `useStableValue` → `useStructuralMemo` to match the useMemo family and signal non-referential equality. - JSDoc: expanded the hook's docblock to spell out every JSON.stringify footgun consumers need to know about (functions dropped, class instances collapsed, undefined values dropped, NaN/Infinity → null, circular refs, key-insertion-order sensitivity). - Tests: replaced the brittle `setTimeout(100)` negative assertion with a synchronous `ref.getInstance()` identity check; strengthened the "rebuilds on change" test to also assert a second onReady + a fresh instance; added a `users`-prop stability test; added a StrictMode + rerender test to guard the ref-write-during-render path. * fix(react): define event types explicitly to unblock CI type-check (SD-2635) CI type-check failed with `Property 'onEditorUpdate' does not exist on type 'Config'` even though the JSDoc `Config` typedef in superdoc clearly declares it. The old approach derived SuperDocEditorUpdateEvent and SuperDocTransactionEvent via `Parameters<NonNullable<SuperDocConstructorConfig['onEditorUpdate']>>[0]`, which walked a chain: ConstructorParameters<typeof SuperDoc>[0] → @param {Config} in core/SuperDoc.js → @typedef Config in core/types/index.js → @Property onEditorUpdate with @typedef EditorUpdateEvent This chain resolves fine locally but breaks on CI — the exact failure point in JSDoc resolution depends on TS version, moduleResolution mode, and the `customConditions: ["source"]` in tsconfig.base.json (which routes imports to the raw .js source instead of the built .d.ts). Define the two event shapes inline instead, mirroring superdoc's JSDoc. No behavior change for consumers — same fields, same optionality. * fix(react): round-4 review feedback — lock modules contract, restore transaction: any (SD-2635) Addressing consensus findings from the round-3 review: - Revert `transaction: unknown` back to `any` to match superdoc's upstream typedef. `unknown` was a narrowing from `any` that would have broken existing consumer code like `event.transaction.docChanged`. - Re-export `EditorSurface` from the package barrel. It's referenced by two public event interfaces but wasn't exported, so consumers couldn't name the `surface` field's type. - Symmetrize per-field JSDoc on `SuperDocTransactionEvent` to match its sibling `SuperDocEditorUpdateEvent`. - Add a regression test asserting that passing a new `modules` object with identical content DOES rebuild the editor. This pins the contract that `modules` stays on reference identity (it can carry functions and live objects that structural compare misses) — a future "cleanup" that wraps `modules` in useStructuralMemo would silently re-introduce the SD-2635 blocker without this test. Also trim commentary added during rounds 1-3: the stabilization rationale was documented twice in SuperDocEditor.tsx (once at the destructure, once near the dep array), and the types.ts docstrings leaked maintainer build-tooling rationale into consumer IDE hovers. * refactor(react): swap JSON.stringify compare for lodash.isequal; drop TransactionEvent (SD-2635) - Swap the hand-rolled JSON.stringify-based structural compare for `lodash.isequal`. +10KB raw / +3.7KB gzipped cost, but removes the 5-bullet footgun list from JSDoc and gets proper handling of Dates, Maps, circular refs, and key ordering for free. For our use (stabilizing `user`/`users` plain-data records) the 3.7KB buys nothing practical, but it removes ~40 lines of hand-maintained code and a bag of edge cases. Trade maintenance cost for bundle cost. - Rename `useStructuralMemo` → `useMemoByValue`. Plainer, matches the useMemo family, says what it does without jargon. - Drop `SuperDocTransactionEvent` from the public API. It was exported but never wired up to a callback prop in `CallbackProps`, so nothing fires it. Its shape also leaked ProseMirror's `transaction` object — a deprecated surface per superdoc's own notes. Removing it now is cheaper than removing it after someone starts relying on it. - Replace the JSON-stringify-specific unit tests with tests that exercise what lodash.isequal actually gives us (key-order insensitivity, same-reference function equality). 27/27 tests pass. * refactor(react): drop lodash.isequal dep, inline JSON.stringify compare (SD-2635) Round 5 added lodash.isequal for `useMemoByValue` correctness at the cost of +10KB raw / +3.7KB gzipped. For the two props actually using the hook (`user` and `users` — small plain-data records) the extra correctness buys nothing practical: those records contain strings only, no Dates, no Maps, no circular refs, no functions. Revert to an inline JSON.stringify compare wrapped in a `try/catch` for circular refs. The hook is now ~15 lines, zero dependencies, and the unit test that required lodash (key-order insensitivity) is replaced by a circular-ref fallback test that matches the implementation. Bundle is back to 3.69 KB / 1.60 KB gzipped. * test(react): cover StrictMode + user prop value change (SD-2635) The existing StrictMode test only proved same-content rerender stays stable — the positive path (real value change under StrictMode still triggers destroy + fresh onReady) wasn't exercised. Coverage audit after rounds 2-6 flagged this as the one gap worth closing before ship. Mirrors the existing non-StrictMode rebuild test, wrapped in <StrictMode>.
…ers) (#2804) * feat(layout): enable odd/even header-footer support (w:evenAndOddHeaders) Thread alternateHeaders from document settings through the layout engine so the paginator selects the correct header/footer variant per page. Fixes margin calculation for documents with different odd and even page headers. Also fixes getVariantTypeForPage to use document page number (not section-relative) for even/odd selection, matching the rendering side (headerFooterUtils.ts). Closes #2803 * fix: address PR review — type guard + multi-section/footer/fallback tests - Guard alternateHeaders behind isSemanticFlow check to match ResolvedLayoutOptions type (paginated-only field) - Add multi-section test: section 2 starting on page 4 (even by document number, verifies the documentPageNumber fix) - Add footer test: even/odd footer heights with alternateHeaders - Add default-only fallback test: only default header defined * test(layout): strengthen alternateHeaders tests and thread flag via resolver Review round 2 follow-ups on PR #2804. Tests - Footer test now goes through sectionMetadata.footerRefs + footerContentHeightsByRId (the real path) and asserts page.margins.bottom. The old test only checked body-top y, which is footer-independent — stubbing getFooterHeightForPage to always return 0 left that assertion passing. Mutation-tested: forcing getVariantTypeForPage to always return 'default' now breaks it. - Default-only fallback test now exercises the production path (headerRefs.default + per-rId heights) and asserts the correct outcome (y ~= 90 via the step-3 default fallback). Old assertion of y ~= 50 codified a code path that never runs in production because document imports always supply section refs. Mutation-tested: removing the step-3 fallback breaks this test. - New combined test: multi-section + titlePg + alternateHeaders, where section 2 has titlePg=true and starts on an even document page. Guards both the titlePg interaction across a section boundary AND the documentPageNumber (not sectionPageNumber) rule on pages 5 and 6. Mutation-tested: reverting to sectionPageNumber breaks this test alongside the original multi-section case. Layout engine - getVariantTypeForPage now takes a named-params object. Two adjacent `number` params (sectionPageNumber, documentPageNumber) are swap-vulnerable. - JSDoc on LayoutOptions.alternateHeaders cross-references getHeaderFooterTypeForSection in layout-bridge — the two sides must agree on variant selection and the pointer helps future maintainers keep them in sync. PresentationEditor - alternateHeaders is now populated inside #resolveLayoutOptions, alongside the other paginated-only fields (columns, sectionMetadata). The render-site spread collapses back to the single ternary it was before, and the `as EditorWithConverter` cast there disappears. types.ts didn't need changes — the field was already declared on the paginated variant of ResolvedLayoutOptions but unpopulated; it's now legitimately set by the resolver. * test(alternate-headers): add unit + integration + behavior coverage Follow-up to round 2 review. Closes the three test gaps flagged in the gap analysis: Unit (PresentationEditor threading) - 3 tests in PresentationEditor.test.ts assert that `converter.pageStyles.alternateHeaders` is forwarded to the layout options that reach `incrementalLayout`. Covers true, unset, and falsy-non-boolean cases. A refactor that drops the threading no longer passes silently. Unit (docxImporter) - Export `isAlternatingHeadersOddEven` and add 4 tests covering the import-side signal: `<w:evenAndOddHeaders/>` present, absent, missing settings.xml, empty settings. Pins the contract between OOXML settings and `pageStyles.alternateHeaders`. Behavior (Playwright) - `tests/headers/odd-even-header-variants.spec.ts` loads `h_f-normal-odd-even.docx` (already in the corpus) and asserts: - pages 1/3 render the default/odd header text, pages 2/4 render the even header text - page 1 and page 3 use the same `data-block-id` (same rId) but differ from page 2 — catches regressions that produce the right text from the wrong rId - footers follow the same alternation The existing layout/visual corpus already includes `h_f-normal-odd-even*.docx` and `sd-2234-even-odd-headers.docx`, so rendering regressions show up in `pnpm test:layout` and `pnpm test:visual` without any additional wiring. --------- Co-authored-by: Caio Pizzol <caio@harbourshare.com> Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
Co-authored-by: Gabriel Chittolina <gabrielchittolina1@gmail.com> Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
Co-authored-by: Artem Nistuley <artem@superdoc.dev>
…(SD-1680) (#2881) * fix(layout-bridge): keep footnote fragments within the reserved band (SD-1680) When multiple footnotes competed for space on a single page, footnote fragments could render past the bottom page margin — sometimes 150+ pixels into the footer area or off the page entirely. Caused by three interacting issues in `computeFootnoteLayoutPlan` + the multi-pass reserve loop: 1. Placement used `maxReserve` (~the whole page) as the height ceiling, so the plan happily placed more footnote content than the body layout had actually reserved at the bottom. 2. The multi-pass stabilization loop could oscillate when reducing the reserve shifted refs between pages, and exit with a `reserves` vector that didn't match what the plan placed — leaving body layout and footnote plan inconsistent. 3. The post-loop "reservesDiffer → relayout" branch could then reintroduce the mismatch by laying out with one reserve vector while the final plan operated on another. Fixes: - `computeFootnoteLayoutPlan`: when `baseReserve > 0`, cap `placeFootnote`'s `availableHeight` to `baseReserve` (the band actually reserved by the body layout), not `maxReserve`. Excess footnotes are pushed to the next page via the existing `pendingByColumn` mechanism. Initial pass (`baseReserve === 0`) still uses `maxReserve` to seed a meaningful reserve for subsequent passes. - Multi-pass loop: merge reserves element-wise with `max(prev, next)` each pass. Reserves are monotonically non-decreasing, so the loop is guaranteed to converge (bounded by `maxReserve` per page) and the old `[247] ↔ [394]` oscillation can't recur. - Post-loop: drop the `reservesDiffer → relayout` branch. With the monotonic loop already converged and placement bounded by `reserves`, `finalPlan`'s slices always fit within the body's reserved band. The extra relayout was the mechanism that reintroduced inconsistency. Visual validation (in-browser) on the 3 fixtures from the Linear issue: - reporter-test.docx: max overflow 156px → 0px - harvey-nvca.docx: overflow present → 0px - torke-carlsbad.docx: overflow present → 0px Tests: - 2 new tests in footnoteBandOverflow.test.ts asserting the invariant "every footnote fragment's bottom-Y ≤ page's physical bottom limit" for both multi-footnote pressure and oversized-footnote cases. - 1168 layout-bridge tests pass, 604 layout-engine tests pass, 1737 pm-adapter tests pass, 11385 super-editor tests pass. Follow-up (Step 3, separate PR): footnote-aware body pagination — consult footnote heights when deciding whether paragraph P fits on page N, rather than reactively shrinking body via reserves. This would eliminate the multi-pass loop entirely. * fix(layout-bridge): drive footnote reserve from per-page demand (SD-1680) Cap footnote placement at each page's measured demand (capped at maxReserve) rather than the body's prior-pass reserve, and merge element-wise max across oscillating reserve vectors before the post-loop relayout. Ensures every page's reserve is large enough to hold its placed footnotes so fragments cannot render past the bottom margin, and iterates the post-loop relayout until the layout's reserve per page matches the final plan's demand.
* fix: allow putting cursor after inline sdt field * fix: simplify code; create helper file
|
Now I have what I need. Here's the review: Status: FAIL One spec compliance issue in
const evenOdd = elements.find((el) => el.name === 'w:evenAndOddHeaders');
return !!evenOdd;
Fix: check if (!evenOdd) return false;
const val = evenOdd.attributes?.['w:val'];
return val === undefined || val === '' || val === '1' || val === 'true' || val === 'on';Details: https://ooxml.dev/spec?q=evenAndOddHeaders Everything else looks clean:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b36ae6ab86
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
🎉 This PR is included in @superdoc-dev/react v1.2.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.28.0-next.1 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-cli v0.8.0-next.1 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.6.0 |
No description provided.