Skip to content

Commit 78572d7

Browse files
committed
feat: import and render document-level background
1 parent 07815d3 commit 78572d7

11 files changed

Lines changed: 163 additions & 19 deletions

File tree

packages/layout-engine/contracts/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,11 @@ export type PageMargins = {
800800
gutter?: number;
801801
};
802802

803+
export type DocumentBackground = {
804+
/** Solid page background color as a CSS hex value. */
805+
color: string;
806+
};
807+
803808
export type ImageBlockAttrs = {
804809
sdt?: SdtMetadata;
805810
containerSdt?: SdtMetadata;
@@ -2257,6 +2262,8 @@ export type HeaderFooterLayout = {
22572262
export type Layout = {
22582263
pageSize: { w: number; h: number };
22592264
pages: Page[];
2265+
/** Optional document-level page background from OOXML w:background. */
2266+
documentBackground?: DocumentBackground;
22602267
columns?: ColumnLayout;
22612268
headerFooter?: Partial<Record<HeaderFooterType, HeaderFooterLayout>>;
22622269
/**

packages/layout-engine/contracts/src/resolved-layout.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
ColumnLayout,
33
ColumnRegion,
4+
DocumentBackground,
45
DrawingBlock,
56
FlowMode,
67
Fragment,
@@ -32,6 +33,8 @@ export type ResolvedLayout = {
3233
blockVersions?: Record<string, string>;
3334
/** Resolved pages with normalized dimensions. */
3435
pages: ResolvedPage[];
36+
/** Optional document-level page background from OOXML w:background. */
37+
documentBackground?: DocumentBackground;
3538
/** Document epoch identifier from the source layout. Used for change tracking in the painter. */
3639
layoutEpoch?: number;
3740
};

packages/layout-engine/layout-engine/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
SectionNumbering,
2929
FlowMode,
3030
NormalizedColumnLayout,
31+
DocumentBackground,
3132
} from '@superdoc/contracts';
3233
import { buildLayoutSourceIdentityForFragment, normalizeColumnLayout, getFragmentZIndex } from '@superdoc/contracts';
3334
import { createFloatingObjectManager, computeAnchorX } from './floating-objects.js';
@@ -456,6 +457,7 @@ function calculateChainHeight(
456457
export type LayoutOptions = {
457458
pageSize?: PageSize;
458459
margins?: Margins;
460+
documentBackground?: DocumentBackground;
459461
columns?: ColumnLayout;
460462
flowMode?: FlowMode;
461463
semantic?: {
@@ -3219,6 +3221,7 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
32193221
return {
32203222
pageSize,
32213223
pages,
3224+
...(options.documentBackground ? { documentBackground: options.documentBackground } : {}),
32223225
// Note: columns here reflects the effective default for subsequent pages
32233226
// after processing sections. Page/region-specific column changes are encoded
32243227
// implicitly via fragment positions. Consumers should not assume this is

packages/layout-engine/layout-resolved/src/resolveLayout.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export function resolveLayout(input: ResolveLayoutInput): ResolvedLayout {
343343
flowMode,
344344
pageGap: layout.pageGap ?? 0,
345345
pages,
346+
...(layout.documentBackground ? { documentBackground: layout.documentBackground } : {}),
346347
};
347348

348349
if (blocks.length > 0) {

packages/layout-engine/painters/dom/src/index.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,26 @@ describe('DomPainter', () => {
431431
expect(fragment.textContent).toContain('world');
432432
});
433433

434+
it('paints document-level page background from resolved layout', () => {
435+
const painter = createTestPainter({ blocks: [block], measures: [measure] });
436+
painter.paint({ ...layout, documentBackground: { color: '#EEEEEE' } }, mount);
437+
438+
const page = mount.querySelector('.superdoc-page') as HTMLElement;
439+
expectCssColor(page.style.background, '#EEEEEE');
440+
});
441+
442+
it('keeps the configured page background when no document background is present', () => {
443+
const painter = createTestPainter({
444+
blocks: [block],
445+
measures: [measure],
446+
pageStyles: { background: '#FFFFFF' },
447+
});
448+
painter.paint(layout, mount);
449+
450+
const page = mount.querySelector('.superdoc-page') as HTMLElement;
451+
expectCssColor(page.style.background, '#FFFFFF');
452+
});
453+
434454
it('applies paragraph alignment to line elements', () => {
435455
const alignedBlock: FlowBlock = {
436456
kind: 'paragraph',

packages/layout-engine/painters/dom/src/renderer.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,18 +1201,19 @@ export class DomPainter {
12011201
this.beginPaintSnapshot(resolvedLayout);
12021202

12031203
this.totalPages = resolvedLayout.pages.length;
1204+
const previousLayout = this.currentLayout;
1205+
this.currentLayout = resolvedLayout;
12041206
if (this.isSemanticFlow) {
12051207
// Semantic mode always renders as a single continuous surface.
12061208
applyStyles(mount, containerStyles);
12071209
mount.style.gap = '0px';
12081210
mount.style.alignItems = 'stretch';
1209-
if (!this.currentLayout || this.pageStates.length === 0) {
1211+
if (!previousLayout || this.pageStates.length === 0) {
12101212
this.fullRender(resolvedLayout);
12111213
} else {
12121214
this.patchLayout(resolvedLayout);
12131215
}
12141216
this.setMountedPageIndices(this.createAllPageIndices(resolvedLayout.pages.length));
1215-
this.currentLayout = resolvedLayout;
12161217
this.changedBlocks.clear();
12171218
this.currentMapping = null;
12181219
return;
@@ -1259,7 +1260,7 @@ export class DomPainter {
12591260
} else {
12601261
// Use configured page gap for normal vertical rendering
12611262
mount.style.gap = `${this.pageGap}px`;
1262-
if (!this.currentLayout || this.pageStates.length === 0) {
1263+
if (!previousLayout || this.pageStates.length === 0) {
12631264
this.fullRender(resolvedLayout);
12641265
} else {
12651266
this.patchLayout(resolvedLayout);
@@ -2475,22 +2476,26 @@ export class DomPainter {
24752476
}
24762477

24772478
private getEffectivePageStyles(): PageStyles | undefined {
2479+
const documentBackgroundColor = this.currentLayout?.documentBackground?.color;
2480+
const base = this.options.pageStyles ?? {};
2481+
const baseWithDocumentBackground = documentBackgroundColor
2482+
? { ...base, background: documentBackgroundColor }
2483+
: base;
2484+
24782485
if (this.isSemanticFlow) {
2479-
const base = this.options.pageStyles ?? {};
24802486
return {
2481-
...base,
2482-
background: base.background ?? 'var(--sd-layout-page-bg, #fff)',
2487+
...baseWithDocumentBackground,
2488+
background: baseWithDocumentBackground.background ?? 'var(--sd-layout-page-bg, #fff)',
24832489
boxShadow: 'none',
24842490
border: 'none',
24852491
margin: '0',
24862492
};
24872493
}
24882494
if (this.virtualEnabled && this.layoutMode === 'vertical') {
24892495
// Remove top/bottom margins to avoid double-counting with container gap during virtualization
2490-
const base = this.options.pageStyles ?? {};
2491-
return { ...base, margin: '0 auto' };
2496+
return { ...baseWithDocumentBackground, margin: '0 auto' };
24922497
}
2493-
return this.options.pageStyles;
2498+
return documentBackgroundColor ? baseWithDocumentBackground : this.options.pageStyles;
24942499
}
24952500

24962501
private renderFragment(

packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ import type {
191191
SectionMetadata,
192192
TrackedChangesMode,
193193
Fragment,
194+
DocumentBackground,
194195
} from '@superdoc/contracts';
195196
import { extractHeaderFooterSpace as _extractHeaderFooterSpace } from '@superdoc/contracts';
196197
// TrackChangesBasePluginKey is used by #syncTrackedChangesPreferences and getTrackChangesPluginState.
@@ -7833,6 +7834,7 @@ export class PresentationEditor extends EventEmitter {
78337834
this.#layoutOptions.pageSize = pageSize;
78347835
this.#layoutOptions.margins = margins;
78357836
const flowMode = this.#layoutOptions.flowMode ?? 'paginated';
7837+
const documentBackground = this.#resolveDocumentBackground();
78367838

78377839
const resolvedMargins = {
78387840
top: margins.top!,
@@ -7872,23 +7874,31 @@ export class PresentationEditor extends EventEmitter {
78727874
marginBottom: semanticMargins.bottom,
78737875
},
78747876
sectionMetadata,
7877+
...(documentBackground ? { documentBackground } : {}),
78757878
};
78767879
}
78777880

78787881
this.#hiddenHost.style.width = `${pageSize.w}px`;
78797882

78807883
const alternateHeaders = this.#resolveAlternateHeadersFlag();
7881-
78827884
return {
78837885
flowMode: 'paginated',
78847886
pageSize,
78857887
margins: resolvedMargins,
7888+
...(documentBackground ? { documentBackground } : {}),
78867889
...(columns ? { columns } : {}),
78877890
sectionMetadata,
78887891
alternateHeaders,
78897892
};
78907893
}
78917894

7895+
#resolveDocumentBackground(): DocumentBackground | undefined {
7896+
const background = this.#editor?.state?.doc?.attrs?.documentBackground;
7897+
if (!background || typeof background !== 'object') return undefined;
7898+
const color = (background as { color?: unknown }).color;
7899+
return typeof color === 'string' && color.length > 0 ? { color } : undefined;
7900+
}
7901+
78927902
#buildHeaderFooterInput() {
78937903
if (this.#isSemanticFlowMode()) {
78947904
return null;

packages/super-editor/src/editors/v1/core/presentation-editor/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
FlowMode,
1616
SectionMetadata,
1717
TrackChangeAuthor,
18+
DocumentBackground,
1819
} from '@superdoc/contracts';
1920
import type { LayoutMode, RulerOptions } from '@superdoc/painter-dom';
2021
import type { ProofingConfig } from './proofing/types.js';
@@ -130,6 +131,7 @@ export type ResolvedLayoutOptions =
130131
pageSize: PageSize;
131132
margins: ResolvedMarginsBase;
132133
columns?: { count: number; gap: number };
134+
documentBackground?: DocumentBackground;
133135
sectionMetadata: SectionMetadata[];
134136
alternateHeaders?: boolean;
135137
}
@@ -138,6 +140,7 @@ export type ResolvedLayoutOptions =
138140
pageSize: PageSize;
139141
margins: ResolvedMarginsBase;
140142
columns: { count: 1; gap: 0 };
143+
documentBackground?: DocumentBackground;
141144
semantic: {
142145
contentWidth: number;
143146
marginLeft: number;
@@ -151,6 +154,7 @@ export type ResolvedLayoutOptions =
151154
export type LayoutEngineOptions = {
152155
pageSize?: PageSize;
153156
margins?: PageMargins;
157+
documentBackground?: DocumentBackground;
154158
zoom?: number;
155159
virtualization?: VirtualizationOptions;
156160
pageStyles?: Record<string, unknown>;

packages/super-editor/src/editors/v1/core/super-converter/v2/importer/docxImporter.js

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,23 @@ const parseTrackedChangeSourceIdMap = (raw) => {
179179
const readTrackedChangeSourceIdMap = (docx) =>
180180
parseTrackedChangeSourceIdMap(readCustomProperty(docx, TRACKED_CHANGE_SOURCE_ID_MAP_PROPERTY));
181181

182+
const normalizeDocumentBackgroundColor = (value) => {
183+
if (typeof value !== 'string') return null;
184+
const trimmed = value.trim();
185+
if (!/^[0-9a-fA-F]{6}$/.test(trimmed)) return null;
186+
return `#${trimmed.toUpperCase()}`;
187+
};
188+
189+
const getDocumentBackground = (documentNode) => {
190+
const background = documentNode?.elements?.find((el) => el?.name === 'w:background');
191+
const color = normalizeDocumentBackgroundColor(background?.attributes?.['w:color'] ?? background?.attributes?.color);
192+
if (!background || !color) return null;
193+
return {
194+
color,
195+
originalXml: carbonCopy(background),
196+
};
197+
};
198+
182199
/**
183200
* Detect the document-level threading profile for comments based on file structure.
184201
* @param {ParsedDocx} docx The parsed docx object
@@ -203,6 +220,7 @@ const detectCommentThreadingProfile = (docx) => {
203220
export const createDocumentJson = (docx, converter, editor) => {
204221
const json = carbonCopy(getInitialJSON(docx));
205222
if (!json) return null;
223+
const documentBackground = getDocumentBackground(json.elements?.[0]);
206224

207225
if (converter) {
208226
importFootnotePropertiesFromSettings(docx, converter);
@@ -290,21 +308,26 @@ export const createDocumentJson = (docx, converter, editor) => {
290308
attributes: json.elements[0].attributes,
291309
// Attach body-level sectPr if it exists
292310
...(bodySectPr ? { bodySectPr } : {}),
311+
...(documentBackground ? { documentBackground } : {}),
293312
},
294313
};
314+
const pageStyles = getDocumentStyles(
315+
node,
316+
docx,
317+
converter,
318+
editor,
319+
numbering,
320+
translatedNumbering,
321+
translatedLinkedStyles,
322+
);
323+
if (documentBackground) {
324+
pageStyles.documentBackground = { color: documentBackground.color };
325+
}
295326

296327
return {
297328
pmDoc: result,
298329
savedTagsToRestore: node,
299-
pageStyles: getDocumentStyles(
300-
node,
301-
docx,
302-
converter,
303-
editor,
304-
numbering,
305-
translatedNumbering,
306-
translatedLinkedStyles,
307-
),
330+
pageStyles,
308331
comments,
309332
footnotes,
310333
endnotes,

packages/super-editor/src/editors/v1/extensions/document/document.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ export const Document = Node.create({
5858
* ensuring that the last section’s page size/orientation/margins are applied correctly.
5959
*/
6060
},
61+
documentBackground: {
62+
rendered: false,
63+
default: null,
64+
/**
65+
* Document-level background metadata extracted from w:document > w:background.
66+
* Stored separately from paragraph/table shading so page paint can apply it
67+
* without changing content-level background semantics.
68+
*/
69+
},
6170
};
6271
},
6372

0 commit comments

Comments
 (0)