From 54a7bd430594afc733d0b058e9aab0616da7cd4b Mon Sep 17 00:00:00 2001 From: VladaHarbour Date: Thu, 5 Feb 2026 17:54:40 +0200 Subject: [PATCH 1/2] fix: image z-index --- packages/layout-engine/contracts/src/index.ts | 2 + .../layout-engine/layout-engine/package.json | 3 +- .../layout-engine/layout-engine/src/index.ts | 3 +- .../layout-engine/src/layout-drawing.ts | 3 +- .../layout-engine/src/layout-paragraph.ts | 5 +- .../layout-engine/painters/dom/package.json | 1 + .../painters/dom/src/table/renderTableCell.ts | 7 +- .../pm-adapter/src/converters/image.test.ts | 59 ++++++ .../pm-adapter/src/converters/image.ts | 7 +- .../pm-adapter/src/converters/paragraph.ts | 8 +- .../layout-engine/pm-adapter/src/internal.ts | 1 + .../pm-adapter/src/utilities.test.ts | 121 ++++++++++++ .../layout-engine/pm-adapter/src/utilities.ts | 37 +++- .../src/extensions/image/image.js | 9 +- .../src/extensions/shared/constants.js | 1 + .../extensions/vector-shape/vector-shape.js | 9 +- pnpm-lock.yaml | 181 ++++++++---------- 17 files changed, 334 insertions(+), 123 deletions(-) create mode 100644 packages/super-editor/src/extensions/shared/constants.js diff --git a/packages/layout-engine/contracts/src/index.ts b/packages/layout-engine/contracts/src/index.ts index eaebd6444c..a88b8a2140 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) 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..9990c540a8 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 = { @@ -1956,7 +1957,7 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options width: imgMeasure.width, height: imgMeasure.height, isAnchored: true, - zIndex: imgBlock.anchor?.behindDoc ? 0 : 1, + 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..4aefc73e87 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,7 @@ export function layoutParagraphBlock(ctx: ParagraphLayoutContext, anchors?: Para width: entry.measure.width, height: entry.measure.height, isAnchored: true, - zIndex: entry.block.anchor?.behindDoc ? 0 : 1, + zIndex: getFragmentZIndex(entry.block), metadata, }; if (pmRange.pmStart != null) fragment.pmStart = pmRange.pmStart; @@ -406,7 +407,7 @@ export function layoutParagraphBlock(ctx: ParagraphLayoutContext, anchors?: Para geometry: entry.measure.geometry, scale: entry.measure.scale, isAnchored: true, - zIndex: entry.block.anchor?.behindDoc ? 0 : 1, + zIndex: getFragmentZIndex(entry.block), drawingContentId: entry.block.drawingContentId, }; if (pmRange.pmStart != null) fragment.pmStart = pmRange.pmStart; 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/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..2d9a8a9c09 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,65 @@ 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('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..4614a5fe8a 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 } 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 zIndex = + normalizeZIndex(attrs.originalAttributes as Record | undefined) ?? (anchor?.behindDoc ? 0 : 1); + 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/paragraph.ts b/packages/layout-engine/pm-adapter/src/converters/paragraph.ts index 6e2051a921..331ed38582 100644 --- a/packages/layout-engine/pm-adapter/src/converters/paragraph.ts +++ b/packages/layout-engine/pm-adapter/src/converters/paragraph.ts @@ -660,12 +660,12 @@ export function handleParagraphNode(node: PMNode, context: NodeHandlerContext): reusedBlocks.forEach((block) => { blocks.push(block); - recordBlockKind(block.kind); + recordBlockKind?.(block.kind); }); // Store in next cache generation with current position (reuse nodeJson) flowBlockCache.set(prefixedStableId, nodeJson, nodeRev, reusedBlocks, pmStart); - sectionState.currentParagraphIndex++; + sectionState!.currentParagraphIndex++; return; } @@ -686,12 +686,12 @@ export function handleParagraphNode(node: PMNode, context: NodeHandlerContext): paragraphBlocks.forEach((block) => { blocks.push(block); - recordBlockKind(block.kind); + recordBlockKind?.(block.kind); }); // Store in cache using pre-computed nodeJson (avoids double serialization) flowBlockCache.set(prefixedStableId, nodeJson, nodeRev, paragraphBlocks, pmStart); - sectionState.currentParagraphIndex++; + sectionState!.currentParagraphIndex++; return; } diff --git a/packages/layout-engine/pm-adapter/src/internal.ts b/packages/layout-engine/pm-adapter/src/internal.ts index 1eb1da8f8d..98fd0ed49b 100644 --- a/packages/layout-engine/pm-adapter/src/internal.ts +++ b/packages/layout-engine/pm-adapter/src/internal.ts @@ -246,6 +246,7 @@ export function toFlowBlocksMap(documents: PMDocumentMap, options?: BatchAdapter const prefixFactory = options?.blockIdPrefixFactory; Object.entries(documents).forEach(([key, doc]) => { + if (doc == null) return; const blockIdPrefix = prefixFactory ? prefixFactory(key) : options?.blockIdPrefix; const result = toFlowBlocks(doc, { ...options, blockIdPrefix }); results[key] = result.blocks; diff --git a/packages/layout-engine/pm-adapter/src/utilities.test.ts b/packages/layout-engine/pm-adapter/src/utilities.test.ts index b5a3a4d4e4..87d3e12519 100644 --- a/packages/layout-engine/pm-adapter/src/utilities.test.ts +++ b/packages/layout-engine/pm-adapter/src/utilities.test.ts @@ -31,6 +31,10 @@ import { normalizeShapeGroupChildren, normalizeLineEnds, normalizeEffectExtent, + coerceRelativeHeight, + normalizeZIndex, + getFragmentZIndex, + OOXML_Z_INDEX_BASE, } from './utilities.js'; // ============================================================================ @@ -1492,3 +1496,120 @@ 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('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('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('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..590954b730 100644 --- a/packages/layout-engine/pm-adapter/src/utilities.ts +++ b/packages/layout-engine/pm-adapter/src/utilities.ts @@ -1438,9 +1438,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 +1475,25 @@ 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); } + +/** + * 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. + */ +export function getFragmentZIndex(block: ImageBlock | DrawingBlock): number { + if (typeof block.zIndex === 'number') return block.zIndex; + const attrs = block.attrs as { originalAttributes?: unknown } | undefined; + const z = normalizeZIndex(attrs?.originalAttributes); + if (z !== undefined) return z; + return block.anchor?.behindDoc ? 0 : 1; +} 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/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/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/pnpm-lock.yaml b/pnpm-lock.yaml index 19a1579a28..34d8936020 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -422,7 +422,7 @@ importers: version: 14.0.3 mintlify: specifier: ^4.2.295 - version: 4.2.315(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.2)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3) + version: 4.2.315(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.8)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3) apps/vscode-ext: dependencies: @@ -820,6 +820,9 @@ importers: '@superdoc/contracts': specifier: workspace:* version: link:../contracts + '@superdoc/pm-adapter': + specifier: workspace:* + version: link:../pm-adapter packages/layout-engine/measuring/dom: dependencies: @@ -847,6 +850,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 @@ -9891,14 +9897,6 @@ packages: resolution: {integrity: sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==} engines: {node: '>=18'} - superdoc@1.10.0: - resolution: {integrity: sha512-3I3c5B2ja5HXEpgSesXMiRadJXoWezeYlwpwAvGr8Uh47gCT773LXbL7GrH49xTZCR0lzPvTNyrZRj0Uz+SCUw==} - peerDependencies: - '@hocuspocus/provider': ^2.13.6 - pdfjs-dist: '>=4.3.136 <=4.6.82' - y-prosemirror: ^1.3.7 - yjs: 13.6.19 - supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -12384,128 +12382,128 @@ snapshots: '@inquirer/ansi@1.0.2': {} - '@inquirer/checkbox@4.3.2(@types/node@22.19.2)': + '@inquirer/checkbox@4.3.2(@types/node@22.19.8)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@22.19.2) + '@inquirer/core': 10.3.2(@types/node@22.19.8) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.8) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/confirm@5.1.21(@types/node@22.19.2)': + '@inquirer/confirm@5.1.21(@types/node@22.19.8)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.2) - '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@inquirer/core': 10.3.2(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.8) optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/core@10.3.2(@types/node@22.19.2)': + '@inquirer/core@10.3.2(@types/node@22.19.8)': dependencies: '@inquirer/ansi': 1.0.2 '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.8) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/editor@4.2.23(@types/node@22.19.2)': + '@inquirer/editor@4.2.23(@types/node@22.19.8)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.2) - '@inquirer/external-editor': 1.0.3(@types/node@22.19.2) - '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@inquirer/core': 10.3.2(@types/node@22.19.8) + '@inquirer/external-editor': 1.0.3(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.8) optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/expand@4.0.23(@types/node@22.19.2)': + '@inquirer/expand@4.0.23(@types/node@22.19.8)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.2) - '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@inquirer/core': 10.3.2(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.8) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/external-editor@1.0.3(@types/node@22.19.2)': + '@inquirer/external-editor@1.0.3(@types/node@22.19.8)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 '@inquirer/figures@1.0.15': {} - '@inquirer/input@4.3.1(@types/node@22.19.2)': + '@inquirer/input@4.3.1(@types/node@22.19.8)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.2) - '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@inquirer/core': 10.3.2(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.8) optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/number@3.0.23(@types/node@22.19.2)': + '@inquirer/number@3.0.23(@types/node@22.19.8)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.2) - '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@inquirer/core': 10.3.2(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.8) optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/password@4.0.23(@types/node@22.19.2)': + '@inquirer/password@4.0.23(@types/node@22.19.8)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@22.19.2) - '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@inquirer/core': 10.3.2(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.8) optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/prompts@7.9.0(@types/node@22.19.2)': - dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@22.19.2) - '@inquirer/confirm': 5.1.21(@types/node@22.19.2) - '@inquirer/editor': 4.2.23(@types/node@22.19.2) - '@inquirer/expand': 4.0.23(@types/node@22.19.2) - '@inquirer/input': 4.3.1(@types/node@22.19.2) - '@inquirer/number': 3.0.23(@types/node@22.19.2) - '@inquirer/password': 4.0.23(@types/node@22.19.2) - '@inquirer/rawlist': 4.1.11(@types/node@22.19.2) - '@inquirer/search': 3.2.2(@types/node@22.19.2) - '@inquirer/select': 4.4.2(@types/node@22.19.2) + '@inquirer/prompts@7.9.0(@types/node@22.19.8)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.19.8) + '@inquirer/confirm': 5.1.21(@types/node@22.19.8) + '@inquirer/editor': 4.2.23(@types/node@22.19.8) + '@inquirer/expand': 4.0.23(@types/node@22.19.8) + '@inquirer/input': 4.3.1(@types/node@22.19.8) + '@inquirer/number': 3.0.23(@types/node@22.19.8) + '@inquirer/password': 4.0.23(@types/node@22.19.8) + '@inquirer/rawlist': 4.1.11(@types/node@22.19.8) + '@inquirer/search': 3.2.2(@types/node@22.19.8) + '@inquirer/select': 4.4.2(@types/node@22.19.8) optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/rawlist@4.1.11(@types/node@22.19.2)': + '@inquirer/rawlist@4.1.11(@types/node@22.19.8)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.2) - '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@inquirer/core': 10.3.2(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.8) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/search@3.2.2(@types/node@22.19.2)': + '@inquirer/search@3.2.2(@types/node@22.19.8)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.2) + '@inquirer/core': 10.3.2(@types/node@22.19.8) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.8) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/select@4.4.2(@types/node@22.19.2)': + '@inquirer/select@4.4.2(@types/node@22.19.8)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@22.19.2) + '@inquirer/core': 10.3.2(@types/node@22.19.8) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.8) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 - '@inquirer/type@3.0.10(@types/node@22.19.2)': + '@inquirer/type@3.0.10(@types/node@22.19.8)': optionalDependencies: - '@types/node': 22.19.2 + '@types/node': 22.19.8 '@isaacs/balanced-match@4.0.1': {} @@ -12859,9 +12857,9 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} - '@mintlify/cli@4.0.919(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.2)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3)': + '@mintlify/cli@4.0.919(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.8)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3)': dependencies: - '@inquirer/prompts': 7.9.0(@types/node@22.19.2) + '@inquirer/prompts': 7.9.0(@types/node@22.19.8) '@mintlify/common': 1.0.698(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@mintlify/link-rot': 3.0.857(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@mintlify/models': 0.0.263 @@ -12875,7 +12873,7 @@ snapshots: front-matter: 4.0.2 fs-extra: 11.2.0 ink: 6.3.0(@types/react@19.2.11)(react@19.2.3) - inquirer: 12.3.0(@types/node@22.19.2) + inquirer: 12.3.0(@types/node@22.19.8) js-yaml: 4.1.0 mdast-util-mdx-jsx: 3.2.0 react: 19.2.3 @@ -18342,12 +18340,12 @@ snapshots: inline-style-parser@0.2.7: {} - inquirer@12.3.0(@types/node@22.19.2): + inquirer@12.3.0(@types/node@22.19.8): dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.2) - '@inquirer/prompts': 7.9.0(@types/node@22.19.2) - '@inquirer/type': 3.0.10(@types/node@22.19.2) - '@types/node': 22.19.2 + '@inquirer/core': 10.3.2(@types/node@22.19.8) + '@inquirer/prompts': 7.9.0(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@types/node': 22.19.8 ansi-escapes: 4.3.2 mute-stream: 2.0.0 run-async: 3.0.0 @@ -20053,9 +20051,9 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 - mintlify@4.2.315(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.2)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3): + mintlify@4.2.315(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.8)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3): dependencies: - '@mintlify/cli': 4.0.919(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.2)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3) + '@mintlify/cli': 4.0.919(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.8)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3) transitivePeerDependencies: - '@radix-ui/react-popover' - '@types/node' @@ -22299,29 +22297,6 @@ snapshots: make-asynchronous: 1.0.1 time-span: 5.1.0 - superdoc@1.10.0(@hocuspocus/provider@2.15.3(y-protocols@1.0.7(yjs@13.6.19))(yjs@13.6.19))(canvas@3.2.0)(pdfjs-dist@4.3.136)(typescript@5.9.3)(y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.19))(yjs@13.6.19))(yjs@13.6.19): - dependencies: - '@hocuspocus/provider': 2.15.3(y-protocols@1.0.7(yjs@13.6.19))(yjs@13.6.19) - buffer-crc32: 1.0.0 - eventemitter3: 5.0.4 - jsdom: 27.3.0(canvas@3.2.0) - naive-ui: 2.43.2(vue@3.5.25(typescript@5.9.3)) - pdfjs-dist: 4.3.136 - pinia: 2.3.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)) - rollup-plugin-copy: 3.5.0 - uuid: 9.0.1 - vue: 3.5.25(typescript@5.9.3) - y-prosemirror: 1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.19))(yjs@13.6.19) - y-websocket: 3.0.0(yjs@13.6.19) - yjs: 13.6.19 - transitivePeerDependencies: - - '@vue/composition-api' - - bufferutil - - canvas - - supports-color - - typescript - - utf-8-validate - supports-color@5.5.0: dependencies: has-flag: 3.0.0 From 5d7e82be9070ac0d2b62e65ed0a8c6bf077e0f49 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Thu, 5 Feb 2026 16:40:53 -0800 Subject: [PATCH 2/2] fix: anchored image stacking: explicit behindDoc routing + preserve relativeHeight layering --- .../packages/harness/package.json | 2 +- .../packages/test-helpers/package.json | 3 + .../scripts/compare-interactions.ts | 92 ++++++++- devtools/visual-testing/scripts/compare.ts | 85 ++++++-- packages/layout-engine/contracts/src/index.ts | 2 + .../layout-engine/layout-engine/src/index.ts | 2 +- .../layout-engine/src/layout-paragraph.ts | 3 +- .../painters/dom/src/index.test.ts | 11 +- .../painters/dom/src/renderer.ts | 20 +- .../layout-engine/painters/dom/src/styles.ts | 10 +- .../pm-adapter/src/converters/image.test.ts | 30 +++ .../pm-adapter/src/converters/image.ts | 6 +- .../pm-adapter/src/converters/shapes.test.ts | 16 ++ .../pm-adapter/src/converters/shapes.ts | 6 +- .../pm-adapter/src/utilities.test.ts | 74 +++++++ .../layout-engine/pm-adapter/src/utilities.ts | 34 +++- pnpm-lock.yaml | 183 +++++++++++------- 17 files changed, 460 insertions(+), 119 deletions(-) diff --git a/devtools/visual-testing/packages/harness/package.json b/devtools/visual-testing/packages/harness/package.json index 858156dbea..2a9a8580d4 100644 --- a/devtools/visual-testing/packages/harness/package.json +++ b/devtools/visual-testing/packages/harness/package.json @@ -11,7 +11,7 @@ "dependencies": { "@fontsource/inter": "^5.2.8", "vue": "^3.5.13", - "superdoc": "workspace:*" + "superdoc": "file:../../../../packages/superdoc/superdoc.tgz" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.2", diff --git a/devtools/visual-testing/packages/test-helpers/package.json b/devtools/visual-testing/packages/test-helpers/package.json index ad526fa6f7..388026b2ed 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 a88b8a2140..943aa4ab95 100644 --- a/packages/layout-engine/contracts/src/index.ts +++ b/packages/layout-engine/contracts/src/index.ts @@ -1589,6 +1589,7 @@ export type ImageFragment = { width: number; height: number; isAnchored?: boolean; + behindDoc?: boolean; zIndex?: number; pmStart?: number; pmEnd?: number; @@ -1604,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/src/index.ts b/packages/layout-engine/layout-engine/src/index.ts index 9990c540a8..e7629254ac 100644 --- a/packages/layout-engine/layout-engine/src/index.ts +++ b/packages/layout-engine/layout-engine/src/index.ts @@ -1063,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, @@ -1957,6 +1956,7 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options width: imgMeasure.width, height: imgMeasure.height, isAnchored: true, + behindDoc: imgBlock.anchor?.behindDoc === true, zIndex: getFragmentZIndex(imgBlock), metadata, }; diff --git a/packages/layout-engine/layout-engine/src/layout-paragraph.ts b/packages/layout-engine/layout-engine/src/layout-paragraph.ts index 4aefc73e87..4b91b15e78 100644 --- a/packages/layout-engine/layout-engine/src/layout-paragraph.ts +++ b/packages/layout-engine/layout-engine/src/layout-paragraph.ts @@ -389,6 +389,7 @@ export function layoutParagraphBlock(ctx: ParagraphLayoutContext, anchors?: Para width: entry.measure.width, height: entry.measure.height, isAnchored: true, + behindDoc: entry.block.anchor?.behindDoc === true, zIndex: getFragmentZIndex(entry.block), metadata, }; @@ -407,6 +408,7 @@ export function layoutParagraphBlock(ctx: ParagraphLayoutContext, anchors?: Para geometry: entry.measure.geometry, scale: entry.measure.scale, isAnchored: true, + behindDoc: entry.block.anchor?.behindDoc === true, zIndex: getFragmentZIndex(entry.block), drawingContentId: entry.block.drawingContentId, }; @@ -433,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/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/pm-adapter/src/converters/image.test.ts b/packages/layout-engine/pm-adapter/src/converters/image.test.ts index 2d9a8a9c09..828756e8cf 100644 --- a/packages/layout-engine/pm-adapter/src/converters/image.test.ts +++ b/packages/layout-engine/pm-adapter/src/converters/image.test.ts @@ -490,6 +490,36 @@ describe('image converter', () => { 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', diff --git a/packages/layout-engine/pm-adapter/src/converters/image.ts b/packages/layout-engine/pm-adapter/src/converters/image.ts index 4614a5fe8a..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, normalizeZIndex } from '../utilities.js'; +import { isFiniteNumber, pickNumber, normalizeZIndex, resolveFloatingZIndex } from '../utilities.js'; // ============================================================================ // Constants @@ -269,8 +269,8 @@ export function imageNodeToBlock( : 'contain'; // Same z-index as editor: from OOXML relativeHeight (Math.max(0, relativeHeight - OOXML_Z_INDEX_BASE)) - const zIndex = - normalizeZIndex(attrs.originalAttributes as Record | undefined) ?? (anchor?.behindDoc ? 0 : 1); + const zIndexFromRelativeHeight = normalizeZIndex(attrs.originalAttributes as Record | undefined); + const zIndex = resolveFloatingZIndex(anchor?.behindDoc === true, zIndexFromRelativeHeight); return { kind: 'image', 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 87d3e12519..7e9fad8390 100644 --- a/packages/layout-engine/pm-adapter/src/utilities.test.ts +++ b/packages/layout-engine/pm-adapter/src/utilities.test.ts @@ -34,6 +34,7 @@ import { coerceRelativeHeight, normalizeZIndex, getFragmentZIndex, + resolveFloatingZIndex, OOXML_Z_INDEX_BASE, } from './utilities.js'; @@ -1551,6 +1552,33 @@ describe('z-index utilities', () => { }); }); + 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 = { @@ -1583,6 +1611,30 @@ describe('z-index utilities', () => { 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, @@ -1602,6 +1654,28 @@ describe('z-index utilities', () => { 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, diff --git a/packages/layout-engine/pm-adapter/src/utilities.ts b/packages/layout-engine/pm-adapter/src/utilities.ts index 590954b730..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, @@ -1480,6 +1479,25 @@ export function normalizeZIndex(originalAttributes: unknown): number | 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. * @@ -1489,11 +1507,17 @@ export function normalizeZIndex(originalAttributes: unknown): number | undefined * `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 { - if (typeof block.zIndex === 'number') return block.zIndex; const attrs = block.attrs as { originalAttributes?: unknown } | undefined; - const z = normalizeZIndex(attrs?.originalAttributes); - if (z !== undefined) return z; - return block.anchor?.behindDoc ? 0 : 1; + const raw = typeof block.zIndex === 'number' ? block.zIndex : normalizeZIndex(attrs?.originalAttributes); + + return resolveFloatingZIndex(block.anchor?.behindDoc === true, raw); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34d8936020..28b278ed9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -422,7 +422,7 @@ importers: version: 14.0.3 mintlify: specifier: ^4.2.295 - version: 4.2.315(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.8)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3) + version: 4.2.315(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.2)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3) apps/vscode-ext: dependencies: @@ -492,8 +492,8 @@ importers: specifier: ^5.2.8 version: 5.2.8 superdoc: - specifier: workspace:* - version: link:../../../../packages/superdoc + specifier: file:../../../../packages/superdoc/superdoc.tgz + version: file:packages/superdoc/superdoc.tgz(@hocuspocus/provider@2.15.3(y-protocols@1.0.7(yjs@13.6.19))(yjs@13.6.19))(canvas@3.2.0)(pdfjs-dist@4.3.136)(typescript@5.9.3)(y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.19))(yjs@13.6.19))(yjs@13.6.19) vue: specifier: 3.5.25 version: 3.5.25(typescript@5.9.3) @@ -510,6 +510,9 @@ importers: '@playwright/test': specifier: '>=1.40.0' version: 1.58.1 + '@superdoc-testing/harness': + specifier: workspace:* + version: link:../harness devDependencies: eslint_d: specifier: ^14.3.0 @@ -9897,6 +9900,15 @@ packages: resolution: {integrity: sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==} engines: {node: '>=18'} + superdoc@file:packages/superdoc/superdoc.tgz: + resolution: {integrity: sha512-bWAWn/eZIojIXcFNVaYuyBgSdUjWatcU8yR0P0RX1CEtbWhBHJ8x3OujNnP6F1NbXDi1/PLOEC3XLZm7igbWfg==, tarball: file:packages/superdoc/superdoc.tgz} + version: 1.10.1-next.4 + peerDependencies: + '@hocuspocus/provider': ^2.13.6 + pdfjs-dist: '>=4.3.136 <=4.6.82' + y-prosemirror: ^1.3.7 + yjs: 13.6.19 + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -12382,128 +12394,128 @@ snapshots: '@inquirer/ansi@1.0.2': {} - '@inquirer/checkbox@4.3.2(@types/node@22.19.8)': + '@inquirer/checkbox@4.3.2(@types/node@22.19.2)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@22.19.8) + '@inquirer/core': 10.3.2(@types/node@22.19.2) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.2) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/confirm@5.1.21(@types/node@22.19.8)': + '@inquirer/confirm@5.1.21(@types/node@22.19.2)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.8) - '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@inquirer/core': 10.3.2(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.2) optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/core@10.3.2(@types/node@22.19.8)': + '@inquirer/core@10.3.2(@types/node@22.19.2)': dependencies: '@inquirer/ansi': 1.0.2 '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.2) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/editor@4.2.23(@types/node@22.19.8)': + '@inquirer/editor@4.2.23(@types/node@22.19.2)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.8) - '@inquirer/external-editor': 1.0.3(@types/node@22.19.8) - '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@inquirer/core': 10.3.2(@types/node@22.19.2) + '@inquirer/external-editor': 1.0.3(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.2) optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/expand@4.0.23(@types/node@22.19.8)': + '@inquirer/expand@4.0.23(@types/node@22.19.2)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.8) - '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@inquirer/core': 10.3.2(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.2) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/external-editor@1.0.3(@types/node@22.19.8)': + '@inquirer/external-editor@1.0.3(@types/node@22.19.2)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 '@inquirer/figures@1.0.15': {} - '@inquirer/input@4.3.1(@types/node@22.19.8)': + '@inquirer/input@4.3.1(@types/node@22.19.2)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.8) - '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@inquirer/core': 10.3.2(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.2) optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/number@3.0.23(@types/node@22.19.8)': + '@inquirer/number@3.0.23(@types/node@22.19.2)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.8) - '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@inquirer/core': 10.3.2(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.2) optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/password@4.0.23(@types/node@22.19.8)': + '@inquirer/password@4.0.23(@types/node@22.19.2)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@22.19.8) - '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@inquirer/core': 10.3.2(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.2) optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/prompts@7.9.0(@types/node@22.19.8)': - dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@22.19.8) - '@inquirer/confirm': 5.1.21(@types/node@22.19.8) - '@inquirer/editor': 4.2.23(@types/node@22.19.8) - '@inquirer/expand': 4.0.23(@types/node@22.19.8) - '@inquirer/input': 4.3.1(@types/node@22.19.8) - '@inquirer/number': 3.0.23(@types/node@22.19.8) - '@inquirer/password': 4.0.23(@types/node@22.19.8) - '@inquirer/rawlist': 4.1.11(@types/node@22.19.8) - '@inquirer/search': 3.2.2(@types/node@22.19.8) - '@inquirer/select': 4.4.2(@types/node@22.19.8) + '@inquirer/prompts@7.9.0(@types/node@22.19.2)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.19.2) + '@inquirer/confirm': 5.1.21(@types/node@22.19.2) + '@inquirer/editor': 4.2.23(@types/node@22.19.2) + '@inquirer/expand': 4.0.23(@types/node@22.19.2) + '@inquirer/input': 4.3.1(@types/node@22.19.2) + '@inquirer/number': 3.0.23(@types/node@22.19.2) + '@inquirer/password': 4.0.23(@types/node@22.19.2) + '@inquirer/rawlist': 4.1.11(@types/node@22.19.2) + '@inquirer/search': 3.2.2(@types/node@22.19.2) + '@inquirer/select': 4.4.2(@types/node@22.19.2) optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/rawlist@4.1.11(@types/node@22.19.8)': + '@inquirer/rawlist@4.1.11(@types/node@22.19.2)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.8) - '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@inquirer/core': 10.3.2(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.2) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/search@3.2.2(@types/node@22.19.8)': + '@inquirer/search@3.2.2(@types/node@22.19.2)': dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.8) + '@inquirer/core': 10.3.2(@types/node@22.19.2) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.2) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/select@4.4.2(@types/node@22.19.8)': + '@inquirer/select@4.4.2(@types/node@22.19.2)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@22.19.8) + '@inquirer/core': 10.3.2(@types/node@22.19.2) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@22.19.8) + '@inquirer/type': 3.0.10(@types/node@22.19.2) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 - '@inquirer/type@3.0.10(@types/node@22.19.8)': + '@inquirer/type@3.0.10(@types/node@22.19.2)': optionalDependencies: - '@types/node': 22.19.8 + '@types/node': 22.19.2 '@isaacs/balanced-match@4.0.1': {} @@ -12857,9 +12869,9 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} - '@mintlify/cli@4.0.919(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.8)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3)': + '@mintlify/cli@4.0.919(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.2)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3)': dependencies: - '@inquirer/prompts': 7.9.0(@types/node@22.19.8) + '@inquirer/prompts': 7.9.0(@types/node@22.19.2) '@mintlify/common': 1.0.698(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@mintlify/link-rot': 3.0.857(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@mintlify/models': 0.0.263 @@ -12873,7 +12885,7 @@ snapshots: front-matter: 4.0.2 fs-extra: 11.2.0 ink: 6.3.0(@types/react@19.2.11)(react@19.2.3) - inquirer: 12.3.0(@types/node@22.19.8) + inquirer: 12.3.0(@types/node@22.19.2) js-yaml: 4.1.0 mdast-util-mdx-jsx: 3.2.0 react: 19.2.3 @@ -18340,12 +18352,12 @@ snapshots: inline-style-parser@0.2.7: {} - inquirer@12.3.0(@types/node@22.19.8): + inquirer@12.3.0(@types/node@22.19.2): dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.8) - '@inquirer/prompts': 7.9.0(@types/node@22.19.8) - '@inquirer/type': 3.0.10(@types/node@22.19.8) - '@types/node': 22.19.8 + '@inquirer/core': 10.3.2(@types/node@22.19.2) + '@inquirer/prompts': 7.9.0(@types/node@22.19.2) + '@inquirer/type': 3.0.10(@types/node@22.19.2) + '@types/node': 22.19.2 ansi-escapes: 4.3.2 mute-stream: 2.0.0 run-async: 3.0.0 @@ -20051,9 +20063,9 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 - mintlify@4.2.315(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.8)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3): + mintlify@4.2.315(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.2)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3): dependencies: - '@mintlify/cli': 4.0.919(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.8)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3) + '@mintlify/cli': 4.0.919(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(react@19.2.3))(@types/node@22.19.2)(@types/react@19.2.11)(react-dom@19.2.0(react@19.2.3))(typescript@5.9.3) transitivePeerDependencies: - '@radix-ui/react-popover' - '@types/node' @@ -22297,6 +22309,29 @@ snapshots: make-asynchronous: 1.0.1 time-span: 5.1.0 + superdoc@file:packages/superdoc/superdoc.tgz(@hocuspocus/provider@2.15.3(y-protocols@1.0.7(yjs@13.6.19))(yjs@13.6.19))(canvas@3.2.0)(pdfjs-dist@4.3.136)(typescript@5.9.3)(y-prosemirror@1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.19))(yjs@13.6.19))(yjs@13.6.19): + dependencies: + '@hocuspocus/provider': 2.15.3(y-protocols@1.0.7(yjs@13.6.19))(yjs@13.6.19) + buffer-crc32: 1.0.0 + eventemitter3: 5.0.4 + jsdom: 27.3.0(canvas@3.2.0) + naive-ui: 2.43.2(vue@3.5.25(typescript@5.9.3)) + pdfjs-dist: 4.3.136 + pinia: 2.3.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)) + rollup-plugin-copy: 3.5.0 + uuid: 9.0.1 + vue: 3.5.25(typescript@5.9.3) + y-prosemirror: 1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(y-protocols@1.0.7(yjs@13.6.19))(yjs@13.6.19) + y-websocket: 3.0.0(yjs@13.6.19) + yjs: 13.6.19 + transitivePeerDependencies: + - '@vue/composition-api' + - bufferutil + - canvas + - supports-color + - typescript + - utf-8-validate + supports-color@5.5.0: dependencies: has-flag: 3.0.0