e2a3b5fc4d
Ten media/embed node types move their TOP-LEVEL serialization off raw schema HTML onto a readable markdown target plus an always-emitted discriminator comment whose NAME selects the node type. The schema-HTML form is retained on the raw-HTML/columns path (comments are dropped by the DOM parse stage there). image-form <!--name …--> youtube, video, audio, drawio, excalidraw link-form [text](src)<!--name …--> pdf, attachment, embed (text=filename/provider) standalone <!--pageembed …--> / <!--transclusion …--> pageEmbed, transclusionReference The comment NAME is the node-type discriminator and is ALWAYS emitted, even when the attr JSON is empty (`<!--youtube-->`), so a bare `` is never mistaken for an `image` and a bare `[t](u)` stays a plain link — no URL-sniffing. src rides in the markdown target; every other non-default attr (incl. the id links attachmentId/sourcePageId/transclusionId) rides in the comment JSON (stable key order, numerics stringified, align="center" omitted). New src/lib/media-html.ts: byte-exact builders reproducing the schema HTML each old processNode case returned. Both the serializer's raw-HTML path (blockToHtml, now de-delegated from `return processNode(block)` to explicit per-type cases) and the importer call these, so serialize and parse cannot drift. Import (applyCommentDirectives): image-form binds the preceding <img> (src from it), link-form the preceding <a> (src=href, text=filename/provider), standalone replaces the comment (same leading-doc-level handling as #5). Each rebuilds the schema element via the media-html builder, then swaps it in; the empty-<p> hoist is absorbed by stripEmptyParagraphs. Fail-open: wrong element/position/name or malformed JSON -> inert, no throw. Link-form visible text is escaped (escapeLinkText) for the FULL set of CommonMark inline-active punctuation (\ ` * _ ~ [ ] < & ! ( )), not just [ ] \: the label is parsed as inline content, so a filename/provider like `report *v2*.pdf` or `.pdf` would otherwise lose the markup (or fragment the parse) when the importer reads a.textContent back — a data-loss regression vs the old data-attachment-name form. Adversarial round-trip fixtures lock byte- and value-stability for emphasis/code/strike/autolink/entity/image markers and nested-link names. Tests: new media-comments.test.ts (40 cases: per-type exact md + lossless byte-stable round-trip incl. id links, minimal-node discriminator-still-emitted, in-column schema-HTML form, discriminator integrity, fail-open, active-punct filenames). Goldens in media-roundtrip / markdown-converter-golden / markdown-converter / diagram-roundtrip updated to the md+comment form (columns stay schema-HTML). The former known-limitation image-diagrams fixture is now byte- AND canonically-stable (canon #8 omits the diagram align="center" default) and was promoted from an it.fails into the green corpus (11-image-diagrams.json). git-sync stabilize.test.ts: the "diagram materializes data-align=center" fixpoint moved into a column (where the raw-HTML asymmetry still holds), since top level is now byte-stable. package vitest: 540 passed; tsc clean. git-sync: 268 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
2.9 KiB
TypeScript
69 lines
2.9 KiB
TypeScript
import { readFile } from 'node:fs/promises';
|
|
import { readdirSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
convertProseMirrorToMarkdown,
|
|
markdownToProseMirror,
|
|
docsCanonicallyEqual,
|
|
} from 'docmost-client';
|
|
|
|
// Resolve fixtures relative to this test file so the test is CWD-independent.
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const CORPUS_DIR = join(here, 'fixtures', 'corpus');
|
|
|
|
/** Run a single document through export -> import -> export. */
|
|
async function roundTrip(doc: any) {
|
|
const md1 = convertProseMirrorToMarkdown(doc);
|
|
const doc2 = await markdownToProseMirror(md1);
|
|
const md2 = convertProseMirrorToMarkdown(doc2);
|
|
return { md1, md2, doc2 };
|
|
}
|
|
|
|
describe('round-trip corpus (SPEC §11)', () => {
|
|
// Discover the corpus synchronously at collection time so each fixture gets
|
|
// its own `it` with the file name in the test title.
|
|
const files = readdirSync(CORPUS_DIR)
|
|
.filter((name) => name.endsWith('.json'))
|
|
.sort();
|
|
|
|
it('has a non-empty corpus', () => {
|
|
expect(files.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
for (const name of files) {
|
|
it(`${name}: markdown byte-stable AND canonically stable`, async () => {
|
|
const doc = JSON.parse(await readFile(join(CORPUS_DIR, name), 'utf8'));
|
|
const { md1, md2, doc2 } = await roundTrip(doc);
|
|
|
|
// 1) The byte-stable markdown property git actually needs.
|
|
expect(md2, `${name}: markdown not byte-stable`).toBe(md1);
|
|
// 2) Semantic stability (block ids stripped, default-null normalized).
|
|
expect(
|
|
docsCanonicallyEqual(doc, doc2),
|
|
`${name}: document not canonically stable`,
|
|
).toBe(true);
|
|
});
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FORMER KNOWN LIMITATION — now promoted into the green corpus above.
|
|
//
|
|
// SPEC §11 flagged images and diagrams as high round-trip risk, and
|
|
// `image-diagrams.json` (a paragraph + block image + drawio + excalidraw) was
|
|
// held out here with `it.fails` because it was not byte-stable on export #1: the
|
|
// drawio/excalidraw `align` default "center" materialized on import, so export
|
|
// #2 grew a `data-align="center"` suffix.
|
|
//
|
|
// #293 canon #8 removes that divergence. The diagram family now serializes at
|
|
// TOP LEVEL as `<!--drawio|excalidraw {…}-->`, and — exactly like the
|
|
// image `` form — a default `align:"center"` is OMITTED from the comment
|
|
// JSON (it re-materializes as the schema default on import, then is omitted again
|
|
// on re-export). The block-image hoist that once left a phantom empty paragraph
|
|
// is already absorbed by `stripEmptyParagraphs`. The fixture is therefore now
|
|
// BOTH byte-stable AND canonically stable, so it lives in fixtures/corpus as
|
|
// `11-image-diagrams.json` and is exercised by the green corpus loop above.
|
|
// ---------------------------------------------------------------------------
|