Skip to content
Open
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
6 changes: 6 additions & 0 deletions server/milkdown-headless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ function createSerializer(schema: Schema): (doc: ProseMirrorNode) => string {
.use(remarkGfm)
.use(remarkFrontmatter, ['yaml'])
.use(remarkStringify, {
// Preserve canonical markdown style on round-trip. Without these, remark-stringify
// defaults to `*` for bullets and `***` for thematic breaks, producing cosmetic git
// diffs for any source that uses the `-` / `---` convention (which is the dominant
// style in CommonMark and GFM ecosystems).
bullet: '-',
rule: '-',
handlers: {
proofMark: proofMarkHandler,
},
Expand Down
4 changes: 4 additions & 0 deletions src/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,10 @@ class ProofEditorImpl implements ProofEditor {
// Note: remarkProofMarks is now registered via .use(remarkProofMarksPlugin)
ctx.update(remarkStringifyOptionsCtx, (prev) => ({
...prev,
// Preserve canonical markdown style on round-trip. See server/milkdown-headless.ts
// for the same options applied on the headless serializer path.
bullet: '-',
rule: '-',
handlers: {
...(prev.handlers ?? {}),
proofMark: proofMarkHandler,
Expand Down
74 changes: 74 additions & 0 deletions src/tests/milkdown-headless-serializer-style-fidelity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { getHeadlessMilkdownParser, serializeMarkdown } from '../../server/milkdown-headless.js';

function assert(condition: boolean, message: string): void {
if (!condition) throw new Error(message);
}

async function run(): Promise<void> {
const parser = await getHeadlessMilkdownParser();

// Canonical source: dash bullets, triple-dash thematic breaks. This is the
// dominant style in CommonMark/GFM ecosystems (prettier, markdownlint, GitHub).
const source = [
'# Heading',
'',
'Paragraph one.',
'',
'- bullet one',
'- bullet two',
'- bullet three',
'',
'---',
'',
'Paragraph two.',
'',
'- nested list parent',
' - nested child a',
' - nested child b',
'',
'---',
'',
'Paragraph three.',
'',
].join('\n');

const doc = parser.parseMarkdown(source);
const out = await serializeMarkdown(doc);

// Style fidelity: the serializer must emit the same bullet and rule markers
// it received. Without remark-stringify { bullet: '-', rule: '-' }, output
// drifts to '*' bullets and '***' rules — semantically equivalent CommonMark
// but cosmetically destructive for any workflow that round-trips real docs.
assert(
!out.includes('* bullet'),
`Bullet drift: serializer emitted '*' bullets. Output was:\n${out}`,
);
assert(
out.includes('- bullet one') && out.includes('- bullet two') && out.includes('- bullet three'),
`Top-level bullets should serialize as '- '. Output was:\n${out}`,
);
assert(
!out.includes('***'),
`Thematic break drift: serializer emitted '***' rules. Output was:\n${out}`,
);
assert(
out.includes('\n---\n'),
`Thematic breaks should serialize as '---'. Output was:\n${out}`,
);

// Stability: serialize(parse(source)) === serialize(parse(serialize(parse(source))))
// (the property the existing roundtrip test checks; verifying we didn't regress it).
const reparsed = parser.parseMarkdown(out);
const out2 = await serializeMarkdown(reparsed);
assert(
out === out2,
`Serializer is not stable across round-trips. First output:\n${out}\nSecond output:\n${out2}`,
);

console.log('✓ headless Milkdown serializer style fidelity (bullets, thematic breaks)');
}

run().catch((error) => {
console.error(error);
process.exit(1);
});