fix: Hard break serialization for commonMark (#12603)

* fix: Hard break serialization for commonMark

* tests
This commit is contained in:
Tom Moor
2026-06-06 07:24:16 -04:00
committed by GitHub
parent be3f28afea
commit f329b56d0e
7 changed files with 113 additions and 6 deletions
@@ -61,7 +61,7 @@ export default class ClipboardTextSerializer extends Extension {
.map((node) => ProsemirrorHelper.toPlainText(node))
.join("\n")
: mdSerializer.serialize(slice.content, {
softBreak: true,
commonMark: true,
});
},
},
+1 -1
View File
@@ -29,7 +29,7 @@ export class ProsemirrorHelper {
);
const markdown = serializer.serialize(doc, {
softBreak: true,
commonMark: true,
});
return markdown;
};
+12 -1
View File
@@ -3,7 +3,18 @@
// https://raw.githubusercontent.com/ProseMirror/prosemirror-markdown/master/src/to_markdown.js
// forked for table support
type Options = { tightLists?: boolean; softBreak?: boolean };
/** Options that control how a ProseMirror document is serialized to Markdown. */
type Options = {
/** Whether list items are rendered without blank lines between them. */
tightLists?: boolean;
/**
* Whether to emit portable, standard CommonMark intended to leave Outline,
* such as when copying to the clipboard or exporting. When false the
* serializer uses Outline's internal escaped representation, which round-trips
* losslessly through its own parser but is not standard Markdown.
*/
commonMark?: boolean;
};
// ::- A specification for serializing a ProseMirror document as
// Markdown/CommonMark text.
+3 -1
View File
@@ -55,8 +55,10 @@ export default class HardBreak extends Node {
}
toMarkdown(state: MarkdownSerializerState) {
// Two trailing spaces is a CommonMark hard break that survives a
// copy/export round-trip, unlike a bare newline.
state.write(
state.inTable ? "<br>" : state.options.softBreak ? "\n" : "\\n"
state.inTable ? "<br>" : state.options.commonMark ? " \n" : "\\n"
);
}
+1 -1
View File
@@ -58,7 +58,7 @@ export default class Paragraph extends Node {
node.childCount === 0 &&
!state.inTable
) {
state.write(state.options.softBreak ? "\n" : "\\\n");
state.write(state.options.commonMark ? "\n" : "\\\n");
} else {
state.renderInline(node);
state.closeBlock(node);
@@ -1,4 +1,10 @@
import { parser, serializer } from "@server/editor";
import { extensionManager, schema } from "../../test/editor";
const serializer = extensionManager.serializer();
const parser = extensionManager.parser({
schema,
plugins: extensionManager.rulePlugins,
});
interface ProsemirrorNode {
type: string;
+88
View File
@@ -0,0 +1,88 @@
import { extensionManager, schema } from "../../test/editor";
const serializer = extensionManager.serializer();
const parser = extensionManager.parser({
schema,
plugins: extensionManager.rulePlugins,
});
const docWithHardBreak = schema.nodeFromJSON({
type: "doc",
content: [
{
type: "paragraph",
content: [
{ type: "text", text: "Line one" },
{ type: "br" },
{ type: "text", text: "Line two" },
],
},
],
});
it("parses two trailing spaces as a hard break", () => {
const ast = parser.parse("Line one \nLine two");
expect(ast?.toJSON()).toEqual(docWithHardBreak.toJSON());
});
it("parses a backslash line ending as a hard break", () => {
const ast = parser.parse("Line one\\\nLine two");
expect(ast?.toJSON()).toEqual(docWithHardBreak.toJSON());
});
it("serializes hard breaks as a CommonMark break when commonMark is set", () => {
// The commonMark option is used when copying to the clipboard and exporting
// documents two trailing spaces are a standard Markdown hard break that
// renders in external viewers and parses back into a `br`, unlike a bare
// newline.
expect(serializer.serialize(docWithHardBreak, { commonMark: true })).toBe(
"Line one \nLine two"
);
});
it("round-trips hard breaks through the copy/export serializer", () => {
let node = docWithHardBreak;
for (let i = 0; i < 3; i++) {
const markdown = serializer.serialize(node, { commonMark: true });
node = parser.parse(markdown)!;
expect(node.toJSON()).toEqual(docWithHardBreak.toJSON());
}
});
it("serializes hard breaks inside tables as a literal break tag", () => {
const docWithTable = schema.nodeFromJSON({
type: "doc",
content: [
{
type: "table",
content: [
{
type: "tr",
content: [
{
type: "td",
content: [
{
type: "paragraph",
content: [
{ type: "text", text: "Line one" },
{ type: "br" },
{ type: "text", text: "Line two" },
],
},
],
},
],
},
],
},
],
});
expect(serializer.serialize(docWithTable, { commonMark: true })).toContain(
"Line one<br>Line two"
);
});