From 51a1129f3b149c3d1bb43d5a213705e4d9b91b68 Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Sat, 16 Dec 2023 08:49:26 -0300 Subject: [PATCH] Include the verbatim in the AST (#14) --- README.md | 20 +++ src/ast.ts | 1 + src/parsing.ts | 58 ++++++++- test/parsing.test.ts | 275 +++++++++++++++++++++++++++++++++++++++-- test/rendering.test.ts | 41 ++++-- 5 files changed, 374 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 484f018..b1dcd2a 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,26 @@ const jsx = render(markdown, { }); ``` +#### Handling unsupported features + +In some cases, you might want to intentionally omit certain features from your +rendered Markdown. For instance, if your platform doesn't support image rendering, +ou can simply return the original source text instead of trying to display the image. + +```ts +import {render, unescape} from '@croct/md-lite'; + +render(markdown, { + // ... other render functions + image: node => unescape(node.source), +}); +``` + +This code snippet will simply return the raw source code of the image node +instead of trying to render it as an image. You can adapt this approach +to handle any other unsupported feature by defining appropriate render +functions and accessing the relevant data from the AST. + ## Contributing Contributions to the package are always welcome! diff --git a/src/ast.ts b/src/ast.ts index 623a490..a9f3eb6 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -35,5 +35,6 @@ export type MarkdownNodeType = keyof MarkdownNodeMap; export type MarkdownNode = { [K in MarkdownNodeType]: MarkdownNodeMap[K] & { type: K, + source: string, } }[T]; diff --git a/src/parsing.ts b/src/parsing.ts index 5415bde..60b87c4 100644 --- a/src/parsing.ts +++ b/src/parsing.ts @@ -4,6 +4,24 @@ export function parse(markdown: string): MarkdownNode { return MarkdownParser.parse(markdown); } +export function unescape(input: string): string { + let text = ''; + + for (let index = 0; index < input.length; index++) { + const char = input[index]; + + if (char === '\\' && index + 1 < input.length) { + text += input[++index]; + + continue; + } + + text += char; + } + + return text; +} + class MismatchError extends Error { public constructor() { super('Mismatched token'); @@ -34,10 +52,16 @@ class MarkdownParser { const root: MarkdownNode<'fragment'> = { type: 'fragment', children: [], + source: '', }; + const startIndex = this.index; + let text = ''; + let paragraphStartIndex = this.index; + let textStartIndex = this.index; + while (!this.done) { const escapedText = this.parseText(''); @@ -52,6 +76,8 @@ class MarkdownParser { } if (this.matches(...MarkdownParser.NEW_PARAGRAPH)) { + const paragraphEndIndex = this.index; + while (MarkdownParser.NEWLINE.includes(this.current)) { this.advance(); } @@ -63,15 +89,19 @@ class MarkdownParser { paragraph = { type: 'paragraph', children: root.children, + source: '', }; root.children = [paragraph]; } + paragraph.source = this.getSlice(paragraphStartIndex, paragraphEndIndex); + if (text !== '') { paragraph.children.push({ type: 'text', content: text, + source: this.getSlice(textStartIndex, paragraphEndIndex), }); text = ''; @@ -80,13 +110,17 @@ class MarkdownParser { root.children.push({ type: 'paragraph', children: [], + source: '', }); } + paragraphStartIndex = this.index; + textStartIndex = this.index; + continue; } - const {index} = this; + const nodeStartIndex = this.index; let node: MarkdownNode|null = null; @@ -100,7 +134,7 @@ class MarkdownParser { } if (node === null) { - this.seek(index); + this.seek(nodeStartIndex); text += this.current; @@ -119,11 +153,14 @@ class MarkdownParser { parent.children.push({ type: 'text', content: text, + source: this.getSlice(textStartIndex, nodeStartIndex), }); } text = ''; + textStartIndex = this.index; + parent.children.push(node); } @@ -137,24 +174,32 @@ class MarkdownParser { parent.children.push({ type: 'text', content: text, + source: this.getSlice(textStartIndex, this.index), }); } const lastNode = root.children[root.children.length - 1]; - if (lastNode?.type === 'paragraph' && lastNode.children.length === 0) { - root.children.pop(); + if (lastNode?.type === 'paragraph') { + if (lastNode.children.length === 0) { + root.children.pop(); + } else { + lastNode.source = this.getSlice(paragraphStartIndex, this.index); + } } if (root.children.length === 1) { return root.children[0]; } + root.source = this.getSlice(startIndex, this.index); + return root; } private parseCurrent(): MarkdownNode|null { const char = this.lookAhead(); + const startIndex = this.index; switch (char) { case '*': @@ -170,6 +215,7 @@ class MarkdownParser { return { type: delimiter.length === 1 ? 'italic' : 'bold', children: children, + source: this.getSlice(startIndex, this.index), }; } @@ -183,6 +229,7 @@ class MarkdownParser { return { type: 'strike', children: children, + source: this.getSlice(startIndex, this.index), }; } @@ -206,6 +253,7 @@ class MarkdownParser { return { type: 'code', content: content, + source: this.getSlice(startIndex, this.index), }; } @@ -226,6 +274,7 @@ class MarkdownParser { type: 'image', src: src, alt: alt, + source: this.getSlice(startIndex, this.index), }; } @@ -244,6 +293,7 @@ class MarkdownParser { type: 'link', href: href, children: label, + source: this.getSlice(startIndex, this.index), }; } diff --git a/test/parsing.test.ts b/test/parsing.test.ts index 9f32859..50ee525 100644 --- a/test/parsing.test.ts +++ b/test/parsing.test.ts @@ -1,4 +1,4 @@ -import {parse} from '../src/parsing'; +import {parse, unescape} from '../src/parsing'; import {MarkdownNode} from '../src/ast'; describe('A Markdown parser function', () => { @@ -12,21 +12,26 @@ describe('A Markdown parser function', () => { input: 'First paragraph.\n\nSecond paragraph.', output: { type: 'fragment', + source: 'First paragraph.\n\nSecond paragraph.', children: [ { type: 'paragraph', + source: 'First paragraph.', children: [ { type: 'text', + source: 'First paragraph.', content: 'First paragraph.', }, ], }, { type: 'paragraph', + source: 'Second paragraph.', children: [ { type: 'text', + source: 'Second paragraph.', content: 'Second paragraph.', }, ], @@ -38,39 +43,48 @@ describe('A Markdown parser function', () => { input: 'First\n\nSecond\r\rThird\r\n\r\nFourth', output: { type: 'fragment', + source: 'First\n\nSecond\r\rThird\r\n\r\nFourth', children: [ { type: 'paragraph', + source: 'First', children: [ { type: 'text', + source: 'First', content: 'First', }, ], }, { type: 'paragraph', + source: 'Second', children: [ { type: 'text', + source: 'Second', content: 'Second', }, ], }, { type: 'paragraph', + source: 'Third', children: [ { type: 'text', + source: 'Third', content: 'Third', }, ], }, { type: 'paragraph', + source: 'Fourth', children: [ { type: 'text', + source: 'Fourth', content: 'Fourth', }, ], @@ -82,21 +96,26 @@ describe('A Markdown parser function', () => { input: '\n\n\r\nFirst paragraph.\n\n\r\nSecond paragraph.\n\n\r\n', output: { type: 'fragment', + source: '\n\n\r\nFirst paragraph.\n\n\r\nSecond paragraph.\n\n\r\n', children: [ { type: 'paragraph', + source: 'First paragraph.', children: [ { type: 'text', + source: 'First paragraph.', content: 'First paragraph.', }, ], }, { type: 'paragraph', + source: 'Second paragraph.', children: [ { type: 'text', + source: 'Second paragraph.', content: 'Second paragraph.', }, ], @@ -108,6 +127,7 @@ describe('A Markdown parser function', () => { input: '\n\r\r\n', output: { type: 'fragment', + source: '\n\r\r\n', children: [], }, }, @@ -115,21 +135,26 @@ describe('A Markdown parser function', () => { input: '\n\n\nFirst paragraph.\n\n\nSecond paragraph.\n\n\n\n', output: { type: 'fragment', + source: '\n\n\nFirst paragraph.\n\n\nSecond paragraph.\n\n\n\n', children: [ { type: 'paragraph', + source: 'First paragraph.', children: [ { type: 'text', + source: 'First paragraph.', content: 'First paragraph.', }, ], }, { type: 'paragraph', + source: 'Second paragraph.', children: [ { type: 'text', + source: 'Second paragraph.', content: 'Second paragraph.', }, ], @@ -146,25 +171,37 @@ describe('A Markdown parser function', () => { ].join('\n\n'), output: { type: 'fragment', + source: [ + '**First**\n_paragraph_', + '[Second paragraph](ex)', + '![Third paragraph](ex)', + 'Fourth paragraph', + ].join('\n\n'), children: [ { type: 'paragraph', + source: '**First**\n_paragraph_', children: [ { type: 'bold', + source: '**First**', children: { type: 'text', + source: 'First', content: 'First', }, }, { type: 'text', + source: '\n', content: '\n', }, { type: 'italic', + source: '_paragraph_', children: { type: 'text', + source: 'paragraph', content: 'paragraph', }, }, @@ -172,12 +209,15 @@ describe('A Markdown parser function', () => { }, { type: 'paragraph', + source: '[Second paragraph](ex)', children: [ { type: 'link', + source: '[Second paragraph](ex)', href: 'ex', children: { type: 'text', + source: 'Second paragraph', content: 'Second paragraph', }, }, @@ -185,9 +225,11 @@ describe('A Markdown parser function', () => { }, { type: 'paragraph', + source: '![Third paragraph](ex)', children: [ { type: 'image', + source: '![Third paragraph](ex)', src: 'ex', alt: 'Third paragraph', }, @@ -195,9 +237,11 @@ describe('A Markdown parser function', () => { }, { type: 'paragraph', + source: 'Fourth paragraph', children: [ { type: 'text', + source: 'Fourth paragraph', content: 'Fourth paragraph', }, ], @@ -209,6 +253,7 @@ describe('A Markdown parser function', () => { input: 'Hello, world!', output: { type: 'text', + source: 'Hello, world!', content: 'Hello, world!', }, }, @@ -216,20 +261,25 @@ describe('A Markdown parser function', () => { input: 'Hello, **world**!', output: { type: 'fragment', + source: 'Hello, **world**!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'bold', + source: '**world**', children: { type: 'text', + source: 'world', content: 'world', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -239,20 +289,25 @@ describe('A Markdown parser function', () => { input: 'Hello, \\**world**!', output: { type: 'fragment', + source: 'Hello, \\**world**!', children: [ { type: 'text', + source: 'Hello, \\*', content: 'Hello, *', }, { type: 'italic', + source: '*world*', children: { type: 'text', + source: 'world', content: 'world', }, }, { type: 'text', + source: '*!', content: '*!', }, ], @@ -262,20 +317,25 @@ describe('A Markdown parser function', () => { input: 'Hello, **world\\**!', output: { type: 'fragment', + source: 'Hello, **world\\**!', children: [ { type: 'text', + source: 'Hello, *', content: 'Hello, *', }, { type: 'italic', + source: '*world\\**', children: { type: 'text', + source: 'world\\*', content: 'world*', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -285,20 +345,25 @@ describe('A Markdown parser function', () => { input: 'Hello, **wor\\*\\*ld**!', output: { type: 'fragment', + source: 'Hello, **wor\\*\\*ld**!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'bold', + source: '**wor\\*\\*ld**', children: { type: 'text', + source: 'wor\\*\\*ld', content: 'wor**ld', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -308,6 +373,7 @@ describe('A Markdown parser function', () => { input: 'Hello, **\nworld**!', output: { type: 'text', + source: 'Hello, **\nworld**!', content: 'Hello, **\nworld**!', }, }, @@ -315,20 +381,25 @@ describe('A Markdown parser function', () => { input: 'Hello, **world***!', output: { type: 'fragment', + source: 'Hello, **world***!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'bold', + source: '**world**', children: { type: 'text', + source: 'world', content: 'world', }, }, { type: 'text', + source: '*!', content: '*!', }, ], @@ -338,20 +409,25 @@ describe('A Markdown parser function', () => { input: 'Hello, _world_!', output: { type: 'fragment', + source: 'Hello, _world_!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'italic', + source: '_world_', children: { type: 'text', + source: 'world', content: 'world', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -361,6 +437,7 @@ describe('A Markdown parser function', () => { input: 'Hello, \\_world_!', output: { type: 'text', + source: 'Hello, \\_world_!', content: 'Hello, _world_!', }, }, @@ -368,6 +445,7 @@ describe('A Markdown parser function', () => { input: 'Hello, _world\\_!', output: { type: 'text', + source: 'Hello, _world\\_!', content: 'Hello, _world_!', }, }, @@ -375,20 +453,25 @@ describe('A Markdown parser function', () => { input: 'Hello, _wor\\_ld_!', output: { type: 'fragment', + source: 'Hello, _wor\\_ld_!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'italic', + source: '_wor\\_ld_', children: { type: 'text', + source: 'wor\\_ld', content: 'wor_ld', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -398,6 +481,7 @@ describe('A Markdown parser function', () => { input: 'Hello, _\nworld_!', output: { type: 'text', + source: 'Hello, _\nworld_!', content: 'Hello, _\nworld_!', }, }, @@ -405,20 +489,25 @@ describe('A Markdown parser function', () => { input: 'Hello, *world*!', output: { type: 'fragment', + source: 'Hello, *world*!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'italic', + source: '*world*', children: { type: 'text', + source: 'world', content: 'world', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -428,6 +517,7 @@ describe('A Markdown parser function', () => { input: 'Hello, \\*world*!', output: { type: 'text', + source: 'Hello, \\*world*!', content: 'Hello, *world*!', }, }, @@ -435,6 +525,7 @@ describe('A Markdown parser function', () => { input: 'Hello, *world\\*!', output: { type: 'text', + source: 'Hello, *world\\*!', content: 'Hello, *world*!', }, }, @@ -442,20 +533,25 @@ describe('A Markdown parser function', () => { input: 'Hello, *wor\\*ld*!', output: { type: 'fragment', + source: 'Hello, *wor\\*ld*!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'italic', + source: '*wor\\*ld*', children: { type: 'text', + source: 'wor\\*ld', content: 'wor*ld', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -465,6 +561,7 @@ describe('A Markdown parser function', () => { input: 'Hello, *\nworld*!', output: { type: 'text', + source: 'Hello, *\nworld*!', content: 'Hello, *\nworld*!', }, }, @@ -472,23 +569,29 @@ describe('A Markdown parser function', () => { input: 'Hello, ***world***!', output: { type: 'fragment', + source: 'Hello, ***world***!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'bold', + source: '***world***', children: { type: 'italic', + source: '*world*', children: { type: 'text', + source: 'world', content: 'world', }, }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -498,20 +601,25 @@ describe('A Markdown parser function', () => { input: 'Hello, ~~world~~!', output: { type: 'fragment', + source: 'Hello, ~~world~~!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'strike', + source: '~~world~~', children: { type: 'text', + source: 'world', content: 'world', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -521,6 +629,7 @@ describe('A Markdown parser function', () => { input: 'Hello, \\~~world~~!', output: { type: 'text', + source: 'Hello, \\~~world~~!', content: 'Hello, ~~world~~!', }, }, @@ -528,6 +637,7 @@ describe('A Markdown parser function', () => { input: 'Hello, ~~world\\~~!', output: { type: 'text', + source: 'Hello, ~~world\\~~!', content: 'Hello, ~~world~~!', }, }, @@ -535,20 +645,25 @@ describe('A Markdown parser function', () => { input: 'Hello, ~~wor\\~\\~ld~~!', output: { type: 'fragment', + source: 'Hello, ~~wor\\~\\~ld~~!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'strike', + source: '~~wor\\~\\~ld~~', children: { type: 'text', + source: 'wor\\~\\~ld', content: 'wor~~ld', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -558,6 +673,7 @@ describe('A Markdown parser function', () => { input: 'Hello, ~~\nworld~~!', output: { type: 'text', + source: 'Hello, ~~\nworld~~!', content: 'Hello, ~~\nworld~~!', }, }, @@ -565,17 +681,21 @@ describe('A Markdown parser function', () => { input: 'Hello, `world`!', output: { type: 'fragment', + source: 'Hello, `world`!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'code', + source: '`world`', content: 'world', }, { type: 'text', + source: '!', content: '!', }, ], @@ -585,17 +705,21 @@ describe('A Markdown parser function', () => { input: 'Hello, ` world `!', output: { type: 'fragment', + source: 'Hello, ` world `!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'code', + source: '` world `', content: 'world', }, { type: 'text', + source: '!', content: '!', }, ], @@ -605,6 +729,7 @@ describe('A Markdown parser function', () => { input: 'Hello, \\`world`!', output: { type: 'text', + source: 'Hello, \\`world`!', content: 'Hello, `world`!', }, }, @@ -612,6 +737,7 @@ describe('A Markdown parser function', () => { input: 'Hello, `world\\`!', output: { type: 'text', + source: 'Hello, `world\\`!', content: 'Hello, `world`!', }, }, @@ -619,17 +745,21 @@ describe('A Markdown parser function', () => { input: 'Hello, `wor\\`ld`!', output: { type: 'fragment', + source: 'Hello, `wor\\`ld`!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'code', + source: '`wor\\`ld`', content: 'wor`ld', }, { type: 'text', + source: '!', content: '!', }, ], @@ -639,17 +769,21 @@ describe('A Markdown parser function', () => { input: 'Hello, ``world``!', output: { type: 'fragment', + source: 'Hello, ``world``!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'code', + source: '``world``', content: 'world', }, { type: 'text', + source: '!', content: '!', }, ], @@ -659,15 +793,21 @@ describe('A Markdown parser function', () => { input: 'Hello, `` world ``!', output: { type: 'fragment', + source: 'Hello, `` world ``!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', - }, { + }, + { type: 'code', + source: '`` world ``', content: 'world', - }, { + }, + { type: 'text', + source: '!', content: '!', }, ], @@ -677,17 +817,21 @@ describe('A Markdown parser function', () => { input: 'Hello, \\``world``!', output: { type: 'fragment', + source: 'Hello, \\``world``!', children: [ { type: 'text', + source: 'Hello, \\`', content: 'Hello, `', }, { type: 'code', + source: '`world`', content: 'world', }, { type: 'text', + source: '`!', content: '`!', }, ], @@ -697,17 +841,21 @@ describe('A Markdown parser function', () => { input: 'Hello, ``world\\``!', output: { type: 'fragment', + source: 'Hello, ``world\\``!', children: [ { type: 'text', + source: 'Hello, `', content: 'Hello, `', }, { type: 'code', + source: '`world\\``', content: 'world`', }, { type: 'text', + source: '!', content: '!', }, ], @@ -717,17 +865,21 @@ describe('A Markdown parser function', () => { input: 'Hello, ``wor\\`ld``!', output: { type: 'fragment', + source: 'Hello, ``wor\\`ld``!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'code', + source: '``wor\\`ld``', content: 'wor`ld', }, { type: 'text', + source: '!', content: '!', }, ], @@ -737,17 +889,21 @@ describe('A Markdown parser function', () => { input: 'Hello, ``wor`ld``!', output: { type: 'fragment', + source: 'Hello, ``wor`ld``!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'code', + source: '``wor`ld``', content: 'wor`ld', }, { type: 'text', + source: '!', content: '!', }, ], @@ -757,6 +913,7 @@ describe('A Markdown parser function', () => { input: 'Hello, `\nworld`!', output: { type: 'text', + source: 'Hello, `\nworld`!', content: 'Hello, `\nworld`!', }, }, @@ -764,21 +921,26 @@ describe('A Markdown parser function', () => { input: 'Hello, [world](image.png)!', output: { type: 'fragment', + source: 'Hello, [world](image.png)!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'link', + source: '[world](image.png)', href: 'image.png', children: { type: 'text', + source: 'world', content: 'world', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -788,6 +950,7 @@ describe('A Markdown parser function', () => { input: 'Hello, ```world```!', output: { type: 'text', + source: 'Hello, ```world```!', content: 'Hello, ```world```!', }, }, @@ -795,6 +958,7 @@ describe('A Markdown parser function', () => { input: 'Hello, ``world```!', output: { type: 'text', + source: 'Hello, ``world```!', content: 'Hello, ``world```!', }, }, @@ -802,6 +966,7 @@ describe('A Markdown parser function', () => { input: 'Hello, \\[world](image.png)!', output: { type: 'text', + source: 'Hello, \\[world](image.png)!', content: 'Hello, [world](image.png)!', }, }, @@ -809,21 +974,26 @@ describe('A Markdown parser function', () => { input: 'Hello, [wor\\[ld](image.png)!', output: { type: 'fragment', + source: 'Hello, [wor\\[ld](image.png)!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'link', + source: '[wor\\[ld](image.png)', href: 'image.png', children: { type: 'text', + source: 'wor\\[ld', content: 'wor[ld', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -833,21 +1003,26 @@ describe('A Markdown parser function', () => { input: 'Hello, [wor\\]ld](image.png)!', output: { type: 'fragment', + source: 'Hello, [wor\\]ld](image.png)!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'link', + source: '[wor\\]ld](image.png)', href: 'image.png', children: { type: 'text', + source: 'wor\\]ld', content: 'wor]ld', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -857,21 +1032,26 @@ describe('A Markdown parser function', () => { input: 'Hello, [world](https://\\(example.com)!', output: { type: 'fragment', + source: 'Hello, [world](https://\\(example.com)!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'link', + source: '[world](https://\\(example.com)', href: 'https://(example.com', children: { type: 'text', + source: 'world', content: 'world', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -881,21 +1061,26 @@ describe('A Markdown parser function', () => { input: 'Hello, [world](image.png\\))!', output: { type: 'fragment', + source: 'Hello, [world](image.png\\))!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'link', + source: '[world](image.png\\))', href: 'image.png)', children: { type: 'text', + source: 'world', content: 'world', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -905,22 +1090,30 @@ describe('A Markdown parser function', () => { input: 'Hello, [**world**](image.png)!', output: { type: 'fragment', + source: 'Hello, [**world**](image.png)!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', - }, { + }, + { type: 'link', + source: '[**world**](image.png)', href: 'image.png', children: { type: 'bold', + source: '**world**', children: { type: 'text', + source: 'world', content: 'world', }, }, - }, { + }, + { type: 'text', + source: '!', content: '!', }, ], @@ -930,18 +1123,22 @@ describe('A Markdown parser function', () => { input: 'Hello, ![world](image.png)!', output: { type: 'fragment', + source: 'Hello, ![world](image.png)!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'image', + source: '![world](image.png)', src: 'image.png', alt: 'world', }, { type: 'text', + source: '!', content: '!', }, ], @@ -951,21 +1148,26 @@ describe('A Markdown parser function', () => { input: 'Hello, \\![world](image.png)!', output: { type: 'fragment', + source: 'Hello, \\![world](image.png)!', children: [ { type: 'text', + source: 'Hello, \\!', content: 'Hello, !', }, { type: 'link', + source: '[world](image.png)', href: 'image.png', children: { type: 'text', + source: 'world', content: 'world', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -975,18 +1177,22 @@ describe('A Markdown parser function', () => { input: 'Hello, ![wor\\[ld](image.png)!', output: { type: 'fragment', + source: 'Hello, ![wor\\[ld](image.png)!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'image', + source: '![wor\\[ld](image.png)', src: 'image.png', alt: 'wor[ld', }, { type: 'text', + source: '!', content: '!', }, ], @@ -996,18 +1202,22 @@ describe('A Markdown parser function', () => { input: 'Hello, ![wor\\]ld](image.png)!', output: { type: 'fragment', + source: 'Hello, ![wor\\]ld](image.png)!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'image', + source: '![wor\\]ld](image.png)', src: 'image.png', alt: 'wor]ld', }, { type: 'text', + source: '!', content: '!', }, ], @@ -1017,18 +1227,22 @@ describe('A Markdown parser function', () => { input: 'Hello, ![world](https://\\(example.com)!', output: { type: 'fragment', + source: 'Hello, ![world](https://\\(example.com)!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'image', + source: '![world](https://\\(example.com)', src: 'https://(example.com', alt: 'world', }, { type: 'text', + source: '!', content: '!', }, ], @@ -1038,18 +1252,22 @@ describe('A Markdown parser function', () => { input: 'Hello, ![world](image.png\\))!', output: { type: 'fragment', + source: 'Hello, ![world](image.png\\))!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'image', + source: '![world](image.png\\))', src: 'image.png)', alt: 'world', }, { type: 'text', + source: '!', content: '!', }, ], @@ -1059,18 +1277,22 @@ describe('A Markdown parser function', () => { input: 'Hello, ![**world**](image.png)!', output: { type: 'fragment', + source: 'Hello, ![**world**](image.png)!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'image', + source: '![**world**](image.png)', src: 'image.png', alt: '**world**', }, { type: 'text', + source: '!', content: '!', }, ], @@ -1080,22 +1302,27 @@ describe('A Markdown parser function', () => { input: 'Hello, [![world](image.png)](https://example.com)!', output: { type: 'fragment', + source: 'Hello, [![world](image.png)](https://example.com)!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', }, { type: 'link', + source: '[![world](image.png)](https://example.com)', href: 'https://example.com', children: { type: 'image', + source: '![world](image.png)', src: 'image.png', alt: 'world', }, }, { type: 'text', + source: '!', content: '!', }, ], @@ -1105,24 +1332,33 @@ describe('A Markdown parser function', () => { input: 'Hello, **_~~`[world](image.png)`~~_**!', output: { type: 'fragment', + source: 'Hello, **_~~`[world](image.png)`~~_**!', children: [ { type: 'text', + source: 'Hello, ', content: 'Hello, ', - }, { + }, + { type: 'bold', + source: '**_~~`[world](image.png)`~~_**', children: { type: 'italic', + source: '_~~`[world](image.png)`~~_', children: { type: 'strike', + source: '~~`[world](image.png)`~~', children: { type: 'code', + source: '`[world](image.png)`', content: '[world](image.png)', }, }, }, - }, { + }, + { type: 'text', + source: '!', content: '!', }, ], @@ -1131,4 +1367,29 @@ describe('A Markdown parser function', () => { }))('should parse %s', (_, {input, output}) => { expect(parse(input)).toEqual(output); }); + + it.each([ + [ + 'Hello, \\*world\\*!', + 'Hello, *world*!', + ], + [ + '\\A\\B\\C', + 'ABC', + ], + [ + '\\\\\\', + '\\\\', + ], + [ + 'ABC\\', + 'ABC\\', + ], + [ + 'ABC\\D', + 'ABCD', + ], + ])('should unescape %s to %s', (input, output) => { + expect(unescape(input)).toEqual(output); + }); }); diff --git a/test/rendering.test.ts b/test/rendering.test.ts index a024b25..b643867 100644 --- a/test/rendering.test.ts +++ b/test/rendering.test.ts @@ -40,16 +40,30 @@ describe('A Markdown render function', () => { } } + const markdown = [ + '**Bold**', + '*Italic*', + '***Bold and italic***', + '~~Strike~~', + '`Code`', + '![Image](https://example.com/image.png)', + '[Link](https://example.com)', + ].join('\n\n'); + const tree: MarkdownNode = { type: 'fragment', + source: markdown, children: [ { type: 'paragraph', + source: '**Bold**', children: [ { type: 'bold', + source: '**Bold**', children: { type: 'text', + source: 'Bold', content: 'Bold', }, }, @@ -57,10 +71,13 @@ describe('A Markdown render function', () => { }, { type: 'paragraph', + source: '*Italic*', children: [ { type: 'italic', + source: '*Italic*', children: { + source: 'Italic', type: 'text', content: 'Italic', }, @@ -69,13 +86,17 @@ describe('A Markdown render function', () => { }, { type: 'paragraph', + source: '***Bold and italic***', children: [ { type: 'bold', + source: '***Bold and italic***', children: { type: 'italic', + source: '*Bold and italic*', children: { type: 'text', + source: 'Bold and italic', content: 'Bold and italic', }, }, @@ -84,11 +105,14 @@ describe('A Markdown render function', () => { }, { type: 'paragraph', + source: '~~Strike~~', children: [ { type: 'strike', + source: '~~Strike~~', children: { type: 'text', + source: 'Strike', content: 'Strike', }, }, @@ -96,18 +120,22 @@ describe('A Markdown render function', () => { }, { type: 'paragraph', + source: '`Code`', children: [ { type: 'code', + source: '`Code`', content: 'Code', }, ], }, { type: 'paragraph', + source: '![Image](https://example.com/image.png)', children: [ { type: 'image', + source: '![Image](https://example.com/image.png)', src: 'https://example.com/image.png', alt: 'Image', }, @@ -115,12 +143,15 @@ describe('A Markdown render function', () => { }, { type: 'paragraph', + source: '[Link](https://example.com)', children: [ { type: 'link', + source: '![Link](https://example.com)', href: 'https://example.com', children: { type: 'text', + source: 'https://example.com', content: 'Link', }, }, @@ -129,16 +160,6 @@ describe('A Markdown render function', () => { ], }; - const markdown = [ - '**Bold**', - '*Italic*', - '***Bold and italic***', - '~~Strike~~', - '`Code`', - '![Image](https://example.com/image.png)', - '[Link](https://example.com)', - ].join('\n\n'); - const html = [ '

Bold

', '

Italic

',