Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
119 commits
Select commit Hold shift + click to select a range
dd41af0
refactor(painters/dom): drop list-item fragment renderer
luccas-harbour May 13, 2026
d010861
refactor(painters/dom): unify paragraph rendering across body and tab…
luccas-harbour May 13, 2026
5a0af37
fix(painters/dom): keep split paragraph fragment height
luccas-harbour May 13, 2026
10ec04c
fix(painters/dom): mark remeasured paragraph final lines
luccas-harbour May 13, 2026
9003049
fix(painters/dom): keep list marker right indent
luccas-harbour May 13, 2026
7ce35b5
refactor(painters/dom): reuse shared list marker types
luccas-harbour May 13, 2026
9c641de
test(painters/dom): cover resolved paragraph rendering
luccas-harbour May 13, 2026
4466493
refactor(painters/dom): colocate paragraph borders and marker helpers
luccas-harbour May 14, 2026
fd8b87e
refactor(painters/dom): extract run rendering into runs/ module
luccas-harbour May 14, 2026
46efc9d
refactor(painters/dom): extract paragraph fragment rendering into par…
luccas-harbour May 15, 2026
4417aed
test(painters/dom): drop unused contextSection from paragraph render …
luccas-harbour May 21, 2026
5e1e81b
fix(painter-dom): break list marker renderer cycle
luccas-harbour May 21, 2026
0a463ac
refactor(painters/dom): consolidate SDT container helpers into sdt/ m…
luccas-harbour May 14, 2026
3284962
fix(painters/dom): continue nested table SDT chrome
luccas-harbour May 14, 2026
85e77a2
fix(painters/dom): suppress idless table SDT chrome
luccas-harbour May 14, 2026
c1d92bd
fix(painters/dom): inherit table container SDT chrome
luccas-harbour May 14, 2026
78f8f04
fix(painters/dom): keep nested SDT chrome active
luccas-harbour May 14, 2026
b1ace96
fix(painters/dom): suppress nested table SDT chrome
luccas-harbour May 14, 2026
ad6d6cc
fix(painters/dom): continue partial nested SDT chrome
luccas-harbour May 14, 2026
2bd9f2a
fix(painters/dom): preserve nested table SDT ancestor
luccas-harbour May 14, 2026
ae03e40
fix(painters/dom): allow nested SDT label overflow
luccas-harbour May 14, 2026
685ceb4
fix(painters/dom): scope nested SDT overflow to rendered content
luccas-harbour May 14, 2026
e44f2ec
fix(painters/dom): use rendered SDT lock mode
luccas-harbour May 15, 2026
944f988
fix(painters/dom): preserve ancestor SDT key
luccas-harbour May 15, 2026
28c5950
fix(painters/dom): merge idless SDT siblings
luccas-harbour May 15, 2026
308a3ed
test(painters/dom): cover SDT chrome gaps
luccas-harbour May 15, 2026
8e1627a
refactor(painters/dom): share SDT container keys
luccas-harbour May 15, 2026
913f86f
fix(painters/dom): drop unused table block local
luccas-harbour May 15, 2026
160108b
fix(painters/dom): skip media for SDT boundaries
luccas-harbour May 15, 2026
5c6753a
fix(painters/dom): preserve SDT ancestor chain
luccas-harbour May 15, 2026
cdad4c1
fix(painters/dom): defer SDT overflow to rendered chrome
luccas-harbour May 15, 2026
8f2b80c
fix(painters/dom): continue split SDT paragraph chrome
luccas-harbour May 15, 2026
738fcd3
refactor(painters/dom): extract SDT helpers from renderer
luccas-harbour May 15, 2026
01c2e2f
docs(layout-engine): flag SdtMetadata identity invariant for pm-adapter
luccas-harbour May 21, 2026
22ce0ff
refactor(painters/dom): unify image block rendering
luccas-harbour May 15, 2026
8a2059c
fix(painters/dom): invalidate table image visual edits
luccas-harbour May 15, 2026
112234f
refactor(painters/dom): drop unused block version derivation
luccas-harbour May 15, 2026
6871e02
fix(layout-resolved): invalidate inline image visual edits
luccas-harbour May 15, 2026
b0be9c8
fix(layout-resolved): hash raw inline image clip paths
luccas-harbour May 15, 2026
b512cb9
refactor(painters/dom): extract image rendering helpers into modules
luccas-harbour May 15, 2026
815af26
fix(layout-resolved): avoid image hyperlink hash collisions
luccas-harbour May 15, 2026
f823bb2
fix(painters/dom): keep table images block display
luccas-harbour May 15, 2026
e586394
test(painters/dom): cover unified drawing image rendering
luccas-harbour May 15, 2026
9491a08
refactor(painters/dom): share image clip path reader
luccas-harbour May 15, 2026
9d2871f
refactor(painters/dom): share inline style applier
luccas-harbour May 15, 2026
6e71691
docs(painters/dom): preserve image transform rationale
luccas-harbour May 15, 2026
6100449
refactor(painters/dom): share image hyperlink anchor type
luccas-harbour May 15, 2026
77c811e
refactor(painters/dom): co-locate image utils under images/
luccas-harbour May 15, 2026
dabfe32
docs(layout-resolved): clarify deriveBlockVersion no longer duplicated
luccas-harbour May 21, 2026
aebdba1
refactor(painters/dom): unify drawing block rendering across renderer…
luccas-harbour May 15, 2026
5d23028
fix(painters/dom): render table drawing images without callback
luccas-harbour May 15, 2026
fd56fce
fix(painters/dom): bypass drawing callback for table images
luccas-harbour May 15, 2026
ee7dfd5
fix(painters/dom): guard table drawing image sources
luccas-harbour May 15, 2026
4b16187
refactor(painters/dom): flatten drawing content renderer
luccas-harbour May 15, 2026
babe53b
refactor(painters/dom): route table drawings through callback
luccas-harbour May 15, 2026
efc47bb
refactor(painters/dom): extract drawing fragment renderer
luccas-harbour May 15, 2026
7c46785
refactor(painters/dom): scope table drawing callback to non-image blocks
luccas-harbour May 21, 2026
78eaf0e
refactor(layout-engine): unify nested table cell-slice helpers via co…
luccas-harbour May 18, 2026
0686e18
refactor(painters/dom): extract table fragment rendering from DomPainter
luccas-harbour May 18, 2026
8041f64
fix(layout-engine): price embedded table slice height
luccas-harbour May 18, 2026
efa538f
fix(layout-engine): price partial embedded table slices
luccas-harbour May 18, 2026
4d8e84d
refactor(layout-engine): share table fragment height math
luccas-harbour May 18, 2026
707a9df
refactor(layout-engine): remove table slice shim
luccas-harbour May 18, 2026
9c6b237
docs(contracts): restore table slice height rationale
luccas-harbour May 18, 2026
d0fcec3
test(painter-dom): cover embedded row slice mapping
luccas-harbour May 18, 2026
1293d98
docs(painter-dom): note embedded partial row limit
luccas-harbour May 18, 2026
63ce6e7
docs(contracts): mark table traversal helpers internal
luccas-harbour May 18, 2026
ac7787f
refactor(painter-dom): import table renderer helpers
luccas-harbour May 18, 2026
2ad8ebe
fix(contracts): include cell padding in embedded partial row slice he…
luccas-harbour May 21, 2026
bc9de03
refactor(painter-dom): extract note story block id helpers
luccas-harbour May 18, 2026
08faf46
refactor(painter-dom): extract note frame attribute helper
luccas-harbour May 18, 2026
dee3130
refactor(painter-dom): clarify footnote read-only helper
luccas-harbour May 18, 2026
a1aafa2
refactor(super-editor): share note story block detection
luccas-harbour May 18, 2026
a24ebc9
test(painter-dom): clarify note frame cases
luccas-harbour May 18, 2026
e842176
refactor(dom-contract): share note story block helpers
luccas-harbour May 21, 2026
2e24034
refactor(painter-dom): unify textbox text rendering across shapes
luccas-harbour May 18, 2026
88eec40
refactor(painter-dom): move WordArt watermark detector to textbox module
luccas-harbour May 18, 2026
54cb8e9
fix(painter-dom): always use shared image renderer for table drawings
luccas-harbour May 19, 2026
520a1a8
test(painter-dom): cover anchored table drawing images with callbacks
luccas-harbour May 21, 2026
ab66e7d
refactor(painter-dom): share paragraph border grouping context
luccas-harbour May 18, 2026
26ccebe
test(painter-dom): cover table textbox page fields through callback path
luccas-harbour May 21, 2026
6340cb8
refactor(painter-dom): share marker line painting
luccas-harbour May 18, 2026
ef8b045
refactor(painter-dom): share drawing frame rendering
luccas-harbour May 18, 2026
cb6ce7c
refactor(painter-dom): isolate table cell paragraph and image frames
luccas-harbour May 18, 2026
4c8e25a
fix(painter-dom): clamp anchored table drawings
luccas-harbour May 18, 2026
ff4e1b8
fix(painter-dom): keep anchored table images passive
luccas-harbour May 18, 2026
e778697
fix(painter-dom): ignore anchored media in border grouping
luccas-harbour May 18, 2026
76c9ac4
fix(painter-dom): flag split table cell paragraphs
luccas-harbour May 18, 2026
8334224
refactor(painter-dom): drop unused drawing body frame
luccas-harbour May 18, 2026
df59db5
perf(painter-dom): defer table cell border hashing
luccas-harbour May 18, 2026
1288ea3
test(painter-dom): stabilize table border selectors
luccas-harbour May 18, 2026
4de0731
docs(painter-dom): document renderer organization rules
luccas-harbour May 19, 2026
3d31ca7
fix(painter-dom): render separate table borders without spacing
luccas-harbour May 20, 2026
a06071f
fix(painter-dom): skip zero-height table media segments
luccas-harbour May 20, 2026
619c7e2
fix(painter-dom): rebuild stale sdt boundary chrome
luccas-harbour May 20, 2026
7d88d59
fix(painter-dom): ignore zero-height media for cell spacing
luccas-harbour May 20, 2026
78d5094
fix(contracts): type visible table cell blocks
luccas-harbour May 20, 2026
33ac88f
fix(painter-dom): ignore anchored media for cell spacing
luccas-harbour May 20, 2026
53aba38
fix(painter-dom): preserve embedded table row slices
luccas-harbour May 20, 2026
b741433
fix(painter-dom): coalesce full embedded table rows
luccas-harbour May 20, 2026
9cd977d
fix(painter-dom): suppress split embedded table chrome
luccas-harbour May 20, 2026
1617630
fix(painter-dom): keep zero-height media in border groups
luccas-harbour May 20, 2026
2bb6354
refactor(painter-dom): reuse shared style helper
luccas-harbour May 20, 2026
08a87c0
refactor(painter-dom): move fragment context type
luccas-harbour May 20, 2026
691129e
refactor(painter-dom): fold sdt ancestor state
luccas-harbour May 20, 2026
3ed4c43
refactor(painter-dom): import sdt dataset helpers
luccas-harbour May 20, 2026
59bc5d5
refactor(painter-dom): inline pure render helpers
luccas-harbour May 20, 2026
eb61b03
fix(painter-dom): preserve cell spacing between adjacent partial embe…
luccas-harbour May 20, 2026
8ee7c08
fix(footnotes): render list markers in footnote paragraphs
luccas-harbour May 21, 2026
22ad71f
fix(painter-dom): keep hidden cell paragraphs out of border grouping
luccas-harbour May 21, 2026
7617b48
fix(painter-dom): extend table-cell between-borders through paragraph…
luccas-harbour May 21, 2026
4263a9d
refactor(pm-adapter): resolve list rendering inside adapter via opt-i…
luccas-harbour May 21, 2026
ac70cb1
fix: import issues
luccas-harbour May 21, 2026
c1f02a6
fix: failing table cell slice unit test
luccas-harbour May 22, 2026
adf7e51
chore: fix broken paths in AGENTS.md
luccas-harbour May 22, 2026
c9894c4
Merge pull request #3413 from superdoc-dev/luccas/sd-2838-final-fixes
harbournick May 26, 2026
89f365c
Merge pull request #3401 from superdoc-dev/luccas/sd-2838-unify-textb…
harbournick May 26, 2026
f6e7419
Merge pull request #3383 from superdoc-dev/luccas/sd-2838-unify-footn…
harbournick May 26, 2026
7b63cb2
Merge pull request #3367 from superdoc-dev/luccas/sd-2838-unify-neste…
harbournick May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 25 additions & 14 deletions packages/layout-engine/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ ProseMirror Doc → pm-adapter → FlowBlock[] → layout-engine → Layout[]

