mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +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))
|
||||
.join("\n")
|
||||
: mdSerializer.serialize(slice.content, {
|
||||
softBreak: true,
|
||||
commonMark: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ export class ProsemirrorHelper {
|
||||
);
|
||||
|
||||
const markdown = serializer.serialize(doc, {
|
||||
softBreak: true,
|
||||
commonMark: true,
|
||||
});
|
||||
return markdown;
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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