Files
edentext/tests/normalize.ts
T
Steffen Becker 873af0794d test: fuzz round-trip leg over seeded random documents
tests/fuzzDoc.ts generates documents from a seeded PRNG (marks, lists,
tables, indents, XML-hostile text); fuzz-roundtrip.test.ts runs 50 seeds
through buildOdt -> importOdt and diffs the normalized JSON, naming the
failing seed. normalize/firstDiff moved out of roundtrip.test.ts into
tests/normalize.ts for reuse. The generator documents what is lossy by
design: default-equal marks in headings, indents and start in nested lists.
2026-08-15 22:17:35 +02:00

61 lines
2.4 KiB
TypeScript

// Shared round-trip comparison: reduce editor JSON to a canonical shape (defaults
// dropped, marks sorted, adjacent identical runs merged) and report the first diff.
type N = any;
const ORDERED_DEFAULTS: Record<string, unknown> = {
textAlign: 'left', lineHeight: null, spaceBefore: null, spaceAfter: null,
listStyleType: 'decimal', start: 1, rowHeight: null, colspan: 1, rowspan: 1,
type: null, level: undefined, rotation: 0, wrap: 'inline',
shapeKind: 'textbox', fillColor: '#FFFFFF', strokeColor: '#000000', strokeWidthPt: 1,
};
export function normalize(node: N): N {
const out: N = { type: node.type };
if (node.text != null) out.text = node.text;
if (node.marks?.length) {
out.marks = node.marks
.map((m: N) => {
const mm: N = { type: m.type };
const attrs = Object.fromEntries(Object.entries(m.attrs ?? {}).filter(([, v]) => v != null));
if (Object.keys(attrs).length) mm.attrs = attrs;
return mm;
})
.sort((a: N, b: N) => a.type.localeCompare(b.type));
}
const attrs: N = {};
for (const [k, v] of Object.entries(node.attrs ?? {})) {
if (v == null) continue;
if (k in ORDERED_DEFAULTS && ORDERED_DEFAULTS[k] === v) continue;
if (k === 'colwidth') { attrs.colwidth = 'CW'; continue; } // ratios compared separately
attrs[k] = v;
}
if (node.attrs?.level != null) attrs.level = node.attrs.level;
if (Object.keys(attrs).length) out.attrs = attrs;
if (node.content?.length) {
// merge adjacent identical text nodes so run-splitting differences don't matter
const kids: N[] = [];
for (const c of node.content.map(normalize)) {
const prev = kids[kids.length - 1];
if (prev && prev.type === 'text' && c.type === 'text' &&
JSON.stringify(prev.marks ?? null) === JSON.stringify(c.marks ?? null)) {
prev.text += c.text;
} else kids.push(c);
}
out.content = kids;
}
return out;
}
export function firstDiff(a: N, b: N, path = '$'): string | null {
if (typeof a !== typeof b) return `${path}: type ${typeof a} vs ${typeof b}`;
if (typeof a !== 'object' || a === null || b === null) {
return Object.is(a, b) ? null : `${path}: ${JSON.stringify(a)} vs ${JSON.stringify(b)}`;
}
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
for (const k of keys) {
const d = firstDiff(a[k], b[k], `${path}.${k}`);
if (d) return d;
}
return null;
}