Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"test:behavior:html": "pnpm --filter @superdoc-testing/behavior test:html",
"type-check": "tsc -b tsconfig.references.json",
"type-check:force": "tsc -b --force tsconfig.references.json",
"rebuild:types": "pnpm run --filter=@superdoc/common --filter=@superdoc/word-layout --filter=@superdoc/contracts --filter=@superdoc/layout-resolved --filter=@superdoc/geometry-utils --filter=@superdoc/style-engine --filter=@superdoc/pm-adapter --filter=@superdoc/measuring-dom --filter=@superdoc/layout-engine --filter=@superdoc/painter-dom --filter=@superdoc/layout-bridge build",
"rebuild:types": "pnpm run --filter=@superdoc/common --filter=@superdoc/word-layout --filter=@superdoc/contracts --filter=@superdoc/dom-contract --filter=@superdoc/layout-resolved --filter=@superdoc/geometry-utils --filter=@superdoc/style-engine --filter=@superdoc/pm-adapter --filter=@superdoc/measuring-dom --filter=@superdoc/layout-engine --filter=@superdoc/painter-dom --filter=@superdoc/layout-bridge build",
"validate:commands": "node scripts/validate-command-types.mjs",
"unzip": "bash packages/super-editor/src/editors/v1/tests/helpers/unzip.sh",
"dev": "pnpm --prefix packages/superdoc run dev",
Expand Down
22 changes: 22 additions & 0 deletions packages/layout-engine/dom-contract/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@superdoc/dom-contract",
"version": "0.0.0",
"description": "DOM surface contract: class names, data-attribute constants, and selector helpers shared between the painter and editor-side DOM readers.",
"type": "module",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"scripts": {
"build": "tsc --project tsconfig.json --noEmit",
"test": "vitest run"
},
"devDependencies": {
"vitest": "catalog:"
}
}
53 changes: 53 additions & 0 deletions packages/layout-engine/dom-contract/src/class-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* DOM Contract: Class Names
*
* CSS class names stamped on rendered document elements by the DOM painter.
* These names form a public contract read by the painter (emitter) and by
* editor-side DOM observation code (reader).
*
* Changing a value here is a breaking change for both systems.
*/

export const DOM_CLASS_NAMES = {
/** Top-level page container element. */
PAGE: 'superdoc-page',

/** Fragment container (paragraph, table, image block, etc.). */
FRAGMENT: 'superdoc-fragment',

/** Line container within a fragment. */
LINE: 'superdoc-line',

/**
* Inline structured-content (SDT) wrapper.
*
* Carries `data-pm-start` / `data-pm-end` for selection highlighting.
* Should be EXCLUDED from click-to-position mapping — child spans are
* the character-level targets.
*/
INLINE_SDT_WRAPPER: 'superdoc-structured-content-inline',

/** Block-level structured-content container. */
BLOCK_SDT: 'superdoc-structured-content-block',

/** Table fragment container (resize overlay and click-mapping target). */
TABLE_FRAGMENT: 'superdoc-table-fragment',

/** Document section container. */
DOCUMENT_SECTION: 'superdoc-document-section',

/** Hover highlight applied to all fragments of the same block SDT. */
SDT_HOVER: 'sdt-hover',

/** Block-level image fragment (ImageBlock). */
IMAGE_FRAGMENT: 'superdoc-image-fragment',

/** Inline image element (ImageRun inside a paragraph). */
INLINE_IMAGE: 'superdoc-inline-image',

/** Clip wrapper around a cropped inline image. */
INLINE_IMAGE_CLIP_WRAPPER: 'superdoc-inline-image-clip-wrapper',
} as const;

/** Union of all DOM contract class name values. */
export type DomClassName = (typeof DOM_CLASS_NAMES)[keyof typeof DOM_CLASS_NAMES];
42 changes: 42 additions & 0 deletions packages/layout-engine/dom-contract/src/data-attrs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* DOM Contract: Data Attributes
*
* Named constants for `data-*` attributes stamped on rendered DOM elements.
* These attributes are read by editor-side DOM observers, click-to-position
* mapping, and bridge compatibility code.
*
* Each constant stores the full attribute name (e.g. `"data-pm-start"`) as it
* appears in `getAttribute()` / `setAttribute()` calls and CSS selectors.
*
* The `DATASET_KEYS` mirror provides the camelCase equivalents used with
* `element.dataset.*` access.
*/

