From f441a025a75cc4d5be00333eeb60e19dffaf586d Mon Sep 17 00:00:00 2001 From: Tomas Date: Tue, 16 Jun 2026 19:51:24 +0000 Subject: [PATCH 1/4] Fix nested block SDTs in layout adapter --- .../tests/src/sdt-metadata.test.ts | 49 +++++++++++++++++++ .../sdt/structured-content-block.ts | 4 ++ 2 files changed, 53 insertions(+) diff --git a/packages/layout-engine/tests/src/sdt-metadata.test.ts b/packages/layout-engine/tests/src/sdt-metadata.test.ts index d5213d5bef..5439417f93 100644 --- a/packages/layout-engine/tests/src/sdt-metadata.test.ts +++ b/packages/layout-engine/tests/src/sdt-metadata.test.ts @@ -127,6 +127,55 @@ describe('SDT metadata integration', () => { }); }); + it('preserves nested block structuredContent content and metadata', () => { + const nestedBlockDoc = { + type: 'doc', + content: [ + { + type: 'structuredContentBlock', + attrs: { + id: 'outer-block-sdt', + tag: 'outer_block', + alias: 'Outer Block', + }, + content: [ + { + type: 'paragraph', + content: [{ type: 'text', text: 'Outer paragraph' }], + }, + { + type: 'structuredContentBlock', + attrs: { + id: 'inner-block-sdt', + tag: 'inner_block', + alias: 'Inner Block', + }, + content: [ + { + type: 'paragraph', + content: [{ type: 'text', text: 'Inner paragraph' }], + }, + ], + }, + ], + }, + ], + }; + const { blocks: nestedBlocks } = toFlowBlocks(nestedBlockDoc); + const innerParagraph = nestedBlocks.find( + (block) => block.kind === 'paragraph' && block.runs?.some((run) => run.text === 'Inner paragraph'), + ); + + expect(innerParagraph).toBeDefined(); + expect(innerParagraph?.attrs?.sdt).toMatchObject({ + type: 'structuredContent', + scope: 'block', + id: 'inner-block-sdt', + tag: 'inner_block', + alias: 'Inner Block', + }); + }); + it('handles nested structuredContent (inline within inline)', () => { const nestedBlock = summary.find((b) => b.blockId === '2-paragraph'); const outerRun = nestedBlock?.runMetadata.find((r) => r.metadata?.id === 'nested-outer'); diff --git a/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/structured-content-block.ts b/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/structured-content-block.ts index 4fb2c5f7f7..68d1046c5f 100644 --- a/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/structured-content-block.ts +++ b/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/structured-content-block.ts @@ -232,6 +232,10 @@ export function handleStructuredContentBlockNode(node: PMNode, context: NodeHand // inside this content control is likewise transparent here; render its entry // paragraphs without advancing currentParagraphIndex, since // findParagraphsWithSectPr does not recurse structuredContentBlock. + if (child.type === 'structuredContentBlock') { + handleStructuredContentBlockNode(child, context); + return; + } if ( Array.isArray(child.content) && (child.type === 'documentPartObject' || From 63819e14ad1e6dab6bc886f15d88bac3556179fa Mon Sep 17 00:00:00 2001 From: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com> Date: Thu, 18 Jun 2026 09:45:07 -0300 Subject: [PATCH 2/4] fix(layout-adapter): record nested block content-control container chain Stamp an ordered outer-to-inner SDT container chain (attrs.sdtContainers) on every emitted block - paragraphs, tables, images, and drawings - so a nested block content control keeps its outer control's identity. attrs.sdt stays each block's nearest control. Data-model step of the nested-SDT fix; resolved layout and the painter consume the chain in follow-ups. Refs #3745 --- packages/layout-engine/contracts/src/index.ts | 11 ++++++ .../tests/src/sdt-metadata.test.ts | 36 +++++++++++++++++ .../v1/core/layout-adapter/sdt/metadata.ts | 35 +++++++++++++++++ .../sdt/structured-content-block.ts | 39 +++++++++++++++---- .../editors/v1/core/layout-adapter/types.ts | 13 ++++++- 5 files changed, 125 insertions(+), 9 deletions(-) diff --git a/packages/layout-engine/contracts/src/index.ts b/packages/layout-engine/contracts/src/index.ts index cdf1311eab..f56205c2a2 100644 --- a/packages/layout-engine/contracts/src/index.ts +++ b/packages/layout-engine/contracts/src/index.ts @@ -845,6 +845,8 @@ export type TableAttrs = { tableDirectionContext?: TableDirectionContext; sdt?: SdtMetadata; containerSdt?: SdtMetadata; + /** Ordered chain of enclosing block-container SDTs (outer to inner). See #3752. */ + sdtContainers?: SdtMetadata[]; [key: string]: unknown; }; @@ -937,6 +939,8 @@ export type DocumentBackground = { export type ImageBlockAttrs = { sdt?: SdtMetadata; containerSdt?: SdtMetadata; + /** Ordered chain of enclosing block-container SDTs (outer to inner). See #3752. */ + sdtContainers?: SdtMetadata[]; [key: string]: unknown; }; @@ -1823,6 +1827,13 @@ export type ParagraphAttrs = { sdt?: SdtMetadata; /** Container SDT for blocks with both primary and container metadata. */ containerSdt?: SdtMetadata; + /** + * Ordered chain of enclosing block-container SDTs (outermost first, innermost + * last) for nested content controls. `sdt` stays this block's nearest primary + * metadata; this records the full ancestry so resolved layout and the painter + * can group and draw nested control chrome. See #3752. + */ + sdtContainers?: SdtMetadata[]; }; export type ParagraphFrame = { diff --git a/packages/layout-engine/tests/src/sdt-metadata.test.ts b/packages/layout-engine/tests/src/sdt-metadata.test.ts index 5439417f93..8309698018 100644 --- a/packages/layout-engine/tests/src/sdt-metadata.test.ts +++ b/packages/layout-engine/tests/src/sdt-metadata.test.ts @@ -176,6 +176,42 @@ describe('SDT metadata integration', () => { }); }); + it('records the full outer-to-inner container chain on nested block SDTs', () => { + const nestedBlockDoc = { + type: 'doc', + content: [ + { + type: 'structuredContentBlock', + attrs: { id: 'outer-block-sdt', tag: 'outer_block', alias: 'Outer Block' }, + content: [ + { type: 'paragraph', content: [{ type: 'text', text: 'Outer paragraph' }] }, + { + type: 'structuredContentBlock', + attrs: { id: 'inner-block-sdt', tag: 'inner_block', alias: 'Inner Block' }, + content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Inner paragraph' }] }], + }, + ], + }, + ], + }; + const { blocks } = toFlowBlocks(nestedBlockDoc); + const outerParagraph = blocks.find( + (block) => block.kind === 'paragraph' && block.runs?.some((run) => run.text === 'Outer paragraph'), + ); + const innerParagraph = blocks.find( + (block) => block.kind === 'paragraph' && block.runs?.some((run) => run.text === 'Inner paragraph'), + ); + + // Outer block: outer identity is its nearest sdt and its sole container. + expect(outerParagraph?.attrs?.sdt).toMatchObject({ id: 'outer-block-sdt', scope: 'block' }); + expect(outerParagraph?.attrs?.sdtContainers?.map((s) => s.id)).toEqual(['outer-block-sdt']); + + // Inner block: inner identity stays the nearest sdt, but the container chain + // records the full outer-to-inner ancestry so the outer control is not lost. + expect(innerParagraph?.attrs?.sdt).toMatchObject({ id: 'inner-block-sdt', scope: 'block' }); + expect(innerParagraph?.attrs?.sdtContainers?.map((s) => s.id)).toEqual(['outer-block-sdt', 'inner-block-sdt']); + }); + it('handles nested structuredContent (inline within inline)', () => { const nestedBlock = summary.find((b) => b.blockId === '2-paragraph'); const outerRun = nestedBlock?.runMetadata.find((r) => r.metadata?.id === 'nested-outer'); diff --git a/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/metadata.ts b/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/metadata.ts index 322bad72e5..ca9f10074a 100644 --- a/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/metadata.ts +++ b/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/metadata.ts @@ -146,6 +146,41 @@ export function applySdtMetadataToTableBlock(tableBlock: FlowBlock | undefined, }); } +/** + * Record the ordered block-container chain (outermost first) on every emitted + * block, and fill in `attrs.sdt` for block kinds the metadata helpers skip + * (drawings, images) so a non-paragraph child of a content control no longer + * breaks that control's grouping run. Recurses table cells. + * + * @param blocks - Flow blocks to stamp + * @param nearest - This control's metadata (each block's nearest container) + * @param chain - Full container ancestry, outermost first + */ +export function applySdtContainerChain( + blocks: FlowBlock[], + nearest: SdtMetadata | undefined, + chain: readonly SdtMetadata[], +): void { + if (!nearest && chain.length === 0) return; + for (const block of blocks) applyChainToBlock(block, nearest, chain); +} + +function applyChainToBlock(block: FlowBlock, nearest: SdtMetadata | undefined, chain: readonly SdtMetadata[]): void { + const target = block as { kind: FlowBlock['kind']; attrs?: Record }; + if (!target.attrs) target.attrs = {}; + if (nearest && target.attrs.sdt == null) target.attrs.sdt = nearest; + if (chain.length > 0) target.attrs.sdtContainers = chain as SdtMetadata[]; + if (block.kind === 'table') { + const table = block as TableBlock; + table.rows?.forEach((row) => { + row.cells?.forEach((cell) => { + const cellBlocks = cell.blocks && cell.blocks.length > 0 ? cell.blocks : cell.paragraph ? [cell.paragraph] : []; + cellBlocks.forEach((cellBlock) => applyChainToBlock(cellBlock, nearest, chain)); + }); + }); + } +} + /** * Applies SDT metadata to all list items within a ListBlock. * diff --git a/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/structured-content-block.ts b/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/structured-content-block.ts index 68d1046c5f..210cb2d3d1 100644 --- a/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/structured-content-block.ts +++ b/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/structured-content-block.ts @@ -7,7 +7,12 @@ import type { FlowBlock, ParagraphBlock, TableBlock, TextRun } from '@superdoc/contracts'; import type { PMNode, NodeHandlerContext } from '../types.js'; -import { resolveNodeSdtMetadata, applySdtMetadataToParagraphBlocks, applySdtMetadataToTableBlock } from './metadata.js'; +import { + resolveNodeSdtMetadata, + applySdtMetadataToParagraphBlocks, + applySdtMetadataToTableBlock, + applySdtContainerChain, +} from './metadata.js'; const NON_RENDERED_STRUCTURAL_INLINE_TYPES = new Set([ 'bookmarkEnd', @@ -103,6 +108,15 @@ export function handleStructuredContentBlockNode(node: PMNode, context: NodeHand const structuredContentMetadata = resolveNodeSdtMetadata(node, 'structuredContentBlock'); const paragraphToFlowBlocks = converters?.paragraphToFlowBlocks; + // Ordered block-container ancestry for this node's content: the parent chain + // threaded via context plus this control (when it has resolvable metadata). + // `structuredContentMetadata` stays each block's nearest `sdt`; `chain` + // records the full outer-to-inner ancestry for nested content controls so the + // outer control's identity survives on inner blocks. + const parentChain = context.sdtContainerChain ?? []; + const chain = structuredContentMetadata ? [...parentChain, structuredContentMetadata] : parentChain; + const childContext: NodeHandlerContext = chain.length ? { ...context, sdtContainerChain: chain } : context; + const emitPlaceholderBlock = (contentPos?: number): void => { if (!structuredContentMetadata) return; const placeholderRun: TextRun = { @@ -118,7 +132,9 @@ export function handleStructuredContentBlockNode(node: PMNode, context: NodeHand kind: 'paragraph', id: nextBlockId('paragraph'), runs: [placeholderRun], - attrs: { sdt: structuredContentMetadata }, + attrs: chain.length + ? { sdt: structuredContentMetadata, sdtContainers: chain } + : { sdt: structuredContentMetadata }, }; blocks.push(placeholderBlock); recordBlockKind?.(placeholderBlock.kind); @@ -154,6 +170,7 @@ export function handleStructuredContentBlockNode(node: PMNode, context: NodeHand paragraphBlocks.filter((b) => b.kind === 'paragraph') as ParagraphBlock[], structuredContentMetadata, ); + applySdtContainerChain(paragraphBlocks, structuredContentMetadata, chain); if (applyPlaceholderToEmptyParagraphBlocks(paragraphBlocks, structuredContentMetadata, contentPos)) { paragraphBlocks.forEach((block) => { blocks.push(block); @@ -199,6 +216,7 @@ export function handleStructuredContentBlockNode(node: PMNode, context: NodeHand paragraphBlocks.filter((b) => b.kind === 'paragraph') as ParagraphBlock[], structuredContentMetadata, ); + applySdtContainerChain(paragraphBlocks, structuredContentMetadata, chain); paragraphBlocks.forEach((block) => { blocks.push(block); recordBlockKind?.(block.kind); @@ -221,21 +239,26 @@ export function handleStructuredContentBlockNode(node: PMNode, context: NodeHand }); if (tableBlock) { applySdtMetadataToTableBlock(tableBlock as TableBlock, structuredContentMetadata); + applySdtContainerChain([tableBlock], structuredContentMetadata, chain); blocks.push(tableBlock); recordBlockKind?.(tableBlock.kind); } } return; } - // SD-1333: documentPartObject is a transparent wrapper - recurse its content. - // SD-3005: a block field (bibliography / index / table of authorities) generated - // inside this content control is likewise transparent here; render its entry - // paragraphs without advancing currentParagraphIndex, since - // findParagraphsWithSectPr does not recurse structuredContentBlock. + // A nested block content control. Recurse with the extended container chain + // so its inner blocks carry the full outer-to-inner ancestry while this + // control's metadata stays their nearest `sdt`. findParagraphsWithSectPr + // does not recurse structuredContentBlock, so no currentParagraphIndex + // bookkeeping is needed here. if (child.type === 'structuredContentBlock') { - handleStructuredContentBlockNode(child, context); + handleStructuredContentBlockNode(child, childContext); return; } + // SD-1333: documentPartObject is a transparent wrapper - recurse its content. + // SD-3005: a block field (bibliography / index / table of authorities) generated + // inside this content control is likewise transparent here; render its entry + // paragraphs without advancing currentParagraphIndex. if ( Array.isArray(child.content) && (child.type === 'documentPartObject' || diff --git a/packages/super-editor/src/editors/v1/core/layout-adapter/types.ts b/packages/super-editor/src/editors/v1/core/layout-adapter/types.ts index 8bb95e3310..a84b3ec3f1 100644 --- a/packages/super-editor/src/editors/v1/core/layout-adapter/types.ts +++ b/packages/super-editor/src/editors/v1/core/layout-adapter/types.ts @@ -2,7 +2,13 @@ * Type definitions for ProseMirror to FlowBlock adapter */ -import type { TrackedChangesMode, SectionMetadata, FlowBlock, TrackedChangeMeta } from '@superdoc/contracts'; +import type { + TrackedChangesMode, + SectionMetadata, + FlowBlock, + TrackedChangeMeta, + SdtMetadata, +} from '@superdoc/contracts'; import type { StyleContext as StyleEngineContext, ComputedParagraphStyle } from '@superdoc/style-engine'; import type { SectionRange } from './sections/index.js'; import type { ConverterContext } from './converter-context.js'; @@ -336,6 +342,11 @@ export interface NodeHandlerContext { // Converters for nested content converters: NestedConverters; + // Ordered chain of enclosing block-container SDT metadata (outermost first) + // for the node currently being processed. Threaded by the SDT handlers so + // nested content controls record their full container ancestry on emitted + // blocks. Absent/empty at the top level. + sdtContainerChain?: SdtMetadata[]; themeColors?: ThemeColorPalette; // FlowBlock cache for incremental conversion (optional) flowBlockCache?: import('./cache.js').FlowBlockCache; From 2cd03d3eba0f700561cd1134a665fa26263a0e84 Mon Sep 17 00:00:00 2001 From: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com> Date: Thu, 18 Jun 2026 10:32:31 -0300 Subject: [PATCH 3/4] docs(layout-adapter): scope applySdtContainerChain comment to data, not grouping --- .../src/editors/v1/core/layout-adapter/sdt/metadata.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/metadata.ts b/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/metadata.ts index ca9f10074a..fa8c495637 100644 --- a/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/metadata.ts +++ b/packages/super-editor/src/editors/v1/core/layout-adapter/sdt/metadata.ts @@ -149,8 +149,10 @@ export function applySdtMetadataToTableBlock(tableBlock: FlowBlock | undefined, /** * Record the ordered block-container chain (outermost first) on every emitted * block, and fill in `attrs.sdt` for block kinds the metadata helpers skip - * (drawings, images) so a non-paragraph child of a content control no longer - * breaks that control's grouping run. Recurses table cells. + * (drawings, images) so every block in a control carries that control's + * identity and chain. (Resolved layout ignores image/drawing keys today; once + * it consumes them in a follow-up, this is what keeps a drawing or image from + * splitting the control's grouping run.) Recurses table cells. * * @param blocks - Flow blocks to stamp * @param nearest - This control's metadata (each block's nearest container) From bec8863d8c12f5e9b4a5172580c37b51908bcdf4 Mon Sep 17 00:00:00 2001 From: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:05:36 -0300 Subject: [PATCH 4/4] chore: format baseline files for CI --- .../layout-bridge/src/incrementalLayout.ts | 3 +-- .../test/footnoteDensePageRender.test.ts | 5 ++++- .../layout-bridge/test/footnoteWarmStart.test.ts | 6 ++++-- packages/layout-engine/painters/dom/src/renderer.ts | 5 ++++- .../v1/core/commands/selectFootnoteMarkerBefore.js | 3 +-- .../notes/NoteSessionCoordinator.ts | 1 - .../pointer-events/note-reference-hit.test.ts | 5 ++++- .../selection/VisibleTextOffsetGeometry.ts | 12 ++++++++---- .../tests/EditorInputManager.footnoteClick.test.ts | 12 +++++++++--- .../story-runtime/note-story-runtime.test.ts | 7 ++++++- .../tracked-changes/enumerate-stories.test.ts | 5 ++++- .../tracked-changes/enumerate-stories.ts | 6 +++++- .../extensions/collaboration/note-tombstone-sync.ts | 10 ++-------- .../collaboration/part-sync/seed-parts.test.ts | 9 ++++++--- .../extensions/collaboration/part-sync/seed-parts.ts | 8 +++++--- .../vertical-navigation/vertical-navigation.js | 4 +++- .../tests/import-export/footnotes-roundtrip.test.js | 11 ++--------- 17 files changed, 68 insertions(+), 44 deletions(-) diff --git a/packages/layout-engine/layout-bridge/src/incrementalLayout.ts b/packages/layout-engine/layout-bridge/src/incrementalLayout.ts index 3c8615aa79..de2b58e985 100644 --- a/packages/layout-engine/layout-bridge/src/incrementalLayout.ts +++ b/packages/layout-engine/layout-bridge/src/incrementalLayout.ts @@ -999,8 +999,7 @@ function computeNoteBodyHeights( if (measure.kind === 'paragraph') { const measureH = (measure as { totalHeight?: number }).totalHeight; if (typeof measureH === 'number' && Number.isFinite(measureH)) total += measureH; - const spacing = (block as { attrs?: { spacing?: { after?: number; lineSpaceAfter?: number } } }).attrs - ?.spacing; + const spacing = (block as { attrs?: { spacing?: { after?: number; lineSpaceAfter?: number } } }).attrs?.spacing; const after = spacing?.after ?? spacing?.lineSpaceAfter; if (typeof after === 'number' && Number.isFinite(after) && after > 0) total += after; // SD-2656: first paragraph's first line is the first valid run. diff --git a/packages/layout-engine/layout-bridge/test/footnoteDensePageRender.test.ts b/packages/layout-engine/layout-bridge/test/footnoteDensePageRender.test.ts index 3533734979..34460e0909 100644 --- a/packages/layout-engine/layout-bridge/test/footnoteDensePageRender.test.ts +++ b/packages/layout-engine/layout-bridge/test/footnoteDensePageRender.test.ts @@ -43,7 +43,10 @@ const makeMeasure = (lineHeight: number, lineCount: number): Measure => ({ totalHeight: lineCount * lineHeight, }); -const countFootnoteFragments = (layout: { pages: Array<{ fragments: Array<{ blockId?: string }> }> }, idPrefix: string) => { +const countFootnoteFragments = ( + layout: { pages: Array<{ fragments: Array<{ blockId?: string }> }> }, + idPrefix: string, +) => { let count = 0; for (const page of layout.pages) { for (const f of page.fragments) { diff --git a/packages/layout-engine/layout-bridge/test/footnoteWarmStart.test.ts b/packages/layout-engine/layout-bridge/test/footnoteWarmStart.test.ts index 25952e9e75..608fb8cbb8 100644 --- a/packages/layout-engine/layout-bridge/test/footnoteWarmStart.test.ts +++ b/packages/layout-engine/layout-bridge/test/footnoteWarmStart.test.ts @@ -225,8 +225,10 @@ describe('footnote convergence warm-start (SD-3432)', () => { let settled = false; for (let i = 0; i < 3; i += 1) { const next = await run(bodyBlocks, options, measureBlock, prev.footnoteReserveSeed ?? null); - if (JSON.stringify(layoutJson(next)) === JSON.stringify(layoutJson(prev)) && - JSON.stringify(next.footnoteReserveSeed) === JSON.stringify(prev.footnoteReserveSeed)) { + if ( + JSON.stringify(layoutJson(next)) === JSON.stringify(layoutJson(prev)) && + JSON.stringify(next.footnoteReserveSeed) === JSON.stringify(prev.footnoteReserveSeed) + ) { settled = true; prev = next; break; diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 182251db31..caf7f2dbc9 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -2592,7 +2592,10 @@ export class DomPainter { } if (freshStart == null || !Number.isFinite(freshStart)) return; - const elements = [fragmentEl, ...Array.from(fragmentEl.querySelectorAll('[data-pm-start], [data-pm-end]'))]; + const elements = [ + fragmentEl, + ...Array.from(fragmentEl.querySelectorAll('[data-pm-start], [data-pm-end]')), + ]; let paintedStart = Infinity; for (const el of elements) { const start = Number(el.dataset.pmStart); diff --git a/packages/super-editor/src/editors/v1/core/commands/selectFootnoteMarkerBefore.js b/packages/super-editor/src/editors/v1/core/commands/selectFootnoteMarkerBefore.js index 4eb6c67d71..c8ca1f3b9b 100644 --- a/packages/super-editor/src/editors/v1/core/commands/selectFootnoteMarkerBefore.js +++ b/packages/super-editor/src/editors/v1/core/commands/selectFootnoteMarkerBefore.js @@ -2,8 +2,7 @@ import { TextSelection } from 'prosemirror-state'; export const SELECT_FOOTNOTE_MARKER_META = 'selectFootnoteMarker'; -const isNoteReference = (node) => - node?.type.name === 'footnoteReference' || node?.type.name === 'endnoteReference'; +const isNoteReference = (node) => node?.type.name === 'footnoteReference' || node?.type.name === 'endnoteReference'; /** * Resolves the note marker ending at `boundaryPos` (the position right after it). diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/notes/NoteSessionCoordinator.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/notes/NoteSessionCoordinator.ts index d2082b4f4f..bbc3232c36 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/notes/NoteSessionCoordinator.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/notes/NoteSessionCoordinator.ts @@ -124,5 +124,4 @@ export class NoteSessionCoordinator { if (fullyVisible) return; fragment.scrollIntoView({ block: 'center', behavior: 'smooth' }); } - } diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/note-reference-hit.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/note-reference-hit.test.ts index 065f8a8d8c..8176035916 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/note-reference-hit.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/note-reference-hit.test.ts @@ -56,7 +56,10 @@ const schema = new Schema({ * p1: "See" + bookmarkStart(_Ref1)[ run[ ] ] + "below" * p2: "as noted in" + crossReference(target=_Ref1, "footnote 8") */ -function makeDoc(noteRefType: 'footnoteReference' | 'endnoteReference' = 'footnoteReference', bookmarkContent?: ProseMirrorNode[]) { +function makeDoc( + noteRefType: 'footnoteReference' | 'endnoteReference' = 'footnoteReference', + bookmarkContent?: ProseMirrorNode[], +) { const noteRef = schema.nodes[noteRefType].create({ id: '8' }); const bookmark = schema.nodes.bookmarkStart.create( { name: '_Ref1', id: '1' }, diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/selection/VisibleTextOffsetGeometry.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/selection/VisibleTextOffsetGeometry.ts index e5abe5b87d..32783ce205 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/selection/VisibleTextOffsetGeometry.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/selection/VisibleTextOffsetGeometry.ts @@ -334,8 +334,10 @@ function resolvePmPoint( if (blockContainers.length) { const blockLines = collectRenderedLineElements(blockContainers) .map((line) => ({ line, pmStart: getPmStart(line), pmEnd: getPmEnd(line) })) - .filter((entry): entry is { line: HTMLElement; pmStart: number; pmEnd: number } => - entry.pmStart != null && entry.pmEnd != null) + .filter( + (entry): entry is { line: HTMLElement; pmStart: number; pmEnd: number } => + entry.pmStart != null && entry.pmEnd != null, + ) .sort((a, b) => a.pmStart - b.pmStart || a.pmEnd - b.pmEnd); if (blockLines.length) { const delta = anchor.currentStart - blockLines[0].pmStart; @@ -357,8 +359,10 @@ function resolvePmPoint( // forward-affinity scan and the gap snap pick the right line (SD-3400). const lines = collectRenderedLineElements(containers) .map((line) => ({ line, pmStart: getPmStart(line), pmEnd: getPmEnd(line) })) - .filter((entry): entry is { line: HTMLElement; pmStart: number; pmEnd: number } => - entry.pmStart != null && entry.pmEnd != null) + .filter( + (entry): entry is { line: HTMLElement; pmStart: number; pmEnd: number } => + entry.pmStart != null && entry.pmEnd != null, + ) .sort((a, b) => a.pmStart - b.pmStart || a.pmEnd - b.pmEnd); let lineElement: HTMLElement | null = null; let resolvedPos = pos; diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/EditorInputManager.footnoteClick.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/EditorInputManager.footnoteClick.test.ts index 3cb9de0d5d..2faaff9d5a 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/EditorInputManager.footnoteClick.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/EditorInputManager.footnoteClick.test.ts @@ -458,7 +458,9 @@ describe('EditorInputManager - Footnote click selection behavior', () => { pos === 38 ? { type: { name: 'footnoteReference' }, attrs: { id: '3' } } : null; const refEl = makeRefSpan(38, '3'); - refEl.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true, button: 0, clientX: 5, clientY: 5 })); + refEl.dispatchEvent( + new MouseEvent('dblclick', { bubbles: true, cancelable: true, button: 0, clientX: 5, clientY: 5 }), + ); expect(activateRenderedNoteSession).toHaveBeenCalledWith( { storyType: 'footnote', noteId: '3' }, @@ -471,7 +473,9 @@ describe('EditorInputManager - Footnote click selection behavior', () => { pos === 50 ? { type: { name: 'endnoteReference' }, attrs: { id: '2' } } : null; const refEl = makeRefSpan(50, 'ii'); - refEl.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true, button: 0, clientX: 7, clientY: 9 })); + refEl.dispatchEvent( + new MouseEvent('dblclick', { bubbles: true, cancelable: true, button: 0, clientX: 7, clientY: 9 }), + ); expect(activateRenderedNoteSession).toHaveBeenCalledWith( { storyType: 'endnote', noteId: '2' }, @@ -519,7 +523,9 @@ describe('EditorInputManager - Footnote click selection behavior', () => { pos === 12 ? { type: { name: 'text' }, attrs: {} } : null; const refEl = makeRefSpan(12, 'word'); - refEl.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true, button: 0, clientX: 5, clientY: 5 })); + refEl.dispatchEvent( + new MouseEvent('dblclick', { bubbles: true, cancelable: true, button: 0, clientX: 5, clientY: 5 }), + ); expect(activateRenderedNoteSession).not.toHaveBeenCalled(); }); diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/story-runtime/note-story-runtime.test.ts b/packages/super-editor/src/editors/v1/document-api-adapters/story-runtime/note-story-runtime.test.ts index d47e2765e2..9e9d7c3e96 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/story-runtime/note-story-runtime.test.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/story-runtime/note-story-runtime.test.ts @@ -323,7 +323,12 @@ describe('SD-3400: clearing a note in the area removes the footnote on both side const makeHost = () => ({ converter: { footnotes: [{ id: '1', content: [{ type: 'paragraph' }] }], endnotes: [] }, - state: { doc: { descendants: (cb: (n: unknown, p: number) => void) => cb({ type: { name: 'footnoteReference' }, attrs: { id: '1' } }, 5) } }, + state: { + doc: { + descendants: (cb: (n: unknown, p: number) => void) => + cb({ type: { name: 'footnoteReference' }, attrs: { id: '1' } }, 5), + }, + }, on: vi.fn(), }) as any; diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/tracked-changes/enumerate-stories.test.ts b/packages/super-editor/src/editors/v1/document-api-adapters/tracked-changes/enumerate-stories.test.ts index 9fe188c5ff..5f9e88afab 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/tracked-changes/enumerate-stories.test.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/tracked-changes/enumerate-stories.test.ts @@ -2,7 +2,10 @@ import { describe, it, expect } from 'vitest'; import type { Editor } from '../../core/Editor.js'; import { enumerateRevisionCapableStories } from './enumerate-stories.js'; -function makeEditor(converter?: Record, refIds: { footnotes?: string[]; endnotes?: string[] } = {}): Editor { +function makeEditor( + converter?: Record, + refIds: { footnotes?: string[]; endnotes?: string[] } = {}, +): Editor { const doc = { descendants: (cb: (node: unknown) => boolean | void) => { (refIds.footnotes ?? []).forEach((id) => cb({ type: { name: 'footnoteReference' }, attrs: { id } })); diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/tracked-changes/enumerate-stories.ts b/packages/super-editor/src/editors/v1/document-api-adapters/tracked-changes/enumerate-stories.ts index 9f0f147141..89acf07194 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/tracked-changes/enumerate-stories.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/tracked-changes/enumerate-stories.ts @@ -97,7 +97,11 @@ export function enumerateRevisionCapableStories(editor: Editor): StoryLocator[] * surface as revision-capable stories. Pre-existing orphan notes are never * registered, so their enumeration behavior is unchanged. */ -function collectTombstonedNoteIds(editor: Editor, converter: ConverterShape, type: 'footnote' | 'endnote'): Set { +function collectTombstonedNoteIds( + editor: Editor, + converter: ConverterShape, + type: 'footnote' | 'endnote', +): Set { const registry = converter.sessionManagedNoteIds?.[type === 'endnote' ? 'endnotes' : 'footnotes']; if (!registry || registry.size === 0) return new Set(); diff --git a/packages/super-editor/src/editors/v1/extensions/collaboration/note-tombstone-sync.ts b/packages/super-editor/src/editors/v1/extensions/collaboration/note-tombstone-sync.ts index b9e482287b..49b6737788 100644 --- a/packages/super-editor/src/editors/v1/extensions/collaboration/note-tombstone-sync.ts +++ b/packages/super-editor/src/editors/v1/extensions/collaboration/note-tombstone-sync.ts @@ -31,10 +31,7 @@ import { onCollaborationProviderSynced, } from '../../core/helpers/collaboration-provider-sync.js'; import type { CollaborationProvider } from '../../core/types/EditorConfig.js'; -import { - NOTE_TOMBSTONE_EVENT, - type SessionManagedNoteIds, -} from '../../core/parts/adapters/notes-part-descriptor.js'; +import { NOTE_TOMBSTONE_EVENT, type SessionManagedNoteIds } from '../../core/parts/adapters/notes-part-descriptor.js'; export { NOTE_TOMBSTONE_EVENT }; @@ -104,10 +101,7 @@ export function clearNoteTombstonesFromMeta(ydoc: Y.Doc): void { } /** Publish every locally tracked session-managed note id into the shared meta map. */ -export function publishSessionManagedNoteIds( - ydoc: Y.Doc, - registry: SessionManagedNoteIds | null | undefined, -): void { +export function publishSessionManagedNoteIds(ydoc: Y.Doc, registry: SessionManagedNoteIds | null | undefined): void { if (!registry) return; for (const noteId of registry.footnotes) { publishNoteTombstone(ydoc, 'footnote', noteId); diff --git a/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/seed-parts.test.ts b/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/seed-parts.test.ts index cf031ee5f6..3f20d17122 100644 --- a/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/seed-parts.test.ts +++ b/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/seed-parts.test.ts @@ -110,9 +110,12 @@ describe('seedPartsFromEditor', () => { metaMap.set('noteTombstone:footnote:7', true); metaMap.set('bodySectPr', { pgSz: { w: 12240, h: 15840 } }); - const editor = createMockEditor({ - 'word/styles.xml': { type: 'element', name: 'fresh' }, - }, { footnotes: new Set(['9']), endnotes: new Set(['3']) }); + const editor = createMockEditor( + { + 'word/styles.xml': { type: 'element', name: 'fresh' }, + }, + { footnotes: new Set(['9']), endnotes: new Set(['3']) }, + ); seedPartsFromEditor(editor, ydoc, { replaceExisting: true }); diff --git a/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/seed-parts.ts b/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/seed-parts.ts index e88873ffec..df20abd82c 100644 --- a/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/seed-parts.ts +++ b/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/seed-parts.ts @@ -40,9 +40,11 @@ export function seedPartsFromEditor(editor: Editor, ydoc: Y.Doc, options?: SeedO const convertedXml = (editor as unknown as { converter?: { convertedXml?: Record } }).converter ?.convertedXml; if (!convertedXml) return; - const sessionManagedNoteIds = (editor as unknown as { - converter?: { sessionManagedNoteIds?: SessionManagedNoteIds }; - }).converter?.sessionManagedNoteIds; + const sessionManagedNoteIds = ( + editor as unknown as { + converter?: { sessionManagedNoteIds?: SessionManagedNoteIds }; + } + ).converter?.sessionManagedNoteIds; const partsMap = ydoc.getMap(PARTS_MAP_KEY) as Y.Map; const metaMap = ydoc.getMap(META_MAP_KEY); diff --git a/packages/super-editor/src/editors/v1/extensions/vertical-navigation/vertical-navigation.js b/packages/super-editor/src/editors/v1/extensions/vertical-navigation/vertical-navigation.js index 3606d4afb6..16da685a12 100644 --- a/packages/super-editor/src/editors/v1/extensions/vertical-navigation/vertical-navigation.js +++ b/packages/super-editor/src/editors/v1/extensions/vertical-navigation/vertical-navigation.js @@ -130,7 +130,9 @@ export const VerticalNavigation = Extension.create({ goalX = coords?.x; goalClientX = coords?.clientX; if (!Number.isFinite(goalX) || !Number.isFinite(goalClientX)) return false; - view.dispatch(view.state.tr.setMeta(VerticalNavigationPluginKey, { type: 'set-goal-x', goalX, goalClientX })); + view.dispatch( + view.state.tr.setMeta(VerticalNavigationPluginKey, { type: 'set-goal-x', goalX, goalClientX }), + ); } // 2. Find adjacent line diff --git a/packages/super-editor/src/editors/v1/tests/import-export/footnotes-roundtrip.test.js b/packages/super-editor/src/editors/v1/tests/import-export/footnotes-roundtrip.test.js index 05efebb43b..338c24ed32 100644 --- a/packages/super-editor/src/editors/v1/tests/import-export/footnotes-roundtrip.test.js +++ b/packages/super-editor/src/editors/v1/tests/import-export/footnotes-roundtrip.test.js @@ -200,9 +200,7 @@ describe('footnotes import/export roundtrip', () => { const originalZipper = new DocxZipper(); const originalFiles = await originalZipper.getDocxData(docxBuffer, true); - const originalFootnotesJson = parseXmlToJson( - originalFiles.find((f) => f.name === 'word/footnotes.xml').content, - ); + const originalFootnotesJson = parseXmlToJson(originalFiles.find((f) => f.name === 'word/footnotes.xml').content); const originalRoot = findFootnotesRoot(originalFootnotesJson); const regularIds = collectFootnoteIds(originalRoot).filter((id) => { const type = findFootnoteById(originalRoot, id)?.attributes?.['w:type']; @@ -691,11 +689,7 @@ describe('footnotesExporter unit tests', () => { }); it('never prunes separator/continuationSeparator entries even when registered', () => { - const part = makePart([ - { id: '-1', type: 'separator' }, - { id: '0', type: 'continuationSeparator' }, - { id: '1' }, - ]); + const part = makePart([{ id: '-1', type: 'separator' }, { id: '0', type: 'continuationSeparator' }, { id: '1' }]); const pruned = pruneSessionDeletedNotesPart(part, { converter: makeConverter(['-1', '0', '1']), documentXml: makeDocumentXml([]), @@ -752,7 +746,6 @@ describe('footnotesExporter unit tests', () => { expect(prunedIds(pruned, 'w:endnote')).toEqual(['2']); }); - }); });