diff --git a/src/ast-types.ts b/src/ast-types.ts index f3e98c0..b6b1a83 100644 --- a/src/ast-types.ts +++ b/src/ast-types.ts @@ -115,6 +115,7 @@ export interface OptionType { linkDestinationKey?: string; imageSourceKey?: string; imageCaptionKey?: string; + lineBreakSymbol?: string; } export interface MdastNode { diff --git a/src/deserialize.ts b/src/deserialize.ts index 9d4322a..1d4dcda 100644 --- a/src/deserialize.ts +++ b/src/deserialize.ts @@ -33,6 +33,7 @@ export default function deserialize( const linkDestinationKey = opts?.linkDestinationKey ?? 'link'; const imageSourceKey = opts?.imageSourceKey ?? 'link'; const imageCaptionKey = opts?.imageCaptionKey ?? 'caption'; + const lineBreakSymbol = opts?.lineBreakSymbol ?? '
'; let children: Array> = [{ text: '' }]; @@ -87,11 +88,13 @@ export default function deserialize( } as CodeBlockNode; case 'html': - if (node.value?.includes('
')) { + if (node.value?.includes(lineBreakSymbol)) { + const regexp = new RegExp(lineBreakSymbol, 'g'); + return { break: true, type: types.paragraph, - children: [{ text: node.value?.replace(/
/g, '') || '' }], + children: [{ text: node.value?.replace(regexp, '') || '' }], } as ParagraphNode; } return { type: 'paragraph', children: [{ text: node.value || '' }] }; diff --git a/src/serialize.ts b/src/serialize.ts index d525d03..9ced8ec 100644 --- a/src/serialize.ts +++ b/src/serialize.ts @@ -2,9 +2,11 @@ import { BlockType, defaultNodeTypes, LeafType, NodeTypes } from './ast-types'; import escapeHtml from 'escape-html'; interface Options { - nodeTypes: NodeTypes; + nodeTypes?: NodeTypes; listDepth?: number; ignoreParagraphNewline?: boolean; + // lineBreakSymbol is used as an attempt to persist whitespace across serialize and deserialize + lineBreakSymbol?: string; } const isLeafNode = (node: BlockType | LeafType): node is LeafType => { @@ -13,16 +15,15 @@ const isLeafNode = (node: BlockType | LeafType): node is LeafType => { const VOID_ELEMENTS: Array = ['thematic_break', 'image']; -const BREAK_TAG = '
'; - export default function serialize( chunk: BlockType | LeafType, - opts: Options = { nodeTypes: defaultNodeTypes } + opts: Options = {} ) { const { nodeTypes: userNodeTypes = defaultNodeTypes, ignoreParagraphNewline = false, listDepth = 0, + lineBreakSymbol = '
', } = opts; let text = (chunk as LeafType).text || ''; @@ -73,6 +74,7 @@ export default function serialize( { ...c, parentType: type }, { nodeTypes, + lineBreakSymbol, // WOAH. // what we're doing here is pretty tricky, it relates to the block below where // we check for ignoreParagraphNewline and set type to paragraph. @@ -107,7 +109,7 @@ export default function serialize( chunk.parentType === nodeTypes.paragraph ) { type = nodeTypes.paragraph; - children = BREAK_TAG; + children = lineBreakSymbol; } if (children === '' && !VOID_ELEMENTS.find((k) => nodeTypes[k] === type)) @@ -120,7 +122,7 @@ export default function serialize( // we try applying formatting like to a node like this: // "Text foo bar **baz**" resulting in "**Text foo bar **baz****" // which is invalid markup and can mess everything up - if (children !== BREAK_TAG && isLeafNode(chunk)) { + if (children !== lineBreakSymbol && isLeafNode(chunk)) { if (chunk.strikeThrough && chunk.bold && chunk.italic) { children = retainWhitespaceAndFormat(children, '~~***'); } else if (chunk.bold && chunk.italic) { diff --git a/test/serialize/__snapshots__/serialize-lineBreakSymbol.test.ts.snap b/test/serialize/__snapshots__/serialize-lineBreakSymbol.test.ts.snap new file mode 100644 index 0000000..7ef813d --- /dev/null +++ b/test/serialize/__snapshots__/serialize-lineBreakSymbol.test.ts.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Deserialize and serialize mix of break tags and new lines 1`] = ` +Array [ + Object { + "children": Array [ + Object { + "text": "a +", + }, + Object { + "break": true, + "children": Array [ + Object { + "text": "", + }, + ], + "type": "paragraph", + }, + ], + "type": "paragraph", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "text": "https://a.com", + }, + ], + "link": "https://a.com", + "type": "link", + }, + Object { + "text": " +", + }, + Object { + "break": true, + "children": Array [ + Object { + "text": "", + }, + ], + "type": "paragraph", + }, + ], + "type": "paragraph", + }, + Object { + "children": Array [ + Object { + "text": "e", + }, + ], + "type": "paragraph", + }, +] +`; + +exports[`Deserialize heading with new line 1`] = ` +Array [ + Object { + "children": Array [ + Object { + "text": "heading one", + }, + ], + "type": "heading_one", + }, + Object { + "break": true, + "children": Array [ + Object { + "text": "", + }, + ], + "type": "paragraph", + }, + Object { + "children": Array [ + Object { + "text": "Foo", + }, + ], + "type": "paragraph", + }, +] +`; diff --git a/test/serialize/serialize-lineBreakSymbol.test.ts b/test/serialize/serialize-lineBreakSymbol.test.ts new file mode 100644 index 0000000..9488616 --- /dev/null +++ b/test/serialize/serialize-lineBreakSymbol.test.ts @@ -0,0 +1,53 @@ +import unified from 'unified'; +import parse from 'remark-parse'; +import plugin, { serialize } from '../../src'; +import { LeafType, BlockType } from '../../src/ast-types'; + +const mdown = `a + + +[https://a.com](https://a.com) + + +e +`; + +test('Deserialize and serialize mix of break tags and new lines', (done) => { + const lineBreakSymbol = ''; + + unified() + .use(parse) + .use(plugin, { lineBreakSymbol }) + .process(mdown, (_, file) => { + const res = file.result as (LeafType | BlockType)[]; + expect(res).toMatchSnapshot(); + + const fin = res.map((v) => serialize(v, { lineBreakSymbol })).join(''); + + expect(fin).toEqual(mdown); + done(); + }); +}); + +const headingWithNewLine = `# heading one + + +Foo +`; + +test('Deserialize heading with new line', (done) => { + unified() + .use(parse) + .use(plugin, { lineBreakSymbol: '' }) + .process(headingWithNewLine, (_, file) => { + const res = file.result as (LeafType | BlockType)[]; + expect(res).toMatchSnapshot(); + + const fin = res + .map((v) => serialize(v, { lineBreakSymbol: '' })) + .join(''); + + expect(fin).toEqual(headingWithNewLine); + done(); + }); +});