| Package | Purpose | Key Entry |
|---------|---------|-----------|
| `contracts/` | Shared types (FlowBlock, Layout, etc.) | `src/index.ts` |
| `pm-adapter/` | PM document → FlowBlocks conversion | `src/internal.ts` |
| `layout-engine/` | Pagination algorithms | `src/index.ts` |
| `layout-bridge/` | Layout orchestration & bridge utilities | `src/incrementalLayout.ts` |
| `painters/dom/` | DOM rendering | `src/renderer.ts` |
| `style-engine/` | OOXML style resolution | `src/index.ts` |
| `geometry-utils/` | Math utilities for layout | `src/index.ts` |
| `contracts/` | Shared types (FlowBlock, Layout, etc.) | `contracts/src/index.ts` |
| `pm-adapter/` | PM document → FlowBlocks conversion | `pm-adapter/src/internal.ts` |
| `layout-engine/` | Pagination algorithms | `layout-engine/src/index.ts` |
| `layout-bridge/` | Layout orchestration & bridge utilities | `layout-bridge/src/incrementalLayout.ts` |
| `painters/dom/` | DOM rendering | `painters/dom/AGENTS.md`, `painters/dom/src/renderer.ts` |
| `style-engine/` | OOXML style resolution | `style-engine/src/index.ts` |
| `geometry-utils/` | Math utilities for layout | `geometry-utils/src/index.ts` |

