mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
fix: Hard break serialization for commonMark (#12603)
* fix: Hard break serialization for commonMark * tests
This commit is contained in:
@@ -61,7 +61,7 @@ export default class ClipboardTextSerializer extends Extension {
|
|||||||
.map((node) => ProsemirrorHelper.toPlainText(node))
|
.map((node) => ProsemirrorHelper.toPlainText(node))
|
||||||
.join("\n")
|
.join("\n")
|
||||||
: mdSerializer.serialize(slice.content, {
|
: mdSerializer.serialize(slice.content, {
|
||||||
softBreak: true,
|
commonMark: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export class ProsemirrorHelper {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const markdown = serializer.serialize(doc, {
|
const markdown = serializer.serialize(doc, {
|
||||||
softBreak: true,
|
commonMark: true,
|
||||||
});
|
});
|
||||||
return markdown;
|
return markdown;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,18 @@
|
|||||||
// https://raw.githubusercontent.com/ProseMirror/prosemirror-markdown/master/src/to_markdown.js
|
// https://raw.githubusercontent.com/ProseMirror/prosemirror-markdown/master/src/to_markdown.js
|
||||||
// forked for table support
|
// 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
|
// ::- A specification for serializing a ProseMirror document as
|
||||||
// Markdown/CommonMark text.
|
// Markdown/CommonMark text.
|
||||||
|
|||||||
@@ -55,8 +55,10 @@ export default class HardBreak extends Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toMarkdown(state: MarkdownSerializerState) {
|
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.write(
|
||||||
state.inTable ? "<br>" : state.options.softBreak ? "\n" : "\\n"
|
state.inTable ? "<br>" : state.options.commonMark ? " \n" : "\\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default class Paragraph extends Node {
|
|||||||
node.childCount === 0 &&
|
node.childCount === 0 &&
|
||||||
!state.inTable
|
!state.inTable
|
||||||
) {
|
) {
|
||||||
state.write(state.options.softBreak ? "\n" : "\\\n");
|
state.write(state.options.commonMark ? "\n" : "\\\n");
|
||||||
} else {
|
} else {
|
||||||
state.renderInline(node);
|
state.renderInline(node);
|
||||||
state.closeBlock(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 {
|
interface ProsemirrorNode {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -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"
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user