Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions packages/layout-engine/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,11 @@ export type PageMargins = {
gutter?: number;
};

export type DocumentBackground = {
/** Solid page background color as a CSS hex value. */
color: string;
};

export type ImageBlockAttrs = {
sdt?: SdtMetadata;
containerSdt?: SdtMetadata;
Expand Down Expand Up @@ -2298,6 +2303,8 @@ export type HeaderFooterLayout = {
export type Layout = {
pageSize: { w: number; h: number };
pages: Page[];
/** Optional document-level page background from OOXML w:background. */
documentBackground?: DocumentBackground;
columns?: ColumnLayout;
headerFooter?: Partial<Record<HeaderFooterType, HeaderFooterLayout>>;
/**
Expand Down
3 changes: 3 additions & 0 deletions packages/layout-engine/contracts/src/resolved-layout.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
ColumnLayout,
ColumnRegion,
DocumentBackground,
DrawingBlock,
FlowMode,
Fragment,
Expand Down Expand Up @@ -32,6 +33,8 @@ export type ResolvedLayout = {
blockVersions?: Record<string, string>;
/** Resolved pages with normalized dimensions. */
pages: ResolvedPage[];
/** Optional document-level page background from OOXML w:background. */
documentBackground?: DocumentBackground;
/** Document epoch identifier from the source layout. Used for change tracking in the painter. */
layoutEpoch?: number;
};
Expand Down
3 changes: 3 additions & 0 deletions packages/layout-engine/layout-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
SectionNumbering,
FlowMode,
NormalizedColumnLayout,
DocumentBackground,
HeaderFooterResolutionSection,
} from '@superdoc/contracts';
import {
Expand Down Expand Up @@ -463,6 +464,7 @@ function calculateChainHeight(
export type LayoutOptions = {
pageSize?: PageSize;
margins?: Margins;
documentBackground?: DocumentBackground;
columns?: ColumnLayout;
flowMode?: FlowMode;
semantic?: {
Expand Down Expand Up @@ -3199,6 +3201,7 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
return {
pageSize,
pages,
...(options.documentBackground ? { documentBackground: options.documentBackground } : {}),
// Note: columns here reflects the effective default for subsequent pages
// after processing sections. Page/region-specific column changes are encoded
// implicitly via fragment positions. Consumers should not assume this is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ export function resolveLayout(input: ResolveLayoutInput): ResolvedLayout {
flowMode,
pageGap: layout.pageGap ?? 0,
pages,
...(layout.documentBackground ? { documentBackground: layout.documentBackground } : {}),
};

if (blocks.length > 0) {
Expand Down
20 changes: 20 additions & 0 deletions packages/layout-engine/painters/dom/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,26 @@ describe('DomPainter', () => {
expect(fragment.textContent).toContain('world');
});

it('paints document-level page background from resolved layout', () => {
const painter = createTestPainter({ blocks: [block], measures: [measure] });
painter.paint({ ...layout, documentBackground: { color: '#EEEEEE' } }, mount);

const page = mount.querySelector('.superdoc-page') as HTMLElement;
expectCssColor(page.style.background, '#EEEEEE');
});

it('keeps the configured page background when no document background is present', () => {
const painter = createTestPainter({
blocks: [block],
measures: [measure],
pageStyles: { background: '#FFFFFF' },
});
painter.paint(layout, mount);

const page = mount.querySelector('.superdoc-page') as HTMLElement;
expectCssColor(page.style.background, '#FFFFFF');
});

it('applies paragraph alignment to line elements', () => {
const alignedBlock: FlowBlock = {
kind: 'paragraph',
Expand Down
23 changes: 14 additions & 9 deletions packages/layout-engine/painters/dom/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1281,18 +1281,19 @@ export class DomPainter {
this.beginPaintSnapshot(resolvedLayout);

this.totalPages = resolvedLayout.pages.length;
const previousLayout = this.currentLayout;
this.currentLayout = resolvedLayout;
if (this.isSemanticFlow) {
// Semantic mode always renders as a single continuous surface.
applyStyles(mount, containerStyles);
mount.style.gap = '0px';
mount.style.alignItems = 'stretch';
if (!this.currentLayout || this.pageStates.length === 0) {
if (!previousLayout || this.pageStates.length === 0) {
this.fullRender(resolvedLayout);
} else {
this.patchLayout(resolvedLayout);
}
this.setMountedPageIndices(this.createAllPageIndices(resolvedLayout.pages.length));
this.currentLayout = resolvedLayout;
this.changedBlocks.clear();
this.currentMapping = null;
return;
Expand Down Expand Up @@ -1339,7 +1340,7 @@ export class DomPainter {
} else {
// Use configured page gap for normal vertical rendering
mount.style.gap = `${this.pageGap}px`;
if (!this.currentLayout || this.pageStates.length === 0) {
if (!previousLayout || this.pageStates.length === 0) {
this.fullRender(resolvedLayout);
} else {
this.patchLayout(resolvedLayout);
Expand Down Expand Up @@ -2561,22 +2562,26 @@ export class DomPainter {
}

private getEffectivePageStyles(): PageStyles | undefined {
const documentBackgroundColor = this.currentLayout?.documentBackground?.color;
const base = this.options.pageStyles ?? {};
const baseWithDocumentBackground = documentBackgroundColor
? { ...base, background: documentBackgroundColor }
: base;

if (this.isSemanticFlow) {
const base = this.options.pageStyles ?? {};
return {
...base,
background: base.background ?? 'var(--sd-layout-page-bg, #fff)',
...baseWithDocumentBackground,
background: baseWithDocumentBackground.background ?? 'var(--sd-layout-page-bg, #fff)',
boxShadow: 'none',
border: 'none',
margin: '0',
};
}
if (this.virtualEnabled && this.layoutMode === 'vertical') {
// Remove top/bottom margins to avoid double-counting with container gap during virtualization
const base = this.options.pageStyles ?? {};
return { ...base, margin: '0 auto' };
return { ...baseWithDocumentBackground, margin: '0 auto' };
}
return this.options.pageStyles;
return documentBackgroundColor ? baseWithDocumentBackground : this.options.pageStyles;
}

private renderFragment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ import type {
SectionMetadata,
TrackedChangesMode,
Fragment,
DocumentBackground,
} from '@superdoc/contracts';
import { extractHeaderFooterSpace as _extractHeaderFooterSpace } from '@superdoc/contracts';
// TrackChangesBasePluginKey is used by #syncTrackedChangesPreferences and getTrackChangesPluginState.
Expand Down Expand Up @@ -502,6 +503,7 @@ export class PresentationEditor extends EventEmitter {
/** Scroll-isolating wrapper around #hiddenHost. Append/remove this from the DOM. */
#hiddenHostWrapper: HTMLElement;
#layoutOptions: LayoutEngineOptions;
#configuredDocumentBackground: DocumentBackground | undefined;
#layoutState: LayoutState = { blocks: [], measures: [], layout: null, bookmarks: new Map() };
#layoutLookupBlocks: FlowBlock[] = [];
#layoutLookupMeasures: Measure[] = [];
Expand Down Expand Up @@ -702,6 +704,9 @@ export class PresentationEditor extends EventEmitter {

const requestedFlowMode = options.layoutEngineOptions?.flowMode === 'semantic' ? 'semantic' : 'paginated';
const requestedLayoutMode = options.layoutEngineOptions?.layoutMode ?? 'vertical';
this.#configuredDocumentBackground = this.#coerceDocumentBackground(
options.layoutEngineOptions?.documentBackground,
);
this.#layoutOptions = {
pageSize: options.layoutEngineOptions?.pageSize ?? DEFAULT_PAGE_SIZE,
margins: options.layoutEngineOptions?.margins ?? DEFAULT_MARGINS,
Expand All @@ -713,6 +718,7 @@ export class PresentationEditor extends EventEmitter {
}
: options.layoutEngineOptions?.virtualization,
zoom: options.layoutEngineOptions?.zoom ?? 1,
...(this.#configuredDocumentBackground ? { documentBackground: this.#configuredDocumentBackground } : {}),
pageStyles: options.layoutEngineOptions?.pageStyles,
debugLabel: options.layoutEngineOptions?.debugLabel,
layoutMode: requestedFlowMode === 'semantic' ? 'vertical' : requestedLayoutMode,
Expand Down Expand Up @@ -7861,6 +7867,12 @@ export class PresentationEditor extends EventEmitter {
this.#layoutOptions.pageSize = pageSize;
this.#layoutOptions.margins = margins;
const flowMode = this.#layoutOptions.flowMode ?? 'paginated';
const documentBackground = this.#resolveDocumentBackground();
if (documentBackground) {
this.#layoutOptions.documentBackground = documentBackground;
} else {
delete this.#layoutOptions.documentBackground;
}

const resolvedMargins = {
top: margins.top!,
Expand Down Expand Up @@ -7900,17 +7912,18 @@ export class PresentationEditor extends EventEmitter {
marginBottom: semanticMargins.bottom,
},
sectionMetadata,
...(documentBackground ? { documentBackground } : {}),
};
}

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

const alternateHeaders = this.#resolveAlternateHeadersFlag();

return {
flowMode: 'paginated',
pageSize,
margins: resolvedMargins,
...(documentBackground ? { documentBackground } : {}),
...(columns ? { columns } : {}),
sectionMetadata,
alternateHeaders,
Expand Down Expand Up @@ -7938,6 +7951,19 @@ export class PresentationEditor extends EventEmitter {
return out;
}

#coerceDocumentBackground(candidate: unknown): DocumentBackground | undefined {
if (!candidate || typeof candidate !== 'object') return undefined;
const color = (candidate as { color?: unknown }).color;
return typeof color === 'string' && color.length > 0 ? { color } : undefined;
}

#resolveDocumentBackground(): DocumentBackground | undefined {
return (
this.#coerceDocumentBackground(this.#editor?.state?.doc?.attrs?.documentBackground) ??
(this.#configuredDocumentBackground ? { ...this.#configuredDocumentBackground } : undefined)
);
}

#buildHeaderFooterInput() {
if (this.#isSemanticFlowMode()) {
return null;
Expand Down
Loading
Loading