## Key Insight: DomPainter Receives Paint-Ready Data

Expand Down Expand Up @@ -57,6 +57,8 @@ reads.
| Change style resolution | `style-engine/` |
| Change text measurement | `measuring-dom/` |

AIDEV-NOTE: `pm-adapter` must preserve shared `SdtMetadata` object identity for sibling blocks in one id-less SDT container; see `contracts/src/sdt-container.ts` before changing SDT imports.

## Style Engine (`style-engine/`)

Single source of truth for OOXML style cascade resolution. All property resolution flows through here.
Expand Down Expand Up @@ -95,26 +97,34 @@ setActiveComment(commentId) → increments layoutVersion → clears pageIndexToS
Maps block IDs to entries for change detection. Only changed pages re-render.
See `blockIdToEntry` in `painters/dom/src/renderer.ts`.

## DomPainter Feature Modules (`painters/dom/src/features/`)
## DomPainter Organization (`painters/dom/AGENTS.md`)

`painters/dom/src/renderer.ts` is the page-level orchestration layer. Keep
feature and content rendering in concern-specific modules under
`painters/dom/src/` (`paragraph/`, `runs/`, `table/`, `images/`, `drawings/`,
`sdt/`, `notes/`, `textbox/`, `ruler/`, `features/`, or `utils/`). Read
`painters/dom/AGENTS.md` before adding renderer code.

