diff --git a/apps/server/src/collaboration/git-sync-converter-gate.spec.ts b/apps/server/src/collaboration/git-sync-converter-gate.spec.ts new file mode 100644 index 00000000..b4f14a02 --- /dev/null +++ b/apps/server/src/collaboration/git-sync-converter-gate.spec.ts @@ -0,0 +1,349 @@ +/** + * §13.1 IDEMPOTENCY GATE — the blocking gate for git-sync Phase B. + * + * Proves the vendored `@docmost/git-sync` pure converter is schema-compatible + * with the server's REAL editor-ext document schema: a representative corpus of + * editor-ext ProseMirror documents must survive a full round trip through the + * actual server write path without losing any node / mark / attribute. + * + * Pipeline per document (plan §13.1): + * 1. md = convertProseMirrorToMarkdown(content) // git-sync export + * 2. doc = await markdownToProseMirror(md) // git-sync import + * 3. push `doc` through the REAL editor-ext Yjs write path the server uses: + * ydoc = TiptapTransformer.toYdoc(doc, 'default', tiptapExtensions) + * normalized = TiptapTransformer.fromYdoc(ydoc, 'default') + * This is exactly what PersistenceExtension does on store + * (apps/server/src/collaboration/extensions/persistence.extension.ts:96/115) + * with the same `tiptapExtensions` (collaboration.util.ts) and the same + * `@hocuspocus/transformer`, so the gate exercises the real schema + * validation that runs on a git-sync write (plan §3.3). + * 4. assert docsCanonicallyEqual(canon(original), canon(normalized)) === true + * + * Any node / mark / attr that editor-ext drops (because the vendored + * docmost-schema named it differently, or declares a different default) makes + * the gate FAIL for that document — exactly the schema-divergence plan §3.3 / + * §13.1 warn about. Genuine, irreducible divergences are isolated into the + * clearly-named `KNOWN DIVERGENCE` block at the bottom (never silently hidden). + * + * Requires the workspace packages built first: + * pnpm --filter @docmost/editor-ext build + * pnpm --filter @docmost/git-sync build + */ +import { TiptapTransformer } from '@hocuspocus/transformer'; +// Import the server's real schema FIRST so `@docmost/editor-ext` resolves to its +// built CJS `dist` (its `main`). Importing the ESM `@docmost/git-sync` package +// first flips jest's resolver to editor-ext's `module` (src) field, which then +// drags in React node views (navigator-less) and breaks the node test env. +import { tiptapExtensions } from './collaboration.util'; +import { + convertProseMirrorToMarkdown, + markdownToProseMirror, + canonicalizeContent, + docsCanonicallyEqual, +} from '@docmost/git-sync'; + +/** + * Run a single editor-ext document through the full gate pipeline and return + * the canonical original vs the canonical doc as it lands after the real Yjs + * write path, plus the intermediate markdown for diagnostics. + */ +async function runGate(original: any): Promise<{ + md: string; + imported: any; + normalized: any; + canonOriginal: any; + canonNormalized: any; +}> { + // 1) editor-ext JSON -> markdown (git-sync export). + const md = convertProseMirrorToMarkdown(original); + + // 2) markdown -> ProseMirror JSON (git-sync import, docmost-schema). + const imported = await markdownToProseMirror(md); + + // 3) push through the REAL editor-ext schema via the server's Yjs write path. + // toYdoc validates `imported` against tiptapExtensions (throws on an + // unknown node, drops unknown attrs); fromYdoc reads it back as the + // normalized editor-ext JSON the server would persist. + const ydoc = TiptapTransformer.toYdoc(imported, 'default', tiptapExtensions); + const normalized = TiptapTransformer.fromYdoc(ydoc, 'default'); + + return { + md, + imported, + normalized, + canonOriginal: canonicalizeContent(original), + canonNormalized: canonicalizeContent(normalized), + }; +} + +const doc = (...content: any[]) => ({ type: 'doc', content }); +const text = (t: string, marks?: any[]) => + marks ? { type: 'text', text: t, marks } : { type: 'text', text: t }; +const para = (...content: any[]) => ({ type: 'paragraph', content }); + +// --------------------------------------------------------------------------- +// Corpus: editor-ext ProseMirror documents covering the common node/mark types. +// Node / mark / attr names and DEFAULTS are taken from the real schema — +// editor-ext (packages/editor-ext/src) + the server's tiptapExtensions +// (collaboration.util.ts) — NOT guessed. Where editor-ext materializes a +// non-null default on import (e.g. image.align="center", callout.type, list +// start) the fixture pre-authors that materialized value so the round trip is +// already at its fixpoint (matches how the engine normalizes-on-write, SPEC §11). +// --------------------------------------------------------------------------- +const CORPUS: Record = { + 'paragraphs + headings (h1-h3)': doc( + { type: 'heading', attrs: { level: 1 }, content: [text('Heading one')] }, + { type: 'heading', attrs: { level: 2 }, content: [text('Heading two')] }, + { type: 'heading', attrs: { level: 3 }, content: [text('Heading three')] }, + para(text('A plain paragraph of text.')), + para(text('Second paragraph.')), + ), + + 'inline marks (bold/italic/strike/code)': doc( + para( + text('normal '), + text('bold', [{ type: 'bold' }]), + text(' '), + text('italic', [{ type: 'italic' }]), + text(' '), + text('struck', [{ type: 'strike' }]), + text(' '), + text('code', [{ type: 'code' }]), + ), + ), + + 'links': doc( + para( + text('see '), + text('the site', [ + { type: 'link', attrs: { href: 'https://example.com' } }, + ]), + text(' for more'), + ), + ), + + 'bullet list': doc({ + type: 'bulletList', + content: [ + { type: 'listItem', content: [para(text('first'))] }, + { type: 'listItem', content: [para(text('second'))] }, + { type: 'listItem', content: [para(text('third'))] }, + ], + }), + + 'ordered list': doc({ + type: 'orderedList', + attrs: { start: 1 }, + content: [ + { type: 'listItem', content: [para(text('one'))] }, + { type: 'listItem', content: [para(text('two'))] }, + ], + }), + + 'task list (checkbox)': doc({ + type: 'taskList', + content: [ + { + type: 'taskItem', + attrs: { checked: true }, + content: [para(text('done item'))], + }, + { + type: 'taskItem', + attrs: { checked: false }, + content: [para(text('todo item'))], + }, + ], + }), + + 'blockquote': doc({ + type: 'blockquote', + content: [para(text('a quoted line')), para(text('second quoted line'))], + }), + + 'callout (info)': doc({ + type: 'callout', + attrs: { type: 'info' }, + content: [para(text('an informational callout'))], + }), + + 'callout (warning)': doc({ + type: 'callout', + attrs: { type: 'warning' }, + content: [para(text('a warning callout'))], + }), + + 'code block (with language)': doc({ + type: 'codeBlock', + attrs: { language: 'typescript' }, + // A fenced code block's body is stored with a trailing newline (the form a + // markdown ``` fence round-trips to: marked normalizes the code text to end + // in "\n"). Authoring the fixture at that fixpoint mirrors how the engine + // normalizes-on-write (SPEC §11): codeBlock + `language` round-trip exactly. + content: [text('const a: number = 1;\nconsole.log(a);\n')], + }), + + 'horizontal rule': doc( + para(text('before')), + { type: 'horizontalRule' }, + para(text('after')), + ), + + 'table (header row + cells)': doc({ + type: 'table', + content: [ + { + type: 'tableRow', + content: [ + { + type: 'tableHeader', + attrs: { colspan: 1, rowspan: 1, colwidth: null }, + content: [para(text('Name'))], + }, + { + type: 'tableHeader', + attrs: { colspan: 1, rowspan: 1, colwidth: null }, + content: [para(text('Value'))], + }, + ], + }, + { + type: 'tableRow', + content: [ + { + type: 'tableCell', + attrs: { colspan: 1, rowspan: 1, colwidth: null }, + content: [para(text('alpha'))], + }, + { + type: 'tableCell', + attrs: { colspan: 1, rowspan: 1, colwidth: null }, + content: [para(text('1'))], + }, + ], + }, + ], + }), + + 'nested / mixed document': doc( + { type: 'heading', attrs: { level: 1 }, content: [text('Mixed')] }, + para( + text('intro with '), + text('bold', [{ type: 'bold' }]), + text(' and a '), + text('link', [{ type: 'link', attrs: { href: 'https://example.com' } }]), + text('.'), + ), + { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [ + para(text('item with '), text('code', [{ type: 'code' }])), + ], + }, + { + type: 'listItem', + content: [ + para(text('item with sublist')), + { + type: 'bulletList', + content: [ + { type: 'listItem', content: [para(text('nested a'))] }, + { type: 'listItem', content: [para(text('nested b'))] }, + ], + }, + ], + }, + ], + }, + { + type: 'callout', + attrs: { type: 'success' }, + content: [ + para(text('callout body')), + { type: 'codeBlock', attrs: { language: 'bash' }, content: [text('echo hi\n')] }, + ], + }, + { + type: 'blockquote', + content: [para(text('quote at the end'))], + }, + ), +}; + +describe('git-sync converter §13.1 idempotency gate (editor-ext schema)', () => { + for (const [name, original] of Object.entries(CORPUS)) { + it(`round-trips losslessly: ${name}`, async () => { + const { md, canonOriginal, canonNormalized } = await runGate(original); + + const equal = docsCanonicallyEqual(original, canonNormalized); + if (!equal) { + // Surface a readable diff so a real divergence is actionable. + // eslint-disable-next-line no-console + console.error( + `\n[GATE FAIL] ${name}\n--- markdown ---\n${md}\n` + + `--- canonical original ---\n${JSON.stringify(canonOriginal, null, 2)}\n` + + `--- canonical round-tripped ---\n${JSON.stringify(canonNormalized, null, 2)}\n`, + ); + } + expect(equal).toBe(true); + }); + } +}); + +// --------------------------------------------------------------------------- +// KNOWN DIVERGENCE — images (isolated so it does NOT silently weaken the gate). +// +// This is NOT a schema-name divergence: the `image` NODE itself round-trips +// through editor-ext fine (it survives toYdoc under the real tiptapExtensions). +// The loss is intrinsic to MARKDOWN, the on-disk transport format git-sync uses: +// +// 1. `convertProseMirrorToMarkdown` emits a standard `![alt](src)` image +// (markdown-converter.ts case "image"). Standard markdown image syntax has +// no way to express `width` / `height` / `align`, so those attrs are +// DROPPED on export and cannot be recovered on import. +// 2. A block-level image is hoisted out of its line by the HTML re-parser, +// leaving a leading EMPTY paragraph (the same block-image-hoist limitation +// documented in packages/git-sync/test/fixtures/known-limitations). +// +// The gate documents the EXACT lossy shape below. If the converter is ever +// taught to preserve image dimensions (e.g. by emitting an HTML with +// data-* attrs, as it already does for video/diagrams), these assertions flip +// and the image fixture should be promoted into the green CORPUS above. +// --------------------------------------------------------------------------- +describe('git-sync converter §13.1 KNOWN DIVERGENCE (markdown image lossiness)', () => { + const imageDoc = doc({ + type: 'image', + attrs: { + src: 'https://example.com/pic.png', + width: 640, + height: 480, + align: 'center', + }, + }); + + it('drops width/height/align (markdown ![](src) cannot carry them) and hoists the block image past a leading empty paragraph', async () => { + const { md, canonNormalized } = await runGate(imageDoc); + + // Export is plain markdown image syntax — no dimensions/align survive. + expect(md.trim()).toBe('![](https://example.com/pic.png)'); + + // The round-tripped doc is the documented lossy shape: a leading empty + // paragraph (block-image hoist) + an image carrying ONLY src (+ alt=""). + expect(canonNormalized).toEqual({ + type: 'doc', + content: [ + { type: 'paragraph' }, + { + type: 'image', + attrs: { alt: '', src: 'https://example.com/pic.png' }, + }, + ], + }); + + // And it is therefore NOT canonically equal to the original (lock the loss). + expect(docsCanonicallyEqual(imageDoc, canonNormalized)).toBe(false); + }); +}); diff --git a/packages/git-sync/build/engine/loop-guard.js b/packages/git-sync/build/engine/loop-guard.js index a85047e4..88f4af00 100644 --- a/packages/git-sync/build/engine/loop-guard.js +++ b/packages/git-sync/build/engine/loop-guard.js @@ -1,3 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.bodyHash = bodyHash; /** * Loop-guard primitives (SPEC §10). The sync engine must never re-pull its OWN * write as if it were a remote edit: after a push, the next poll will see the @@ -10,7 +13,7 @@ * to decide "this is our own write, ignore it") is a future increment — here we * only PRODUCE the hash and the per-page push record (see `src/push.ts`). */ -import { createHash } from "node:crypto"; +const node_crypto_1 = require("node:crypto"); /** * Stable hash of a page's markdown BODY (SPEC §10 "хэш тела"). Deterministic: * the same input string always yields the same digest, a different input a @@ -23,6 +26,6 @@ import { createHash } from "node:crypto"; * caller is responsible for passing a canonical/stable representation if it * wants hash equality across cosmetic-only differences. */ -export function bodyHash(markdownBody) { - return createHash("sha256").update(markdownBody, "utf8").digest("hex"); +function bodyHash(markdownBody) { + return (0, node_crypto_1.createHash)("sha256").update(markdownBody, "utf8").digest("hex"); } diff --git a/packages/git-sync/build/engine/reconcile.js b/packages/git-sync/build/engine/reconcile.js index 9a111bb5..9ebd2989 100644 --- a/packages/git-sync/build/engine/reconcile.js +++ b/packages/git-sync/build/engine/reconcile.js @@ -1,3 +1,4 @@ +"use strict"; /** * Pure reconciliation planner (SPEC §5/§6/§8). * @@ -11,6 +12,10 @@ * This module is intentionally PURE (no IO, no git) so the whole plan is * unit-testable. The actual file writing / git operations happen in pull.ts. */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MASS_DELETE_FRACTION = exports.MASS_DELETE_MIN_EXISTING = void 0; +exports.planReconciliation = planReconciliation; +exports.decideAbsenceDeletions = decideAbsenceDeletions; /** * Compute the reconciliation plan. * @@ -33,7 +38,7 @@ * path is removed (as an absence/move) so the vault converges to exactly the * live set. */ -export function planReconciliation(live, existing) { +function planReconciliation(live, existing) { // Desired path for each live pageId. const liveByPageId = new Map(); // Set of all paths that WILL be written (never delete/remove one of these). @@ -81,9 +86,9 @@ export function planReconciliation(live, existing) { * Below this many tracked files the mass-delete fraction guard is not applied * (a tiny vault where deleting "most" files is normal, e.g. 1-of-2). */ -export const MASS_DELETE_MIN_EXISTING = 4; +exports.MASS_DELETE_MIN_EXISTING = 4; /** Fraction of tracked files above which a delete plan is a suspected wipe. */ -export const MASS_DELETE_FRACTION = 0.5; +exports.MASS_DELETE_FRACTION = 0.5; /** * Pure decision: should the ABSENCE-based deletions (`plan.toDelete`) be applied * this cycle? Encapsulates the SPEC §8 safety invariants so they are unit- @@ -100,7 +105,7 @@ export const MASS_DELETE_FRACTION = 0.5; * Moves are NOT governed by this decision: a moved page IS present in `live`, so * its old-path removal is real (handled by the caller separately). */ -export function decideAbsenceDeletions(args) { +function decideAbsenceDeletions(args) { const { treeComplete, liveCount, existingCount, deleteCount } = args; // No tracked files, or nothing to delete -> trivially fine to "apply". if (existingCount === 0 || deleteCount === 0) @@ -109,8 +114,8 @@ export function decideAbsenceDeletions(args) { return { apply: false, reason: "incomplete-fetch" }; if (liveCount === 0) return { apply: false, reason: "empty-live" }; - if (existingCount >= MASS_DELETE_MIN_EXISTING && - deleteCount > existingCount * MASS_DELETE_FRACTION) { + if (existingCount >= exports.MASS_DELETE_MIN_EXISTING && + deleteCount > existingCount * exports.MASS_DELETE_FRACTION) { return { apply: false, reason: "mass-delete" }; } return { apply: true }; diff --git a/packages/git-sync/build/engine/sanitize.js b/packages/git-sync/build/engine/sanitize.js index 2aff0f3c..684d0bab 100644 --- a/packages/git-sync/build/engine/sanitize.js +++ b/packages/git-sync/build/engine/sanitize.js @@ -1,3 +1,4 @@ +"use strict"; /** * Deterministic filename strategy (SPEC §12). * @@ -6,6 +7,9 @@ * functions are intentionally dependency-free and pure, so they are trivially * unit-testable. */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.sanitizeTitle = sanitizeTitle; +exports.disambiguate = disambiguate; // Printable characters forbidden in file names on common filesystems (mainly // Windows): / \ < > : " | ? *. Each match is replaced with a single "-". // Spaces are NOT in this set; whitespace is normalized separately below. @@ -64,7 +68,7 @@ function stripControlChars(input) { * result, an all-dots result, or a reserved Windows device name by prefixing * with "_". */ -export function sanitizeTitle(title) { +function sanitizeTitle(title) { let name = stripControlChars(title ?? "") .replace(FORBIDDEN_PRINTABLE_RE, "-") .replace(WHITESPACE_RUN_RE, " ") @@ -92,6 +96,6 @@ export function sanitizeTitle(title) { * to the same name. Appends a stable suffix built from the page's `slugId`, so * the result stays deterministic across runs (SPEC §12: `Title ~slugId`). */ -export function disambiguate(name, slugId) { +function disambiguate(name, slugId) { return `${name} ~${slugId}`; } diff --git a/packages/git-sync/build/lib/diff.js b/packages/git-sync/build/lib/diff.js index 5205aff1..e14f7049 100644 --- a/packages/git-sync/build/lib/diff.js +++ b/packages/git-sync/build/lib/diff.js @@ -1,3 +1,4 @@ +"use strict"; /** * Headless, Docmost-equivalent document diff. * @@ -16,13 +17,15 @@ * If recreateTransform / the changeset throws on a pathological document pair, * we fall back to a coarse block-level text diff so the tool never hard-fails. */ -import { getSchema } from "@tiptap/core"; -import { Node } from "@tiptap/pm/model"; -import { ChangeSet, simplifyChanges } from "@tiptap/pm/changeset"; -import { recreateTransform } from "@fellow/prosemirror-recreate-transform"; -import { docmostExtensions } from "./docmost-schema.js"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.diffDocs = diffDocs; +const core_1 = require("@tiptap/core"); +const model_1 = require("@tiptap/pm/model"); +const changeset_1 = require("@tiptap/pm/changeset"); +const prosemirror_recreate_transform_1 = require("@fellow/prosemirror-recreate-transform"); +const docmost_schema_1 = require("./docmost-schema"); /** Build the schema once; it is pure and reused across calls. */ -const schema = getSchema(docmostExtensions); +const schema = (0, core_1.getSchema)(docmost_schema_1.docmostExtensions); /** Recursively concatenate the plain text of a JSON node. */ function plainText(node) { if (!node || typeof node !== "object") @@ -209,7 +212,7 @@ function renderMarkdown(result, fellBack) { * @param newDocJson the later document * @param notesHeading heading delimiting body from notes for footnote counting */ -export function diffDocs(oldDocJson, newDocJson, notesHeading = "Примечания переводчика") { +function diffDocs(oldDocJson, newDocJson, notesHeading = "Примечания переводчика") { const integrity = computeIntegrity(oldDocJson, newDocJson, notesHeading); let changes = []; let inserted = 0; @@ -217,15 +220,15 @@ export function diffDocs(oldDocJson, newDocJson, notesHeading = "Примеча let fellBack = false; const changedBlocks = new Set(); try { - const oldNode = Node.fromJSON(schema, oldDocJson); - const newNode = Node.fromJSON(schema, newDocJson); - const tr = recreateTransform(oldNode, newNode, { + const oldNode = model_1.Node.fromJSON(schema, oldDocJson); + const newNode = model_1.Node.fromJSON(schema, newDocJson); + const tr = (0, prosemirror_recreate_transform_1.recreateTransform)(oldNode, newNode, { complexSteps: false, wordDiffs: true, simplifyDiff: true, }); - const changeSet = ChangeSet.create(oldNode).addSteps(tr.doc, tr.mapping.maps, []); - const simplified = simplifyChanges(changeSet.changes, newNode); + const changeSet = changeset_1.ChangeSet.create(oldNode).addSteps(tr.doc, tr.mapping.maps, []); + const simplified = (0, changeset_1.simplifyChanges)(changeSet.changes, newNode); for (const change of simplified) { // Deleted text lives in the OLD doc coordinate range [fromA, toA). if (change.toA > change.fromA) { diff --git a/packages/git-sync/build/lib/docmost-schema.js b/packages/git-sync/build/lib/docmost-schema.js index 97cdcafd..148ea642 100644 --- a/packages/git-sync/build/lib/docmost-schema.js +++ b/packages/git-sync/build/lib/docmost-schema.js @@ -1,3 +1,9 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.docmostExtensions = exports.sanitizeCssColor = exports.clampCalloutType = void 0; /** * Full TipTap extension set matching the real Docmost document schema. * @@ -7,14 +13,14 @@ * to or from ProseMirror JSON must use THIS set, otherwise a round-trip * loses content. */ -import StarterKit from "@tiptap/starter-kit"; -import Image from "@tiptap/extension-image"; -import TaskList from "@tiptap/extension-task-list"; -import TaskItem from "@tiptap/extension-task-item"; -import Highlight from "@tiptap/extension-highlight"; -import Subscript from "@tiptap/extension-subscript"; -import Superscript from "@tiptap/extension-superscript"; -import { Node, Extension, Mark } from "@tiptap/core"; +const starter_kit_1 = __importDefault(require("@tiptap/starter-kit")); +const extension_image_1 = __importDefault(require("@tiptap/extension-image")); +const extension_task_list_1 = __importDefault(require("@tiptap/extension-task-list")); +const extension_task_item_1 = __importDefault(require("@tiptap/extension-task-item")); +const extension_highlight_1 = __importDefault(require("@tiptap/extension-highlight")); +const extension_subscript_1 = __importDefault(require("@tiptap/extension-subscript")); +const extension_superscript_1 = __importDefault(require("@tiptap/extension-superscript")); +const core_1 = require("@tiptap/core"); // Inlined from @tiptap/core's getStyleProperty (added after 3.20.x) so this // package can stay on the same @tiptap/core version as the editor and avoid a // duplicate-tiptap version split in the monorepo. Reads a single declaration @@ -41,9 +47,10 @@ function getStyleProperty(element, propertyName) { } /** Allowed Docmost callout types; anything else falls back to "info". */ const CALLOUT_TYPES = ["info", "warning", "danger", "success"]; -export const clampCalloutType = (value) => value && CALLOUT_TYPES.includes(value.toLowerCase()) +const clampCalloutType = (value) => value && CALLOUT_TYPES.includes(value.toLowerCase()) ? value.toLowerCase() : "info"; +exports.clampCalloutType = clampCalloutType; /** * Allowlist guard for CSS color values imported from HTML. * @@ -61,14 +68,15 @@ export const clampCalloutType = (value) => value && CALLOUT_TYPES.includes(value * digits, %, ., commas, spaces and slashes */ const SAFE_COLOR_RE = /^(?:[a-zA-Z]+|#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgb|rgba|hsl|hsla)\([0-9.,%/\s]+\))$/; -export const sanitizeCssColor = (value) => { +const sanitizeCssColor = (value) => { if (typeof value !== "string") return null; const color = value.trim(); return color && SAFE_COLOR_RE.test(color) ? color : null; }; +exports.sanitizeCssColor = sanitizeCssColor; /** Docmost callout (info/warning/danger/success banner). */ -const Callout = Node.create({ +const Callout = core_1.Node.create({ name: "callout", group: "block", content: "block+", @@ -79,9 +87,9 @@ const Callout = Node.create({ // it; without an explicit parseHTML every imported callout became "info". type: { default: "info", - parseHTML: (el) => clampCalloutType(el.getAttribute("data-callout-type")), + parseHTML: (el) => (0, exports.clampCalloutType)(el.getAttribute("data-callout-type")), renderHTML: (attrs) => ({ - "data-callout-type": clampCalloutType(attrs.type), + "data-callout-type": (0, exports.clampCalloutType)(attrs.type), }), }, icon: { @@ -99,7 +107,7 @@ const Callout = Node.create({ }, }); /** Minimal table family: enough for schema round-trips and HTML parsing. */ -const Table = Node.create({ +const Table = core_1.Node.create({ name: "table", group: "block", content: "tableRow+", @@ -111,7 +119,7 @@ const Table = Node.create({ return ["table", ["tbody", 0]]; }, }); -const TableRow = Node.create({ +const TableRow = core_1.Node.create({ name: "tableRow", content: "(tableCell | tableHeader)*", parseHTML() { @@ -134,7 +142,7 @@ const cellAttributes = () => ({ renderHTML: (attrs) => attrs.align ? { align: attrs.align } : {}, }, }); -const TableCell = Node.create({ +const TableCell = core_1.Node.create({ name: "tableCell", content: "block+", isolating: true, @@ -146,7 +154,7 @@ const TableCell = Node.create({ return ["td", 0]; }, }); -const TableHeader = Node.create({ +const TableHeader = core_1.Node.create({ name: "tableHeader", content: "block+", isolating: true, @@ -163,7 +171,7 @@ const TableHeader = Node.create({ * do not declare. Without these, Node.fromJSON silently drops them — * including the block ids that heading anchors rely on. */ -const DocmostAttributes = Extension.create({ +const DocmostAttributes = core_1.Extension.create({ name: "docmostAttributes", addGlobalAttributes() { return [ @@ -205,7 +213,7 @@ const DocmostAttributes = Extension.create({ * which breaks update_page_json and edit_page_text on every commented page. * Mirrors Docmost's @docmost/editor-ext comment mark (commentId / resolved). */ -const Comment = Mark.create({ +const Comment = core_1.Mark.create({ name: "comment", exitable: true, inclusive: false, @@ -238,15 +246,15 @@ const Comment = Mark.create({ * attribute. The parsed color is passed through the allowlist guard so a crafted * style cannot break out of the attribute when Docmost re-renders it. */ -const TextStyle = Mark.create({ +const TextStyle = core_1.Mark.create({ name: "textStyle", addAttributes() { return { color: { default: null, - parseHTML: (el) => sanitizeCssColor(el.style.color || el.getAttribute("data-color")), + parseHTML: (el) => (0, exports.sanitizeCssColor)(el.style.color || el.getAttribute("data-color")), renderHTML: (attrs) => { - const color = sanitizeCssColor(attrs.color); + const color = (0, exports.sanitizeCssColor)(attrs.color); return color ? { style: `color: ${color}` } : {}; }, }, @@ -289,7 +297,7 @@ const TextStyle = Mark.create({ * pattern these follow. */ /** Docmost @mention (user/page reference). Inline atom. */ -const Mention = Node.create({ +const Mention = core_1.Node.create({ name: "mention", group: "inline", inline: true, @@ -343,7 +351,7 @@ const Mention = Node.create({ }, }); /** Inline KaTeX expression. Carries the LaTeX source in `text`. */ -const MathInline = Node.create({ +const MathInline = core_1.Node.create({ name: "mathInline", group: "inline", inline: true, @@ -365,7 +373,7 @@ const MathInline = Node.create({ }, }); /** Block KaTeX expression. Carries the LaTeX source in `text`. */ -const MathBlock = Node.create({ +const MathBlock = core_1.Node.create({ name: "mathBlock", group: "block", atom: true, @@ -387,7 +395,7 @@ const MathBlock = Node.create({ }, }); /** Collapsible
wrapper: summary + content children. */ -const Details = Node.create({ +const Details = core_1.Node.create({ name: "details", group: "block", content: "detailsSummary detailsContent", @@ -410,7 +418,7 @@ const Details = Node.create({ }, }); /** Clickable summary line of a
block. */ -const DetailsSummary = Node.create({ +const DetailsSummary = core_1.Node.create({ name: "detailsSummary", group: "block", content: "inline*", @@ -425,7 +433,7 @@ const DetailsSummary = Node.create({ }, }); /** Body of a
block. Permissive content so fromYdoc output validates. */ -const DetailsContent = Node.create({ +const DetailsContent = core_1.Node.create({ name: "detailsContent", group: "block", // Docmost declares block* (an empty details body is valid); block+ would @@ -441,7 +449,7 @@ const DetailsContent = Node.create({ }, }); /** File attachment card (non-image upload). Block atom. */ -const Attachment = Node.create({ +const Attachment = core_1.Node.create({ name: "attachment", group: "block", inline: false, @@ -493,7 +501,7 @@ const Attachment = Node.create({ }, }); /** Uploaded