/**
* Full attribute names for use with `getAttribute` / `setAttribute` / CSS selectors.
*/
export const DATA_ATTRS = {
/** ProseMirror start position of the element's content range. */
PM_START: 'data-pm-start',

/** ProseMirror end position of the element's content range. */
PM_END: 'data-pm-end',

/** Layout epoch stamp — incremented on each layout pass. */
LAYOUT_EPOCH: 'data-layout-epoch',

/** JSON-encoded table boundary metadata for resize overlays. */
TABLE_BOUNDARIES: 'data-table-boundaries',
} as const;

/**
* CamelCase keys for use with `element.dataset.*` property access.
*
* `element.dataset.pmStart` is equivalent to `element.getAttribute('data-pm-start')`.
*/
export const DATASET_KEYS = {
PM_START: 'pmStart',
PM_END: 'pmEnd',
LAYOUT_EPOCH: 'layoutEpoch',
TABLE_BOUNDARIES: 'tableBoundaries',
} as const;
55 changes: 55 additions & 0 deletions packages/layout-engine/dom-contract/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest';

import {
DOM_CLASS_NAMES,
DATA_ATTRS,
DATASET_KEYS,
buildImagePmSelector,
buildInlineImagePmSelector,
} from './index.js';

describe('@superdoc/dom-contract', () => {
it('exports the stable DOM class names used by the painter and DOM observers', () => {
expect(DOM_CLASS_NAMES).toEqual({
PAGE: 'superdoc-page',
FRAGMENT: 'superdoc-fragment',
LINE: 'superdoc-line',
INLINE_SDT_WRAPPER: 'superdoc-structured-content-inline',
BLOCK_SDT: 'superdoc-structured-content-block',
TABLE_FRAGMENT: 'superdoc-table-fragment',
DOCUMENT_SECTION: 'superdoc-document-section',
SDT_HOVER: 'sdt-hover',
IMAGE_FRAGMENT: 'superdoc-image-fragment',
INLINE_IMAGE: 'superdoc-inline-image',
INLINE_IMAGE_CLIP_WRAPPER: 'superdoc-inline-image-clip-wrapper',
});
});

it('exports the stable data attribute names and dataset keys', () => {
expect(DATA_ATTRS).toEqual({
PM_START: 'data-pm-start',
PM_END: 'data-pm-end',
LAYOUT_EPOCH: 'data-layout-epoch',
TABLE_BOUNDARIES: 'data-table-boundaries',
});

expect(DATASET_KEYS).toEqual({
PM_START: 'pmStart',
PM_END: 'pmEnd',
LAYOUT_EPOCH: 'layoutEpoch',
TABLE_BOUNDARIES: 'tableBoundaries',
});
});

it('builds the full image selector for a rendered pm-start value', () => {
expect(buildImagePmSelector(42)).toBe(
'.superdoc-image-fragment[data-pm-start="42"], .superdoc-inline-image-clip-wrapper[data-pm-start="42"], .superdoc-inline-image[data-pm-start="42"]',
);
});

it('builds the inline image selector in clip-wrapper-first order', () => {
expect(buildInlineImagePmSelector('99')).toBe(
'.superdoc-inline-image-clip-wrapper[data-pm-start="99"], .superdoc-inline-image[data-pm-start="99"]',
);
});
});
21 changes: 21 additions & 0 deletions packages/layout-engine/dom-contract/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @superdoc/dom-contract
*
* Source of truth for the DOM surface contract shared between the painter
* (emitter) and editor-side DOM readers.
*
* This package owns:
* - CSS class name constants
* - Data-attribute name constants
* - Selector helpers built from the above
*
* It must NOT contain DOM querying logic, editor behavior, or painter
* implementation details.
*/

export { DOM_CLASS_NAMES } from './class-names.js';
export type { DomClassName } from './class-names.js';

export { DATA_ATTRS, DATASET_KEYS } from './data-attrs.js';

export { buildImagePmSelector, buildInlineImagePmSelector } from './selectors.js';
37 changes: 37 additions & 0 deletions packages/layout-engine/dom-contract/src/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { DOM_CLASS_NAMES } from './class-names.js';
import { DATA_ATTRS } from './data-attrs.js';

/**
* Builds a compound CSS selector matching any image element (block fragment,
* inline clip-wrapper, or bare inline image) by its `data-pm-start` value.
*
* Useful when re-acquiring an image element after a layout re-render.
*
* Callers with untrusted or user-facing values should `CSS.escape()` before
* passing them here; numeric PM positions and pre-escaped IDs are safe as-is.
*/
export function buildImagePmSelector(pmStart: string | number): string {
const v = String(pmStart);
const attr = DATA_ATTRS.PM_START;
return [
`.${DOM_CLASS_NAMES.IMAGE_FRAGMENT}[${attr}="${v}"]`,
`.${DOM_CLASS_NAMES.INLINE_IMAGE_CLIP_WRAPPER}[${attr}="${v}"]`,
`.${DOM_CLASS_NAMES.INLINE_IMAGE}[${attr}="${v}"]`,
].join(', ');
}

/**
* Builds a compound CSS selector matching inline image elements (clip-wrapper
* first, then bare inline image) by their `data-pm-start` value.
*
* Prefers the clip-wrapper because selection outlines and resize handles should
* target the visible cropped portion, not the scaled inner image.
*/
export function buildInlineImagePmSelector(pmStart: string | number): string {
const v = String(pmStart);
const attr = DATA_ATTRS.PM_START;
return [
`.${DOM_CLASS_NAMES.INLINE_IMAGE_CLIP_WRAPPER}[${attr}="${v}"]`,
`.${DOM_CLASS_NAMES.INLINE_IMAGE}[${attr}="${v}"]`,
].join(', ');
}
11 changes: 11 additions & 0 deletions packages/layout-engine/dom-contract/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": true,
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*.ts"]
}
10 changes: 10 additions & 0 deletions packages/layout-engine/dom-contract/vitest.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';
import baseConfig from '../../../vitest.baseConfig';

export default defineConfig({
...baseConfig,
test: {
environment: 'node',
include: ['src/**/*.test.ts'],
},
});
1 change: 1 addition & 0 deletions packages/layout-engine/layout-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dependencies": {
"@superdoc/common": "workspace:*",
"@superdoc/contracts": "workspace:*",
"@superdoc/dom-contract": "workspace:*",
"@superdoc/layout-engine": "workspace:*",
"@superdoc/measuring-dom": "workspace:*",
"@superdoc/painter-dom": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/layout-engine/layout-bridge/src/dom-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* @deprecated Use DomPointerMapping from super-editor/dom-observer instead.
*/

import { DOM_CLASS_NAMES } from '@superdoc/painter-dom';
import { DOM_CLASS_NAMES } from '@superdoc/dom-contract';

// Debug logging for click-to-position pipeline (disabled - enable for debugging)
const DEBUG_CLICK_MAPPING = false;
Expand Down
1 change: 1 addition & 0 deletions packages/layout-engine/layout-bridge/tsconfig.types.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
},
"references": [
{ "path": "../contracts/tsconfig.json" },
{ "path": "../dom-contract/tsconfig.json" },
{ "path": "../layout-engine/tsconfig.json" },
{ "path": "../measuring/dom/tsconfig.json" },
{ "path": "../painters/dom/tsconfig.json" },
Expand Down
4 changes: 2 additions & 2 deletions packages/layout-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"description": "Layout engine POC - manages layout, pagination, and rendering for SuperDoc",
"scripts": {
"build": "pnpm run --filter=@superdoc/contracts --filter=@superdoc/geometry-utils --filter=@superdoc/pm-adapter --filter=@superdoc/measuring-dom --filter=@superdoc/layout-engine --filter=@superdoc/painter-dom build",
"test": "pnpm run --filter=@superdoc/contracts --filter=@superdoc/geometry-utils --filter=@superdoc/pm-adapter --filter=@superdoc/measuring-dom --filter=@superdoc/layout-engine --filter=@superdoc/painter-dom test"
"build": "pnpm run --filter=@superdoc/contracts --filter=@superdoc/dom-contract --filter=@superdoc/geometry-utils --filter=@superdoc/pm-adapter --filter=@superdoc/measuring-dom --filter=@superdoc/layout-engine --filter=@superdoc/painter-dom build",
"test": "pnpm run --filter=@superdoc/contracts --filter=@superdoc/dom-contract --filter=@superdoc/geometry-utils --filter=@superdoc/pm-adapter --filter=@superdoc/measuring-dom --filter=@superdoc/layout-engine --filter=@superdoc/painter-dom test"
}
}
1 change: 1 addition & 0 deletions packages/layout-engine/painters/dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"dependencies": {
"@superdoc/common": "workspace:*",
"@superdoc/contracts": "workspace:*",
"@superdoc/dom-contract": "workspace:*",
"@superdoc/font-utils": "workspace:*",
"@superdoc/preset-geometry": "workspace:*",
"@superdoc/url-validation": "workspace:*"
Expand Down
Loading
Loading