## DomPainter Feature Registry

Rendering logic for specific OOXML features is extracted into **feature modules** under `painters/dom/src/features/<feature-name>/`. This keeps `renderer.ts` focused on orchestration while feature-specific logic lives in discoverable, self-contained modules.
Rendering logic for specific OOXML features belongs in **feature modules** under `painters/dom/src/features/<feature-name>/` or the matching concern directory. This keeps `renderer.ts` focused on orchestration while feature-specific logic lives in discoverable, self-contained modules.

### How to find where an OOXML element renders

1. **Search `features/feature-registry.ts`** — maps OOXML element names (e.g., `w:pBdr`, `w:shd`) to their feature module
1. **Search `painters/dom/src/features/feature-registry.ts`** — maps OOXML element names (e.g., `w:pBdr`, `w:shd`) to their feature module
2. Each entry has: `feature` (folder name), `module` (import path), `handles` (OOXML elements), `spec` (ECMA-376 section)
3. Open the feature's `index.ts` for its public API and `@ooxml`/`@spec` annotations

### Adding a new rendering feature

1. **Add a registry entry** in `features/feature-registry.ts` first — this is the source of truth
1. **Add a registry entry** in `painters/dom/src/features/feature-registry.ts` first — this is the source of truth
2. **Create the feature folder** at `features/<feature-name>/`:
- `index.ts` — barrel exports with `@ooxml` and `@spec` JSDoc annotations
- Split logic into focused files (e.g., `group-analysis.ts`, `border-layer.ts`)
- `types.ts` — shared types if needed
3. **Import from the feature module** in `renderer.ts` — renderer calls feature functions, features don't import from renderer
4. **Remove extracted code** from `renderer.ts` — don't leave dead copies
5. **Update imports** in any other files that used the old renderer exports (e.g., `table/renderTableCell.ts`)
5. **Update imports** in any other files that used the old renderer exports (e.g., `painters/dom/src/table/renderTableCell.ts`)

