diff --git a/devtools/visual-testing/packages/test-helpers/package.json b/devtools/visual-testing/packages/test-helpers/package.json index 08bb3a2683..08e96ef865 100644 --- a/devtools/visual-testing/packages/test-helpers/package.json +++ b/devtools/visual-testing/packages/test-helpers/package.json @@ -11,6 +11,9 @@ "./stability": "./src/stability.ts", "./interactions": "./src/interactions.ts" }, + "dependencies": { + "@superdoc-testing/harness": "workspace:*" + }, "peerDependencies": { "@playwright/test": ">=1.40.0" }, diff --git a/devtools/visual-testing/scripts/compare-interactions.ts b/devtools/visual-testing/scripts/compare-interactions.ts index a1ab1bfa14..fe1bfa6f5e 100644 --- a/devtools/visual-testing/scripts/compare-interactions.ts +++ b/devtools/visual-testing/scripts/compare-interactions.ts @@ -15,9 +15,10 @@ import fs from 'node:fs'; import path from 'node:path'; import { generateResultsFolderName, getSuperdocVersion } from './generate-refs.js'; -import { findPngFiles } from './compare.js'; +import { findPngFiles, matchesFilterWithBrowserPrefix } from './compare.js'; import { colors } from './terminal.js'; -import { resolveBrowserNames } from './browser-utils.js'; +import { normalizePath } from './utils.js'; +import { BROWSER_NAMES, resolveBaselineFolderForBrowser, resolveBrowserNames } from './browser-utils.js'; import { isPathLikeVersion, normalizeVersionLabel, @@ -38,6 +39,35 @@ import { ensureLocalTarballInstalled } from './workspace-utils.js'; const BASELINES_DIR = 'baselines-interactions'; +function listFilteredPngs(dir: string, filters: string[], matches: string[], excludes: string[]): string[] { + return findPngFiles(dir) + .map((relativePath) => normalizePath(relativePath)) + .filter((relativePath) => matchesFilterWithBrowserPrefix(relativePath, undefined, filters, matches, excludes)); +} + +function findMissingBaselineDocFilters(options: { + baselineFolder: string; + resultsFolder: string; + filters: string[]; + matches: string[]; + excludes: string[]; +}): string[] { + const baselineFiles = new Set( + listFilteredPngs(options.baselineFolder, options.filters, options.matches, options.excludes), + ); + const resultFiles = listFilteredPngs(options.resultsFolder, options.filters, options.matches, options.excludes); + const missingDocs = new Set(); + + for (const resultPath of resultFiles) { + if (baselineFiles.has(resultPath)) continue; + const docKey = path.posix.dirname(resultPath); + if (!docKey || docKey === '.') continue; + missingDocs.add(docKey); + } + + return Array.from(missingDocs); +} + interface CompareInteractionArgs { baselineVersion?: string; targetVersion?: string; @@ -451,8 +481,9 @@ async function main(): Promise { await runGenerate(resultsFolderName, filters, matches, excludes, browserArg, scaleFactor, storageArgs); } + let resultsRoot: string | undefined; if (resultsFolderName) { - const resultsRoot = path.isAbsolute(resultsFolderName) + resultsRoot = path.isAbsolute(resultsFolderName) ? path.join(resultsFolderName, 'interactions') : path.join('screenshots', resultsFolderName, 'interactions'); const hasBrowserResults = browsers.some((browser) => fs.existsSync(path.join(resultsRoot, browser))); @@ -466,6 +497,61 @@ async function main(): Promise { } } + if (mode === 'cloud' && !refreshBaselines && resultsFolderName && resultsRoot) { + const baselineVersionDir = path.join(baselineDir, baselineToUse); + const resultsHasBrowserDirs = BROWSER_NAMES.some((browser) => fs.existsSync(path.join(resultsRoot, browser))); + + for (const browser of browsers) { + if (!resultsHasBrowserDirs && browser !== 'chromium') { + continue; + } + const resultsFolder = resultsHasBrowserDirs ? path.join(resultsRoot, browser) : resultsRoot; + if (!fs.existsSync(resultsFolder)) { + continue; + } + const baselineFolder = resolveBaselineFolderForBrowser(baselineVersionDir, browser); + if (!fs.existsSync(baselineFolder)) { + continue; + } + + const missingFilters = findMissingBaselineDocFilters({ + baselineFolder, + resultsFolder, + filters, + matches, + excludes, + }); + if (missingFilters.length === 0) { + continue; + } + + console.log( + colors.muted( + `↻ Missing interaction baselines detected in cache. Refreshing ${missingFilters.length} story(s) for ${browser}...`, + ), + ); + const refreshed = await refreshBaselineSubset({ + prefix: BASELINES_DIR, + version: baselineToUse, + localRoot: baselineDir, + filters: missingFilters, + excludes, + browsers: [browser], + }); + if (refreshed.matched === 0) { + console.warn( + colors.warning(`No interaction baseline files matched for refresh (${browser}). Keeping current cache.`), + ); + } else { + console.log( + colors.success( + `↻ Refreshed ${refreshed.downloaded} interaction baseline file(s) for ${baselineToUse} (${browser}).`, + ), + ); + } + } + } + await runCompare( resultsFolderName, baselineToUse, diff --git a/devtools/visual-testing/scripts/compare.ts b/devtools/visual-testing/scripts/compare.ts index 9a21dda407..5e73d74fbe 100644 --- a/devtools/visual-testing/scripts/compare.ts +++ b/devtools/visual-testing/scripts/compare.ts @@ -26,7 +26,7 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { createHash } from 'node:crypto'; -import { spawn, spawnSync, type ChildProcess } from 'node:child_process'; +import { spawn, spawnSync } from 'node:child_process'; import { PNG } from 'pngjs'; import pixelmatch from 'pixelmatch'; import { generateResultsFolderName, getSuperdocVersion, sanitizeFilename } from './generate-refs.js'; @@ -45,7 +45,6 @@ import { resolveBaselineFolderForBrowser, type BrowserName, } from './browser-utils.js'; -import { sleep, createLogBuffer } from './utils.js'; import { ensureBaselineDownloaded, getLatestBaselineVersion, refreshBaselineSubset } from './r2-baselines.js'; import { buildStorageArgs, @@ -55,16 +54,7 @@ import { resolveDocsDir, type StorageMode, } from './storage-flags.js'; -import { - HARNESS_PORT, - HARNESS_URL, - HARNESS_START_TIMEOUT_MS, - HARNESS_LOG_BUFFER_LIMIT, - isPortOpen, - waitForPort, - ensureHarnessRunning, - stopHarness, -} from './harness-utils.js'; +import { HARNESS_PORT, HARNESS_URL, isPortOpen, ensureHarnessRunning, stopHarness } from './harness-utils.js'; import { ensureLocalTarballInstalled } from './workspace-utils.js'; // Configuration @@ -702,6 +692,31 @@ function extractAssetPath(relativePath: string, resultsFolderName: string, resul return assetPath; } +function deriveMissingBaselineDocFilters( + report: ComparisonReport, + resultsFolderName: string, + resultsPrefix: string | undefined, + browser?: BrowserName, +): string[] { + const filters = new Set(); + + for (const result of report.results) { + if (result.reason !== 'missing_in_baseline') continue; + const assetPath = extractAssetPath(result.relativePath, resultsFolderName, resultsPrefix); + const normalized = normalizePath(assetPath); + let docKey = path.posix.dirname(normalized); + if (!docKey || docKey === '.') continue; + if (browser && docKey.startsWith(`${browser}/`)) { + docKey = docKey.slice(browser.length + 1); + } + if (docKey && docKey !== '.') { + filters.add(docKey); + } + } + + return Array.from(filters); +} + function readStoryMetadata(filePath: string): StoryMetadataFile | null { if (!fs.existsSync(filePath)) return null; try { @@ -2148,6 +2163,52 @@ async function main(): Promise { }, }); + if (mode === 'cloud' && !refreshBaselines && report.summary.missingInBaseline > 0) { + const refreshFilters = deriveMissingBaselineDocFilters(report, resultsFolderName!, resultsPrefix, browser); + if (refreshFilters.length > 0) { + console.log( + colors.muted( + `↻ Missing baseline files detected in cache. Refreshing ${refreshFilters.length} doc(s) from R2...`, + ), + ); + const refreshed = await refreshBaselineSubset({ + prefix: baselinePrefix, + version: baselineToUse, + localRoot: baselineDir, + filters: refreshFilters, + excludes, + browsers: [browser], + }); + if (refreshed.matched > 0) { + report = await runComparison(resultsFolderName!, { + threshold, + baselineVersion: baselineToUse, + baselineRoot: baselineDir, + resultsRoot: resolvedResultsRoot, + resultsPrefix, + browser, + outputFolderName, + filters, + matches, + excludes, + ignorePrefixes, + reportOptions: { + showAll: reportAll, + reportFileName, + mode: resolvedMode, + trimPrefix: resolvedTrim, + }, + }); + } else { + console.warn(colors.warning('No baseline files matched for refresh; keeping current comparison results.')); + } + } else { + console.warn( + colors.warning('Missing baseline files detected but no doc filters could be derived for refresh.'), + ); + } + } + if (resolvedMode === 'visual' && includeWord) { const wordResultsPrefix = browser ? `${normalizePrefix(resultsPrefix) ?? ''}${browser}/` : resultsPrefix; report = await augmentReportWithWord(report, { diff --git a/packages/layout-engine/contracts/src/index.ts b/packages/layout-engine/contracts/src/index.ts index eaebd6444c..943aa4ab95 100644 --- a/packages/layout-engine/contracts/src/index.ts +++ b/packages/layout-engine/contracts/src/index.ts @@ -529,6 +529,8 @@ export type ImageBlock = { margin?: BoxSpacing; anchor?: ImageAnchor; wrap?: ImageWrap; + /** Stacking order from OOXML relativeHeight (same formula as editor: Math.max(0, relativeHeight - OOXML_Z_INDEX_BASE)) */ + zIndex?: number; attrs?: ImageBlockAttrs; // VML image adjustments for watermark effects gain?: string | number; // Brightness/washout (VML hex string or number) @@ -1587,6 +1589,7 @@ export type ImageFragment = { width: number; height: number; isAnchored?: boolean; + behindDoc?: boolean; zIndex?: number; pmStart?: number; pmEnd?: number; @@ -1602,6 +1605,7 @@ export type DrawingFragment = { width: number; height: number; isAnchored?: boolean; + behindDoc?: boolean; zIndex?: number; geometry: DrawingGeometry; scale: number; diff --git a/packages/layout-engine/layout-engine/package.json b/packages/layout-engine/layout-engine/package.json index 781475b170..d21efc8256 100644 --- a/packages/layout-engine/layout-engine/package.json +++ b/packages/layout-engine/layout-engine/package.json @@ -13,7 +13,8 @@ "test": "vitest run" }, "dependencies": { + "@superdoc/common": "workspace:*", "@superdoc/contracts": "workspace:*", - "@superdoc/common": "workspace:*" + "@superdoc/pm-adapter": "workspace:*" } } diff --git a/packages/layout-engine/layout-engine/src/index.ts b/packages/layout-engine/layout-engine/src/index.ts index aed4c5aa2a..e7629254ac 100644 --- a/packages/layout-engine/layout-engine/src/index.ts +++ b/packages/layout-engine/layout-engine/src/index.ts @@ -41,6 +41,7 @@ import { createPaginator, type PageState, type ConstraintBoundary } from './pagi import { formatPageNumber } from './pageNumbering.js'; import { shouldSuppressSpacingForEmpty } from './layout-utils.js'; import { balancePageColumns } from './column-balancing.js'; +import { getFragmentZIndex } from '@superdoc/pm-adapter/utilities.js'; type PageSize = { w: number; h: number }; type Margins = { @@ -1062,7 +1063,6 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options if (!state) { // Track if we're entering a new section (pendingSectionIndex was just set) const isEnteringNewSection = pendingSectionIndex !== null; - const newSectionIndex = isEnteringNewSection ? pendingSectionIndex : activeSectionIndex; const applied = applyPendingToActive({ activeTopMargin, @@ -1956,7 +1956,8 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options width: imgMeasure.width, height: imgMeasure.height, isAnchored: true, - zIndex: imgBlock.anchor?.behindDoc ? 0 : 1, + behindDoc: imgBlock.anchor?.behindDoc === true, + zIndex: getFragmentZIndex(imgBlock), metadata, }; diff --git a/packages/layout-engine/layout-engine/src/layout-drawing.ts b/packages/layout-engine/layout-engine/src/layout-drawing.ts index d95b952934..a1d7f39731 100644 --- a/packages/layout-engine/layout-engine/src/layout-drawing.ts +++ b/packages/layout-engine/layout-engine/src/layout-drawing.ts @@ -2,6 +2,7 @@ import type { DrawingBlock, DrawingMeasure, DrawingFragment } from '@superdoc/co import type { NormalizedColumns } from './layout-image.js'; import type { PageState } from './paginator.js'; import { extractBlockPmRange } from './layout-utils.js'; +import { getFragmentZIndex } from '@superdoc/pm-adapter/utilities.js'; /** * Context for laying out a drawing block (vector shape) within the page layout. @@ -118,7 +119,7 @@ export function layoutDrawingBlock({ geometry: measure.geometry, scale: measure.scale, drawingContentId: block.drawingContentId, - zIndex: block.zIndex, + zIndex: getFragmentZIndex(block), pmStart: pmRange.pmStart, pmEnd: pmRange.pmEnd, }; diff --git a/packages/layout-engine/layout-engine/src/layout-paragraph.ts b/packages/layout-engine/layout-engine/src/layout-paragraph.ts index c1fd5d4d0c..4b91b15e78 100644 --- a/packages/layout-engine/layout-engine/src/layout-paragraph.ts +++ b/packages/layout-engine/layout-engine/src/layout-paragraph.ts @@ -21,6 +21,7 @@ import { isEmptyTextParagraph, } from './layout-utils.js'; import { computeAnchorX } from './floating-objects.js'; +import { getFragmentZIndex } from '@superdoc/pm-adapter/utilities.js'; const spacingDebugEnabled = false; /** @@ -388,7 +389,8 @@ export function layoutParagraphBlock(ctx: ParagraphLayoutContext, anchors?: Para width: entry.measure.width, height: entry.measure.height, isAnchored: true, - zIndex: entry.block.anchor?.behindDoc ? 0 : 1, + behindDoc: entry.block.anchor?.behindDoc === true, + zIndex: getFragmentZIndex(entry.block), metadata, }; if (pmRange.pmStart != null) fragment.pmStart = pmRange.pmStart; @@ -406,7 +408,8 @@ export function layoutParagraphBlock(ctx: ParagraphLayoutContext, anchors?: Para geometry: entry.measure.geometry, scale: entry.measure.scale, isAnchored: true, - zIndex: entry.block.anchor?.behindDoc ? 0 : 1, + behindDoc: entry.block.anchor?.behindDoc === true, + zIndex: getFragmentZIndex(entry.block), drawingContentId: entry.block.drawingContentId, }; if (pmRange.pmStart != null) fragment.pmStart = pmRange.pmStart; @@ -432,7 +435,6 @@ export function layoutParagraphBlock(ctx: ParagraphLayoutContext, anchors?: Para const negativeRightIndent = indentRight < 0 ? indentRight : 0; // Paragraph content width should honor paragraph indents (including negative values). const remeasureWidth = Math.max(1, columnWidth - indentLeft - indentRight); - const hasNegativeIndent = indentLeft < 0 || indentRight < 0; let didRemeasureForColumnWidth = false; // Track remeasured marker info to ensure fragment gets accurate marker text width let remeasuredMarkerInfo: ParagraphMeasure['marker'] | undefined; diff --git a/packages/layout-engine/painters/dom/package.json b/packages/layout-engine/painters/dom/package.json index 799c560689..c61ced9133 100644 --- a/packages/layout-engine/painters/dom/package.json +++ b/packages/layout-engine/painters/dom/package.json @@ -19,6 +19,7 @@ "dependencies": { "@superdoc/contracts": "workspace:*", "@superdoc/font-utils": "workspace:*", + "@superdoc/pm-adapter": "workspace:*", "@superdoc/preset-geometry": "workspace:*", "@superdoc/url-validation": "workspace:*" }, diff --git a/packages/layout-engine/painters/dom/src/index.test.ts b/packages/layout-engine/painters/dom/src/index.test.ts index b2b2513dc7..1aadaf66ea 100644 --- a/packages/layout-engine/painters/dom/src/index.test.ts +++ b/packages/layout-engine/painters/dom/src/index.test.ts @@ -2527,7 +2527,7 @@ describe('DomPainter', () => { height: 30, }; - // behindDoc fragment has zIndex: 0 (set by layout engine) + // behindDoc routing should use explicit fragment metadata, not zIndex proxy. const behindDocFragment = { kind: 'image' as const, blockId: 'behind-doc-img', @@ -2535,7 +2535,8 @@ describe('DomPainter', () => { y: 0, width: 200, height: 100, - zIndex: 0, // behindDoc images get zIndex: 0 + behindDoc: true, + zIndex: 5, // deliberately non-zero to prove routing is metadata-driven isAnchored: true, }; @@ -2547,6 +2548,7 @@ describe('DomPainter', () => { y: 10, width: 50, height: 30, + behindDoc: false, }; const painter = createDomPainter({ @@ -2563,8 +2565,6 @@ describe('DomPainter', () => { const headerEl = mount.querySelector('.superdoc-page-header'); expect(headerEl).toBeTruthy(); - // behindDoc image should NOT be inside header container - const behindDocInHeader = headerEl?.querySelector('img[src*="base64"]'); // Normal image should be inside header container const normalInHeader = headerEl?.querySelectorAll('.superdoc-fragment'); @@ -2615,7 +2615,8 @@ describe('DomPainter', () => { y: 0, width: 200, height: 100, - zIndex: 0, + behindDoc: true, + zIndex: 5, isAnchored: true, }; diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 5b69abb9be..42774924a6 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -69,12 +69,7 @@ import { sanitizeHref, encodeTooltip } from '@superdoc/url-validation'; import { renderTableFragment as renderTableFragmentElement } from './table/renderTableFragment.js'; import { assertPmPositions, assertFragmentPmPositions } from './pm-position-validation.js'; import { applySdtContainerStyling, getSdtContainerKey, type SdtBoundaryOptions } from './utils/sdt-helpers.js'; -import { - generateRulerDefinitionFromPx, - createRulerElement, - ensureRulerStyles, - RULER_CLASS_NAMES, -} from './ruler/index.js'; +import { generateRulerDefinitionFromPx, createRulerElement, ensureRulerStyles } from './ruler/index.js'; import { toCssFontFamily } from '@superdoc/font-utils'; import { hashParagraphBorders, @@ -1639,15 +1634,18 @@ export class DomPainter { pageNumberText: page.numberText, }; - // Separate behindDoc fragments (zIndex === 0) from normal fragments. - // behindDoc fragments need to render behind body content, so they must be - // placed directly on the page (not in the header container) with negative z-index. + // Separate behindDoc fragments from normal fragments. + // Prefer explicit fragment.behindDoc when present. Keep zIndex===0 as a + // compatibility fallback for older layouts that predate explicit metadata. const behindDocFragments: typeof data.fragments = []; const normalFragments: typeof data.fragments = []; for (const fragment of data.fragments) { - const isBehindDoc = - (fragment.kind === 'image' || fragment.kind === 'drawing') && 'zIndex' in fragment && fragment.zIndex === 0; + let isBehindDoc = false; + if (fragment.kind === 'image' || fragment.kind === 'drawing') { + isBehindDoc = + fragment.behindDoc === true || (fragment.behindDoc == null && 'zIndex' in fragment && fragment.zIndex === 0); + } if (isBehindDoc) { behindDocFragments.push(fragment); } else { diff --git a/packages/layout-engine/painters/dom/src/styles.ts b/packages/layout-engine/painters/dom/src/styles.ts index 0ea2ce6e88..b61016001a 100644 --- a/packages/layout-engine/painters/dom/src/styles.ts +++ b/packages/layout-engine/painters/dom/src/styles.ts @@ -76,6 +76,14 @@ export const fragmentStyles: Partial = { boxSizing: 'border-box', }; +/** + * Z-index for paragraph text lines, ensuring they render above floating + * OOXML-derived fragments (which typically range 1–10 000). Must stay + * below browser-UI layers (tooltips, menus, overlays) that may live in the + * same stacking context. + */ +const TEXT_LINE_Z_INDEX = '100000'; + export const lineStyles = (lineHeight: number): Partial => ({ lineHeight: `${lineHeight}px`, height: `${lineHeight}px`, @@ -87,7 +95,7 @@ export const lineStyles = (lineHeight: number): Partial => // provides defense-in-depth against any remaining sub-pixel rendering // differences between measurement and display. overflow: 'visible', - zIndex: '10', + zIndex: TEXT_LINE_Z_INDEX, }); const PRINT_STYLES = ` diff --git a/packages/layout-engine/painters/dom/src/table/renderTableCell.ts b/packages/layout-engine/painters/dom/src/table/renderTableCell.ts index 96ad375ba9..c694c4d7cf 100644 --- a/packages/layout-engine/painters/dom/src/table/renderTableCell.ts +++ b/packages/layout-engine/painters/dom/src/table/renderTableCell.ts @@ -29,6 +29,7 @@ import { getSdtContainerKey, type SdtBoundaryOptions, } from '../utils/sdt-helpers.js'; +import { normalizeZIndex } from '@superdoc/pm-adapter/utilities.js'; /** * Default gap between list marker and text content in pixels. @@ -1063,11 +1064,9 @@ export const renderTableCell = (deps: TableCellRenderDependencies): TableCellRen const behindDoc = anchor.behindDoc === true || (anchoredBlock.wrap?.type === 'None' && anchoredBlock.wrap?.behindDoc); const zIndex = - anchoredBlock.kind === 'drawing' && typeof anchoredBlock.zIndex === 'number' + typeof anchoredBlock.zIndex === 'number' ? anchoredBlock.zIndex - : behindDoc - ? -1 - : 1; + : (normalizeZIndex(anchoredBlock.attrs?.originalAttributes) ?? (behindDoc ? -1 : 1)); const wrap = anchoredBlock.wrap; if (!behindDoc && wrap?.type === 'Square') { diff --git a/packages/layout-engine/pm-adapter/src/converters/image.test.ts b/packages/layout-engine/pm-adapter/src/converters/image.test.ts index 89c81f6595..828756e8cf 100644 --- a/packages/layout-engine/pm-adapter/src/converters/image.test.ts +++ b/packages/layout-engine/pm-adapter/src/converters/image.test.ts @@ -443,6 +443,95 @@ describe('image converter', () => { expect(result.attrs?.pmEnd).toBe(20); }); + describe('zIndex from originalAttributes.relativeHeight', () => { + const OOXML_BASE = 251658240; + + it('sets zIndex when originalAttributes.relativeHeight is a number', () => { + const node: PMNode = { + type: 'image', + attrs: { + src: 'image.jpg', + anchorData: { isAnchored: true }, + originalAttributes: { relativeHeight: OOXML_BASE + 10 }, + }, + }; + + const result = imageNodeToBlock(node, mockBlockIdGenerator, mockPositionMap) as ImageBlock; + + expect(result.zIndex).toBe(10); + }); + + it('sets zIndex when originalAttributes.relativeHeight is a string (OOXML)', () => { + const node: PMNode = { + type: 'image', + attrs: { + src: 'image.jpg', + anchorData: { isAnchored: true }, + originalAttributes: { relativeHeight: '251658291' }, + }, + }; + + const result = imageNodeToBlock(node, mockBlockIdGenerator, mockPositionMap) as ImageBlock; + + expect(result.zIndex).toBe(51); + }); + + it('sets zIndex to 0 when anchor.behindDoc is true and no relativeHeight', () => { + const node: PMNode = { + type: 'image', + attrs: { + src: 'image.jpg', + anchorData: { isAnchored: true, behindDoc: true }, + }, + }; + + const result = imageNodeToBlock(node, mockBlockIdGenerator, mockPositionMap) as ImageBlock; + + expect(result.zIndex).toBe(0); + }); + + it('forces zIndex to 0 when behindDoc is true even with relativeHeight', () => { + const node: PMNode = { + type: 'image', + attrs: { + src: 'image.jpg', + anchorData: { isAnchored: true, behindDoc: true }, + originalAttributes: { relativeHeight: OOXML_BASE + 10 }, + }, + }; + + const result = imageNodeToBlock(node, mockBlockIdGenerator, mockPositionMap) as ImageBlock; + + expect(result.zIndex).toBe(0); + }); + + it('clamps base relativeHeight to 1 when not behindDoc', () => { + const node: PMNode = { + type: 'image', + attrs: { + src: 'image.jpg', + anchorData: { isAnchored: true, behindDoc: false }, + originalAttributes: { relativeHeight: OOXML_BASE }, + }, + }; + + const result = imageNodeToBlock(node, mockBlockIdGenerator, mockPositionMap) as ImageBlock; + + expect(result.zIndex).toBe(1); + }); + + it('sets zIndex to 1 when no originalAttributes and not behindDoc (default stacking)', () => { + const node: PMNode = { + type: 'image', + attrs: { src: 'image.jpg' }, + }; + + const result = imageNodeToBlock(node, mockBlockIdGenerator, mockPositionMap) as ImageBlock; + + expect(result.zIndex).toBe(1); + }); + }); + it('validates and filters invalid wrap type', () => { const node: PMNode = { type: 'image', diff --git a/packages/layout-engine/pm-adapter/src/converters/image.ts b/packages/layout-engine/pm-adapter/src/converters/image.ts index 566ef3c0f5..c140a0feb7 100644 --- a/packages/layout-engine/pm-adapter/src/converters/image.ts +++ b/packages/layout-engine/pm-adapter/src/converters/image.ts @@ -8,7 +8,7 @@ import type { ImageBlock, BoxSpacing, ImageAnchor } from '@superdoc/contracts'; import type { PMNode, BlockIdGenerator, PositionMap, NodeHandlerContext, TrackedChangesConfig } from '../types.js'; import { collectTrackedChangeFromMarks } from '../marks/index.js'; import { shouldHideTrackedNode, annotateBlockWithTrackedChange } from '../tracked-changes.js'; -import { isFiniteNumber, pickNumber } from '../utilities.js'; +import { isFiniteNumber, pickNumber, normalizeZIndex, resolveFloatingZIndex } from '../utilities.js'; // ============================================================================ // Constants @@ -268,6 +268,10 @@ export function imageNodeToBlock( ? 'contain' : 'contain'; + // Same z-index as editor: from OOXML relativeHeight (Math.max(0, relativeHeight - OOXML_Z_INDEX_BASE)) + const zIndexFromRelativeHeight = normalizeZIndex(attrs.originalAttributes as Record | undefined); + const zIndex = resolveFloatingZIndex(anchor?.behindDoc === true, zIndexFromRelativeHeight); + return { kind: 'image', id: nextBlockId('image'), @@ -282,6 +286,7 @@ export function imageNodeToBlock( margin: toBoxSpacing(attrs.marginOffset as Record | undefined), anchor, wrap: normalizedWrap, + ...(zIndex !== undefined && { zIndex }), attrs: attrsWithPm, // VML image adjustments for watermark effects gain: typeof attrs.gain === 'string' || typeof attrs.gain === 'number' ? attrs.gain : undefined, diff --git a/packages/layout-engine/pm-adapter/src/converters/shapes.test.ts b/packages/layout-engine/pm-adapter/src/converters/shapes.test.ts index a224c10933..4563963ddd 100644 --- a/packages/layout-engine/pm-adapter/src/converters/shapes.test.ts +++ b/packages/layout-engine/pm-adapter/src/converters/shapes.test.ts @@ -221,6 +221,22 @@ describe('shapes converter', () => { expect(result.zIndex).toBe(10); }); + it('forces zIndex to 0 when behindDoc is true even with relativeHeight', () => { + const node: PMNode = { + type: 'vectorShape', + attrs: { + width: 100, + height: 100, + anchorData: { isAnchored: true, behindDoc: true }, + originalAttributes: { relativeHeight: 251658250 }, + }, + }; + + const result = vectorShapeNodeToDrawingBlock(node, mockBlockIdGenerator, mockPositionMap) as DrawingBlock; + + expect(result.zIndex).toBe(0); + }); + it('includes PM positions in attrs when available', () => { const node: PMNode = { type: 'vectorShape', diff --git a/packages/layout-engine/pm-adapter/src/converters/shapes.ts b/packages/layout-engine/pm-adapter/src/converters/shapes.ts index 71625751de..ff30f11e51 100644 --- a/packages/layout-engine/pm-adapter/src/converters/shapes.ts +++ b/packages/layout-engine/pm-adapter/src/converters/shapes.ts @@ -28,6 +28,7 @@ import { normalizeTextVerticalAlign, normalizeTextInsets, normalizeZIndex, + resolveFloatingZIndex, } from '../utilities.js'; // ============================================================================ @@ -337,9 +338,10 @@ export const buildDrawingBlock = ( attrsWithPm.pmEnd = pos.end; } + const behindDoc = baseAnchor?.behindDoc === true || normalizedWrap?.behindDoc === true; // Try to get zIndex from relativeHeight first, fallback to direct zIndex attribute const zIndexFromRelativeHeight = normalizeZIndex(rawAttrs.originalAttributes); - const finalZIndex = zIndexFromRelativeHeight ?? coerceNumber(rawAttrs.zIndex); + const resolvedZIndex = resolveFloatingZIndex(behindDoc, zIndexFromRelativeHeight, coerceNumber(rawAttrs.zIndex) ?? 1); return { kind: 'drawing', @@ -351,7 +353,7 @@ export const buildDrawingBlock = ( toBoxSpacing(rawAttrs.margin as Record | undefined), anchor: baseAnchor, wrap: normalizedWrap, - zIndex: finalZIndex, + zIndex: resolvedZIndex, drawingContentId: typeof rawAttrs.drawingContentId === 'string' ? rawAttrs.drawingContentId : undefined, drawingContent: toDrawingContentSnapshot(rawAttrs.drawingContent), attrs: attrsWithPm, diff --git a/packages/layout-engine/pm-adapter/src/utilities.test.ts b/packages/layout-engine/pm-adapter/src/utilities.test.ts index b5a3a4d4e4..7e9fad8390 100644 --- a/packages/layout-engine/pm-adapter/src/utilities.test.ts +++ b/packages/layout-engine/pm-adapter/src/utilities.test.ts @@ -31,6 +31,11 @@ import { normalizeShapeGroupChildren, normalizeLineEnds, normalizeEffectExtent, + coerceRelativeHeight, + normalizeZIndex, + getFragmentZIndex, + resolveFloatingZIndex, + OOXML_Z_INDEX_BASE, } from './utilities.js'; // ============================================================================ @@ -1492,3 +1497,193 @@ describe('normalizeEffectExtent', () => { expect(result).toEqual({ left: 0, top: 0, right: 0, bottom: 10 }); }); }); + +// ============================================================================ +// Z-Index Utilities (OOXML relativeHeight) +// ============================================================================ + +describe('z-index utilities', () => { + describe('coerceRelativeHeight', () => { + it('returns number when given a finite number', () => { + expect(coerceRelativeHeight(251658240)).toBe(251658240); + expect(coerceRelativeHeight(0)).toBe(0); + }); + + it('returns number when given a numeric string', () => { + expect(coerceRelativeHeight('251658240')).toBe(251658240); + expect(coerceRelativeHeight('251659318')).toBe(251659318); + }); + + it('returns undefined for non-finite number', () => { + expect(coerceRelativeHeight(NaN)).toBeUndefined(); + expect(coerceRelativeHeight(Infinity)).toBeUndefined(); + }); + + it('returns undefined for empty or invalid string', () => { + expect(coerceRelativeHeight('')).toBeUndefined(); + expect(coerceRelativeHeight(' ')).toBeUndefined(); + expect(coerceRelativeHeight('abc')).toBeUndefined(); + }); + + it('returns undefined for null, undefined, or non-number/string', () => { + expect(coerceRelativeHeight(null)).toBeUndefined(); + expect(coerceRelativeHeight(undefined)).toBeUndefined(); + expect(coerceRelativeHeight({})).toBeUndefined(); + }); + }); + + describe('normalizeZIndex', () => { + it('returns 0 for OOXML base relativeHeight', () => { + expect(normalizeZIndex({ relativeHeight: OOXML_Z_INDEX_BASE })).toBe(0); + expect(normalizeZIndex({ relativeHeight: '251658240' })).toBe(0); + }); + + it('returns positive z-index for relativeHeight above base', () => { + expect(normalizeZIndex({ relativeHeight: OOXML_Z_INDEX_BASE + 2 })).toBe(2); + expect(normalizeZIndex({ relativeHeight: OOXML_Z_INDEX_BASE + 51 })).toBe(51); + expect(normalizeZIndex({ relativeHeight: '251658291' })).toBe(51); + }); + + it('returns undefined when relativeHeight is missing or invalid', () => { + expect(normalizeZIndex({})).toBeUndefined(); + expect(normalizeZIndex(null)).toBeUndefined(); + expect(normalizeZIndex(undefined)).toBeUndefined(); + expect(normalizeZIndex({ relativeHeight: '' })).toBeUndefined(); + }); + }); + + describe('resolveFloatingZIndex', () => { + it('returns 0 when behindDoc is true', () => { + expect(resolveFloatingZIndex(true, 42)).toBe(0); + expect(resolveFloatingZIndex(true, undefined)).toBe(0); + expect(resolveFloatingZIndex(true, 0)).toBe(0); + }); + + it('returns raw value when non-behindDoc and raw >= 1', () => { + expect(resolveFloatingZIndex(false, 5)).toBe(5); + expect(resolveFloatingZIndex(false, 100)).toBe(100); + }); + + it('clamps raw 0 to 1 for non-behindDoc', () => { + expect(resolveFloatingZIndex(false, 0)).toBe(1); + }); + + it('returns fallback when raw is undefined', () => { + expect(resolveFloatingZIndex(false, undefined)).toBe(1); + expect(resolveFloatingZIndex(false, undefined, 5)).toBe(5); + }); + + it('clamps fallback to at least 1', () => { + expect(resolveFloatingZIndex(false, undefined, 0)).toBe(1); + expect(resolveFloatingZIndex(false, undefined, -1)).toBe(1); + }); + }); + + describe('getFragmentZIndex', () => { + it('uses block.zIndex when set', () => { + const block = { + kind: 'image' as const, + id: 'img-1', + src: 'x.png', + zIndex: 42, + attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE } }, + }; + expect(getFragmentZIndex(block)).toBe(42); + }); + + it('derives z-index from attrs.originalAttributes.relativeHeight (number)', () => { + const block = { + kind: 'image' as const, + id: 'img-1', + src: 'x.png', + attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE + 10 } }, + }; + expect(getFragmentZIndex(block)).toBe(10); + }); + + it('derives z-index from attrs.originalAttributes.relativeHeight (string)', () => { + const block = { + kind: 'image' as const, + id: 'img-1', + src: 'x.png', + attrs: { originalAttributes: { relativeHeight: '251658250' } }, + }; + expect(getFragmentZIndex(block)).toBe(10); + }); + + it('preserves high z-index for wrapped anchored objects', () => { + const block = { + kind: 'image' as const, + id: 'img-1', + src: 'x.png', + anchor: { isAnchored: true, behindDoc: false }, + wrap: { type: 'Through' as const }, + zIndex: 7168, + }; + expect(getFragmentZIndex(block)).toBe(7168); + }); + + it('preserves relativeHeight z-index for wrap None anchored objects', () => { + const block = { + kind: 'image' as const, + id: 'img-1', + src: 'x.png', + anchor: { isAnchored: true, behindDoc: false }, + wrap: { type: 'None' as const }, + attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE + 10 } }, + }; + expect(getFragmentZIndex(block)).toBe(10); + }); + + it('returns 0 when anchor.behindDoc is true and no zIndex/originalAttributes', () => { + const block = { + kind: 'image' as const, + id: 'img-1', + src: 'x.png', + anchor: { isAnchored: true, behindDoc: true }, + }; + expect(getFragmentZIndex(block)).toBe(0); + }); + + it('returns 1 when not behindDoc and no zIndex/originalAttributes', () => { + const block = { + kind: 'image' as const, + id: 'img-1', + src: 'x.png', + }; + expect(getFragmentZIndex(block)).toBe(1); + }); + + it('does not treat base relativeHeight as behindDoc when behindDoc is false', () => { + const block = { + kind: 'image' as const, + id: 'img-1', + src: 'x.png', + anchor: { isAnchored: true, behindDoc: false }, + attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE } }, + }; + expect(getFragmentZIndex(block)).toBeGreaterThan(0); + }); + + it('forces behindDoc fragments to zIndex 0 even with relativeHeight', () => { + const block = { + kind: 'image' as const, + id: 'img-1', + src: 'x.png', + anchor: { isAnchored: true, behindDoc: true }, + attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE + 5 } }, + }; + expect(getFragmentZIndex(block)).toBe(0); + }); + + it('works for drawing blocks', () => { + const block = { + kind: 'drawing' as const, + id: 'd-1', + drawingKind: 'vectorShape' as const, + attrs: { originalAttributes: { relativeHeight: OOXML_Z_INDEX_BASE + 5 } }, + }; + expect(getFragmentZIndex(block)).toBe(5); + }); + }); +}); diff --git a/packages/layout-engine/pm-adapter/src/utilities.ts b/packages/layout-engine/pm-adapter/src/utilities.ts index ae7411d983..73ea7b3c6c 100644 --- a/packages/layout-engine/pm-adapter/src/utilities.ts +++ b/packages/layout-engine/pm-adapter/src/utilities.ts @@ -10,7 +10,6 @@ import type { DrawingBlock, DrawingContentSnapshot, ImageBlock, - ParagraphIndent, ShapeGroupChild, ShapeGroupDrawing, ShapeGroupImageChild, @@ -1438,9 +1437,21 @@ export function normalizeTextInsets( export const OOXML_Z_INDEX_BASE = 251658240; // ============================================================================ -// OOXML Element Utilities +// OOXML Element Utilities (z-index from relativeHeight) // ============================================================================ +/** + * Coerces relativeHeight from OOXML (number or string) to a finite number. + */ +export function coerceRelativeHeight(raw: unknown): number | undefined { + if (typeof raw === 'number' && Number.isFinite(raw)) return raw; + if (typeof raw === 'string' && raw.trim() !== '') { + const n = Number(raw); + if (Number.isFinite(n)) return n; + } + return undefined; +} + /** * Normalizes z-index from OOXML relativeHeight value. * @@ -1463,8 +1474,50 @@ export const OOXML_Z_INDEX_BASE = 251658240; */ export function normalizeZIndex(originalAttributes: unknown): number | undefined { if (!isPlainObject(originalAttributes)) return undefined; - const relativeHeight = originalAttributes.relativeHeight; - if (typeof relativeHeight !== 'number') return undefined; - // Subtract base to get relative z-index, ensuring non-negative values + const relativeHeight = coerceRelativeHeight(originalAttributes.relativeHeight); + if (relativeHeight === undefined) return undefined; return Math.max(0, relativeHeight - OOXML_Z_INDEX_BASE); } + +/** + * Resolves the CSS z-index for a floating object based on its behindDoc flag + * and an OOXML-derived raw value. + * + * - behindDoc objects always return 0. + * - Non-behindDoc objects are clamped to at least 1 so they never share the + * behindDoc sentinel value (0). + * + * @param behindDoc - Whether the object is behind body text + * @param raw - OOXML-derived z-index (from normalizeZIndex or block.zIndex) + * @param fallback - Value to use when raw is undefined (default: 1) + * @returns Resolved z-index + */ +export function resolveFloatingZIndex(behindDoc: boolean, raw: number | undefined, fallback = 1): number { + if (behindDoc) return 0; + if (raw === undefined) return Math.max(1, fallback); + return Math.max(1, raw); +} + +/** + * Returns z-index for an image or drawing block. + * + * We cannot rely on `block.zIndex` only: when the flow-block cache hits, the + * paragraph handler reuses cached blocks and never calls the image/shape + * converters, so those blocks never get `zIndex` set. This helper uses + * `block.zIndex` when present, otherwise derives from + * `block.attrs.originalAttributes.relativeHeight` via normalizeZIndex, + * otherwise behindDoc ? 0 : 1. + * + * Rendering policy: + * - behindDoc anchored objects always return 0. + * - Anchored objects with text wrapping (Square/Tight/Through/TopAndBottom, or + * missing wrap metadata) keep OOXML relativeHeight ordering but are clamped + * to at least 1 (never 0 unless behindDoc=true). + * - Front/no-wrap anchored objects (wrap None) also preserve OOXML relativeHeight order. + */ +export function getFragmentZIndex(block: ImageBlock | DrawingBlock): number { + const attrs = block.attrs as { originalAttributes?: unknown } | undefined; + const raw = typeof block.zIndex === 'number' ? block.zIndex : normalizeZIndex(attrs?.originalAttributes); + + return resolveFloatingZIndex(block.anchor?.behindDoc === true, raw); +} diff --git a/packages/super-editor/src/extensions/image/image.js b/packages/super-editor/src/extensions/image/image.js index 742c56eb61..738cb3741e 100644 --- a/packages/super-editor/src/extensions/image/image.js +++ b/packages/super-editor/src/extensions/image/image.js @@ -4,6 +4,7 @@ import { ImagePositionPlugin } from './imageHelpers/imagePositionPlugin.js'; import { getNormalizedImageAttrs } from './imageHelpers/legacyAttributes.js'; import { getRotationMargins } from './imageHelpers/rotation.js'; import { inchesToPixels } from '@converter/helpers.js'; +import { OOXML_Z_INDEX_BASE } from '@extensions/shared/constants.js'; /** * Configuration options for Image @@ -142,7 +143,13 @@ export const Image = Node.create({ anchorData: { default: null, - rendered: false, + renderDOM: ({ anchorData, originalAttributes }) => { + const relativeHeight = originalAttributes?.relativeHeight; + if (anchorData && relativeHeight) { + const zIndex = Math.max(0, relativeHeight - OOXML_Z_INDEX_BASE); + return { style: `position:relative; z-index: ${zIndex}` }; + } + }, }, isAnchor: { rendered: false }, diff --git a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js index 47b40fa4d4..00b4d9bdd8 100644 --- a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js +++ b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.js @@ -166,7 +166,13 @@ function segmentRunByInlineProps(runNode, paragraphNode, $pos, editor) { runNode.forEach((child) => { if (child.isText) { - const { inlineProps, inlineKey } = computeInlineRunProps(child.marks, runNode.attrs?.runProperties, paragraphNode, $pos, editor); + const { inlineProps, inlineKey } = computeInlineRunProps( + child.marks, + runNode.attrs?.runProperties, + paragraphNode, + $pos, + editor, + ); const last = segments[segments.length - 1]; if (last && inlineKey === lastKey) { last.content.push(child); @@ -206,7 +212,7 @@ function computeInlineRunProps(marks, existingRunProperties, paragraphNode, $pos translatedNumbering: editor.converter?.translatedNumbering ?? {}, translatedLinkedStyles: editor.converter?.translatedLinkedStyles ?? {}, }, - existingRunProperties?.styleId != null ? {styleId: existingRunProperties?.styleId} : {}, + existingRunProperties?.styleId != null ? { styleId: existingRunProperties?.styleId } : {}, paragraphProperties, false, Boolean(paragraphNode.attrs.paragraphProperties?.numberingProperties), diff --git a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js index 7ad9b1a332..c613edaf37 100644 --- a/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js +++ b/packages/super-editor/src/extensions/run/calculateInlineRunPropertiesPlugin.test.js @@ -68,9 +68,7 @@ const makeSchema = () => }); const paragraphDoc = (schema, runAttrs = null, marks = [], text = 'Hello') => - schema.node('doc', null, [ - schema.node('paragraph', null, [schema.node('run', runAttrs, schema.text(text, marks))]), - ]); + schema.node('doc', null, [schema.node('paragraph', null, [schema.node('run', runAttrs, schema.text(text, marks))])]); const runPos = (doc) => { let pos = null; diff --git a/packages/super-editor/src/extensions/run/run.js b/packages/super-editor/src/extensions/run/run.js index d639a7dd6d..75c4eb3e43 100644 --- a/packages/super-editor/src/extensions/run/run.js +++ b/packages/super-editor/src/extensions/run/run.js @@ -69,10 +69,6 @@ export const Run = OxmlNode.create({ return ['span', base, 0]; }, addPmPlugins() { - return [ - wrapTextInRunsPlugin(this.editor), - calculateInlineRunPropertiesPlugin(this.editor), - cleanupEmptyRunsPlugin, - ]; + return [wrapTextInRunsPlugin(this.editor), calculateInlineRunPropertiesPlugin(this.editor), cleanupEmptyRunsPlugin]; }, }); diff --git a/packages/super-editor/src/extensions/shared/constants.js b/packages/super-editor/src/extensions/shared/constants.js new file mode 100644 index 0000000000..005a59bc4f --- /dev/null +++ b/packages/super-editor/src/extensions/shared/constants.js @@ -0,0 +1 @@ +export const OOXML_Z_INDEX_BASE = 251658240; diff --git a/packages/super-editor/src/extensions/tests/headless.test.js b/packages/super-editor/src/extensions/tests/headless.test.js index 2e74c0b505..6c8d4d9282 100644 --- a/packages/super-editor/src/extensions/tests/headless.test.js +++ b/packages/super-editor/src/extensions/tests/headless.test.js @@ -37,7 +37,9 @@ const hasInvalidParagraphRangeError = (calls) => calls.some((args) => args.some( (value) => - (value instanceof RangeError && typeof value.message === 'string' && value.message.includes(INVALID_PARAGRAPH_RANGE_ERROR)) || + (value instanceof RangeError && + typeof value.message === 'string' && + value.message.includes(INVALID_PARAGRAPH_RANGE_ERROR)) || (typeof value === 'string' && value.includes(INVALID_PARAGRAPH_RANGE_ERROR)), ), ); @@ -145,7 +147,9 @@ describe('Headless Mode Optimization', () => { const testDoc = doc.create(null, [ paragraph.create(null, [ - pageReference.create({ instruction: 'PAGEREF _Toc123456789 h' }, [run.create(null, [editor.schema.text('Ref')])]), + pageReference.create({ instruction: 'PAGEREF _Toc123456789 h' }, [ + run.create(null, [editor.schema.text('Ref')]), + ]), run.create(null, [editor.schema.text(' tail')]), ]), ]); diff --git a/packages/super-editor/src/extensions/vector-shape/vector-shape.js b/packages/super-editor/src/extensions/vector-shape/vector-shape.js index 72cbcea503..ed7a38feab 100644 --- a/packages/super-editor/src/extensions/vector-shape/vector-shape.js +++ b/packages/super-editor/src/extensions/vector-shape/vector-shape.js @@ -1,5 +1,6 @@ import { Node, Attribute } from '@core/index'; import { VectorShapeView } from './VectorShapeView'; +import { OOXML_Z_INDEX_BASE } from '@extensions/shared/constants.js'; export const VectorShape = Node.create({ name: 'vectorShape', @@ -112,7 +113,13 @@ export const VectorShape = Node.create({ anchorData: { default: null, - rendered: false, + renderDOM: ({ anchorData, originalAttributes }) => { + const relativeHeight = originalAttributes?.relativeHeight; + if (anchorData && relativeHeight) { + const zIndex = Math.max(0, relativeHeight - OOXML_Z_INDEX_BASE); + return { style: `z-index: ${zIndex}` }; + } + }, }, isAnchor: { diff --git a/packages/super-editor/src/tests/import-export/ooxml-italic-rstyle-combos-roundtrip.test.js b/packages/super-editor/src/tests/import-export/ooxml-italic-rstyle-combos-roundtrip.test.js index daa035ea70..5e8f63bb0a 100644 --- a/packages/super-editor/src/tests/import-export/ooxml-italic-rstyle-combos-roundtrip.test.js +++ b/packages/super-editor/src/tests/import-export/ooxml-italic-rstyle-combos-roundtrip.test.js @@ -54,8 +54,7 @@ const collectExpectedRunsFromImport = async (fileName, italicStyleSet) => { } const runProperties = runNode?.attrs?.runProperties; - const hasInlineItalic = - runProperties != null && Object.prototype.hasOwnProperty.call(runProperties, 'italic'); + const hasInlineItalic = runProperties != null && Object.prototype.hasOwnProperty.call(runProperties, 'italic'); let italic; if (hasInlineItalic) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4fc4a832b..de8e483eb2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -754,6 +754,9 @@ importers: '@superdoc/contracts': specifier: workspace:* version: link:../contracts + '@superdoc/pm-adapter': + specifier: workspace:* + version: link:../pm-adapter packages/layout-engine/measuring/dom: dependencies: @@ -781,6 +784,9 @@ importers: '@superdoc/font-utils': specifier: workspace:* version: link:../../../../shared/font-utils + '@superdoc/pm-adapter': + specifier: workspace:* + version: link:../../pm-adapter '@superdoc/preset-geometry': specifier: workspace:* version: link:../../../preset-geometry @@ -10574,7 +10580,7 @@ snapshots: ajv-errors: 3.0.0(ajv@8.17.1) ajv-formats: 2.1.1(ajv@8.17.1) avsc: 5.7.9 - js-yaml: 4.1.0 + js-yaml: 4.1.1 jsonpath-plus: 10.3.0 node-fetch: 2.6.7 transitivePeerDependencies: @@ -11767,7 +11773,7 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdx': 2.0.13 - acorn: 8.11.2 + acorn: 8.15.0 collapse-white-space: 2.1.0 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 @@ -11776,7 +11782,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.6 markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 - recma-jsx: 1.0.1(acorn@8.11.2) + recma-jsx: 1.0.1(acorn@8.15.0) recma-stringify: 1.0.0 rehype-recma: 1.0.0 remark-mdx: 3.1.0 @@ -11786,7 +11792,7 @@ snapshots: unified: 11.0.5 unist-util-position-from-estree: 2.0.0 unist-util-stringify-position: 4.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 transitivePeerDependencies: - supports-color @@ -12063,12 +12069,12 @@ snapshots: react: 19.2.3 react-dom: 19.2.0(react@19.2.3) rehype-katex: 7.0.1 - remark-gfm: 4.0.0 + remark-gfm: 4.0.1 remark-math: 6.0.0 remark-smartypants: 3.0.2 shiki: 3.22.0 unified: 11.0.5 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 transitivePeerDependencies: - '@types/react' - supports-color @@ -12400,7 +12406,7 @@ snapshots: extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 - semver: 7.7.2 + semver: 7.7.3 tar-fs: 3.1.1 unbzip2-stream: 1.4.3 yargs: 17.7.2 @@ -12964,7 +12970,7 @@ snapshots: dependency-graph: 0.11.0 fast-memoize: 2.5.2 immer: 9.0.21 - lodash: 4.17.21 + lodash: 4.17.23 tslib: 2.8.1 urijs: 1.19.11 @@ -12974,7 +12980,7 @@ snapshots: '@stoplight/path': 1.3.2 '@stoplight/types': 13.20.0 jsonc-parser: 2.2.1 - lodash: 4.17.21 + lodash: 4.17.23 safe-stable-stringify: 1.1.1 '@stoplight/ordered-object-literal@1.0.5': {} @@ -13027,7 +13033,7 @@ snapshots: ajv-draft-04: 1.0.0(ajv@8.17.1) ajv-errors: 3.0.0(ajv@8.17.1) ajv-formats: 2.1.1(ajv@8.17.1) - lodash: 4.17.21 + lodash: 4.17.23 tslib: 2.8.1 transitivePeerDependencies: - encoding @@ -13055,7 +13061,7 @@ snapshots: '@stoplight/path': 1.3.2 '@stoplight/types': 13.20.0 abort-controller: 3.0.0 - lodash: 4.17.21 + lodash: 4.17.23 node-fetch: 2.7.0 tslib: 2.8.1 transitivePeerDependencies: @@ -15452,7 +15458,7 @@ snapshots: esast-util-from-js@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - acorn: 8.11.2 + acorn: 8.15.0 esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 @@ -16652,7 +16658,7 @@ snapshots: rehype-minify-whitespace: 6.0.2 trim-trailing-lines: 2.1.0 unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 hast-util-to-mdast@10.1.2: dependencies: @@ -18302,8 +18308,8 @@ snapshots: micromark-extension-mdxjs@3.0.0: dependencies: - acorn: 8.11.2 - acorn-jsx: 5.3.2(acorn@8.11.2) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) micromark-extension-mdx-expression: 3.0.1 micromark-extension-mdx-jsx: 3.0.1 micromark-extension-mdx-md: 2.0.0 @@ -19855,10 +19861,10 @@ snapshots: estree-util-build-jsx: 3.0.1 vfile: 6.0.3 - recma-jsx@1.0.1(acorn@8.11.2): + recma-jsx@1.0.1(acorn@8.15.0): dependencies: - acorn: 8.11.2 - acorn-jsx: 5.3.2(acorn@8.11.2) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) estree-util-to-js: 2.0.0 recma-parse: 1.0.0 recma-stringify: 1.0.0 @@ -19928,7 +19934,7 @@ snapshots: hast-util-from-html-isomorphic: 2.0.0 hast-util-to-text: 4.0.2 katex: 0.16.28 - unist-util-visit-parents: 6.0.1 + unist-util-visit-parents: 6.0.2 vfile: 6.0.3 rehype-minify-whitespace@6.0.2: @@ -19961,7 +19967,7 @@ snapshots: rehype-stringify@10.0.1: dependencies: '@types/hast': 3.0.4 - hast-util-to-html: 9.0.4 + hast-util-to-html: 9.0.5 unified: 11.0.5 remark-frontmatter@5.0.0: @@ -19985,7 +19991,7 @@ snapshots: remark-gfm@4.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-gfm: 3.0.0 + mdast-util-gfm: 3.1.0 micromark-extension-gfm: 3.0.0 remark-parse: 11.0.0 remark-stringify: 11.0.0 @@ -20080,7 +20086,7 @@ snapshots: retext: 9.0.0 retext-smartypants: 6.2.0 unified: 11.0.5 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 remark-stringify@10.0.3: dependencies: @@ -20165,7 +20171,7 @@ snapshots: dependencies: '@types/nlcst': 2.0.3 nlcst-to-string: 4.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 retext-stringify@4.0.0: dependencies: @@ -20476,7 +20482,7 @@ snapshots: dependencies: color: 4.2.3 detect-libc: 2.1.2 - semver: 7.7.2 + semver: 7.7.3 optionalDependencies: '@img/sharp-darwin-arm64': 0.33.5 '@img/sharp-darwin-x64': 0.33.5 @@ -21382,13 +21388,13 @@ snapshots: unist-util-remove-position@5.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 unist-util-remove@4.0.0: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.1 + unist-util-visit-parents: 6.0.2 unist-util-stringify-position@3.0.3: dependencies: