// Loads the extension's libraries the way the browser does, but under node, so // the round-trip can be tested without driving a browser. // // The only difference from the real thing is where the vendored packages come // from: the browser gets them out of vendor/docx-vendor.js, node gets them // from node_modules — with mammoth taken from .patched/, the same patched copy // that goes into the bundle. import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import path from "node:path"; import vm from "node:vm"; import { JSDOM } from "jsdom"; const here = path.dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); export const ADDON = path.resolve(here, "../../../bundled-addons/docx-editor"); export function loadAddonLibs() { const dom = new JSDOM(""); globalThis.window = dom.window; globalThis.document = dom.window.document; globalThis.DOMParser = dom.window.DOMParser; globalThis.XMLSerializer = dom.window.XMLSerializer; globalThis.Node = dom.window.Node; globalThis.DOCXV = { mammoth: require(path.resolve(here, "../.patched/mammoth")), docx: require("docx"), JSZip: require("jszip"), pm: { state: require("prosemirror-state"), view: null, // not needed outside the browser model: require("prosemirror-model"), schemaBasic: require("prosemirror-schema-basic"), schemaList: require("prosemirror-schema-list"), tables: require("prosemirror-tables"), history: require("prosemirror-history"), commands: require("prosemirror-commands"), keymap: require("prosemirror-keymap"), inputrules: require("prosemirror-inputrules"), dropcursor: null, gapcursor: null, }, }; globalThis.DocxEditor = {}; for (const f of ["pkg.js", "schema.js", "read.js", "write.js"]) { const src = readFileSync(path.join(ADDON, "lib", f), "utf8"); vm.runInThisContext(src, { filename: path.join(ADDON, "lib", f) }); } return globalThis.DocxEditor; } // A compact, comparable view of a ProseMirror document: node types, the // attributes that came from Word, and each text node's marks. Positions and // ids are left out so the diff shows content drift and nothing else. export function summarise(doc) { const MEANINGFUL = ["level", "align", "indent", "lineHeight", "spaceBefore", "spaceAfter", "format", "order", "colspan", "rowspan", "noteType", "noteId", "alt", "width", "height"]; function attrs(node) { const out = {}; for (const k of MEANINGFUL) { const v = node.attrs && node.attrs[k]; if (v === undefined || v === null) continue; if ((k === "colspan" || k === "rowspan" || k === "order") && v === 1) continue; if (k === "indent" && v === 0) continue; out[k] = v; } return out; } function walk(node) { if (node.isText) { const marks = node.marks.map((m) => { const a = Object.keys(m.attrs || {}) .filter((k) => m.attrs[k] !== null && m.attrs[k] !== undefined) .sort() .map((k) => `${k}=${m.attrs[k]}`) .join(","); return a ? `${m.type.name}(${a})` : m.type.name; }).sort(); return { t: "text", text: node.text, marks }; } const entry = { t: node.type.name }; const a = attrs(node); if (Object.keys(a).length) entry.a = a; if (node.type.name === "image") { // Compare the bytes by length, not by the whole base64 blob. entry.a = Object.assign(entry.a || {}, { srcLen: (node.attrs.src || "").length }); } const kids = []; node.forEach((child) => kids.push(walk(child))); if (kids.length) entry.c = kids; return entry; } return walk(doc); } export function diff(a, b, pathStr = "doc", out = []) { const ja = JSON.stringify(a), jb = JSON.stringify(b); if (ja === jb) return out; if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) { out.push(`${pathStr}: ${ja} -> ${jb}`); return out; } if (Array.isArray(a) !== Array.isArray(b)) { out.push(`${pathStr}: shape changed`); return out; } if (Array.isArray(a)) { if (a.length !== b.length) out.push(`${pathStr}: ${a.length} children -> ${b.length}`); for (let i = 0; i < Math.max(a.length, b.length); i++) { if (i >= a.length) { out.push(`${pathStr}[${i}]: added ${JSON.stringify(b[i]).slice(0, 120)}`); continue; } if (i >= b.length) { out.push(`${pathStr}[${i}]: lost ${JSON.stringify(a[i]).slice(0, 120)}`); continue; } diff(a[i], b[i], `${pathStr}[${i}]`, out); } return out; } for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) { diff(a[k], b[k], `${pathStr}.${k}`, out); } return out; }