### Feature module conventions

Expand All @@ -128,11 +138,12 @@ Rendering logic for specific OOXML features is extracted into **feature modules*

| Feature | OOXML elements | Folder |
|---------|---------------|--------|
| Paragraph borders & shading | `w:pBdr`, `w:shd` | `features/paragraph-borders/` |
| Paragraph borders & shading | `w:pBdr`, `w:shd` | `painters/dom/src/paragraph/borders/` |

## Entry Points

- `painters/dom/src/renderer.ts` - Main DOM rendering orchestrator (large file — feature logic is being extracted to `features/`)
- `painters/dom/AGENTS.md` - DOM painter organization and contribution rules
- `painters/dom/src/renderer.ts` - Main DOM rendering orchestrator
- `painters/dom/src/features/feature-registry.ts` - OOXML element → feature module lookup
- `painters/dom/src/styles.ts` - CSS class definitions
- `layout-bridge/src/incrementalLayout.ts` - Layout orchestration (called by PresentationEditor)
Expand Down
18 changes: 18 additions & 0 deletions packages/layout-engine/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ export {
} from './engines/tables.js';

export { effectiveTableCellSpacing } from './table-cell-spacing.js';
export { computeTableFragmentHeight } from './table-fragment-height.js';
export {
computeCellSliceContentHeight,
computeFullCellContentHeight,
createCellSliceCursor,
describeCellRenderBlocks,
getCellLines,
getEmbeddedRowLines,
type CellRenderBlock,
type CellSliceCursor,
} from './table-cell-slice.js';

// Table column rescaling (moved from layout-engine for cross-stage use)
export { rescaleColumnWidths } from './table-column-rescale.js';
Expand Down Expand Up @@ -101,6 +112,13 @@ export type {
import type { LayoutSourceIdentity } from './layout-identity.js';
export { cloneColumnLayout, normalizeColumnLayout, widthsEqual } from './column-layout.js';
export type { NormalizedColumnLayout } from './column-layout.js';
export {
getSdtContainerKey,
getSdtContainerKeyForBlock,
getSdtContainerMetadata,
hasExplicitSdtContainerKey,
isSdtContainerMetadata,
} from './sdt-container.js';
/** Inline field annotation metadata extracted from w:sdt nodes. */
export type FieldAnnotationMetadata = {
type: 'fieldAnnotation';
Expand Down
42 changes: 42 additions & 0 deletions packages/layout-engine/contracts/src/sdt-container.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import type { SdtMetadata } from './index.js';
import {
getSdtContainerKey,
getSdtContainerKeyForBlock,
getSdtContainerMetadata,
hasExplicitSdtContainerKey,
} from './sdt-container.js';

describe('SDT container key helpers', () => {
it('uses the first renderable container metadata', () => {
const containerSdt: SdtMetadata = { type: 'documentSection', id: 'section-1' };

expect(getSdtContainerMetadata({ type: 'structuredContent', scope: 'inline', id: 'inline-1' }, containerSdt)).toBe(
containerSdt,
);
});

it('derives explicit keys for block content controls and document sections', () => {
expect(getSdtContainerKey({ type: 'structuredContent', scope: 'block', id: 'sdt-1' })).toBe(
'structuredContent:sdt-1',
);
expect(getSdtContainerKey({ type: 'documentSection', sdBlockId: 'section-block-1' })).toBe(
'documentSection:section-block-1',
);
});

it('derives stable object keys for id-less containers', () => {
const sharedSdt: SdtMetadata = { type: 'structuredContent', scope: 'block', alias: 'Shared' };
const firstKey = getSdtContainerKey(sharedSdt);

expect(firstKey).toMatch(/^idlessSdt:/);
expect(getSdtContainerKey(sharedSdt)).toBe(firstKey);
expect(hasExplicitSdtContainerKey(sharedSdt)).toBe(false);
});

it('derives keys from any block-like object with SDT attrs', () => {
const sdt: SdtMetadata = { type: 'structuredContent', scope: 'block', id: 'media-sdt' };

expect(getSdtContainerKeyForBlock({ attrs: { sdt } })).toBe('structuredContent:media-sdt');
});
});
78 changes: 78 additions & 0 deletions packages/layout-engine/contracts/src/sdt-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { SdtMetadata } from './index.js';

type SdtBlockCandidate = {
attrs?: {
sdt?: SdtMetadata | null;
containerSdt?: SdtMetadata | null;
} | null;
};

const idlessSdtContainerKeys = new WeakMap<SdtMetadata, string>();
let nextIdlessSdtContainerKey = 0;

function getIdlessSdtContainerKey(metadata: SdtMetadata): string {
const existingKey = idlessSdtContainerKeys.get(metadata);
if (existingKey) return existingKey;

// AIDEV-NOTE: Id-less SDT grouping relies on pm-adapter sharing the same
// SdtMetadata object across sibling blocks in one container. Do not replace
// this with alias/title matching; separate controls can share display text.
const key = `idlessSdt:${++nextIdlessSdtContainerKey}`;
idlessSdtContainerKeys.set(metadata, key);
return key;
}

export function isSdtContainerMetadata(sdt: SdtMetadata | null | undefined): boolean {
if (!sdt) return false;
if (sdt.type === 'documentSection') return true;
if (sdt.type === 'structuredContent' && sdt.scope === 'block') return true;
return false;
}

export function getSdtContainerMetadata(
sdt?: SdtMetadata | null,
containerSdt?: SdtMetadata | null,
): SdtMetadata | null {
if (isSdtContainerMetadata(sdt)) return sdt ?? null;
if (isSdtContainerMetadata(containerSdt)) return containerSdt ?? null;
return null;
}

export function getSdtContainerKey(sdt?: SdtMetadata | null, containerSdt?: SdtMetadata | null): string | null {
const metadata = getSdtContainerMetadata(sdt, containerSdt);
if (!metadata) return null;

if (metadata.type === 'structuredContent') {
if (metadata.scope !== 'block') return null;
if (metadata.id) return `structuredContent:${metadata.id}`;
return getIdlessSdtContainerKey(metadata);
}

if (metadata.type === 'documentSection') {
const sectionId = metadata.id ?? metadata.sdBlockId;
if (sectionId) return `documentSection:${sectionId}`;
return getIdlessSdtContainerKey(metadata);
}

return null;
}

export function hasExplicitSdtContainerKey(sdt?: SdtMetadata | null, containerSdt?: SdtMetadata | null): boolean {
const metadata = getSdtContainerMetadata(sdt, containerSdt);
if (!metadata) return false;

if (metadata.type === 'structuredContent') {
return metadata.scope === 'block' && Boolean(metadata.id);
}

if (metadata.type === 'documentSection') {
return Boolean(metadata.id ?? metadata.sdBlockId);
}

return false;
}

export function getSdtContainerKeyForBlock(block?: SdtBlockCandidate | null): string | null {
if (!block) return null;
return getSdtContainerKey(block.attrs?.sdt, block.attrs?.containerSdt);
}
Loading
Loading