/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import {Parser} from '@fluxer/markdown_parser/src/parser/Parser'; import {AlertType, NodeType, ParserFlags, TableAlignment} from '@fluxer/markdown_parser/src/types/Enums'; import type {BlockquoteNode, TextNode} from '@fluxer/markdown_parser/src/types/Nodes'; import {describe, expect, test} from 'vitest'; describe('Fluxer Markdown Parser', () => { test('heading without newlines', () => { const input = '# Heading 1\n## Heading 2\n### Heading 3'; const flags = ParserFlags.ALLOW_HEADINGS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toHaveLength(3); expect(ast[0]).toEqual({ type: NodeType.Heading, level: 1, children: [{type: NodeType.Text, content: 'Heading 1'}], }); expect(ast[1]).toEqual({ type: NodeType.Heading, level: 2, children: [{type: NodeType.Text, content: 'Heading 2'}], }); expect(ast[2]).toEqual({ type: NodeType.Heading, level: 3, children: [{type: NodeType.Text, content: 'Heading 3'}], }); }); test('heading with paragraph', () => { const input = '# Heading 1\nParagraph text\n## Heading 2'; const flags = ParserFlags.ALLOW_HEADINGS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toHaveLength(3); expect(ast[0]).toEqual({ type: NodeType.Heading, level: 1, children: [{type: NodeType.Text, content: 'Heading 1'}], }); expect(ast[1]).toEqual({type: NodeType.Text, content: 'Paragraph text'}); expect(ast[2]).toEqual({ type: NodeType.Heading, level: 2, children: [{type: NodeType.Text, content: 'Heading 2'}], }); }); test('multiple headings with blank lines', () => { const input = '# Heading 1\n\n## Heading 2\n\n### Heading 3\n\n#### Heading 4'; const flags = ParserFlags.ALLOW_HEADINGS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Heading, level: 1, children: [{type: NodeType.Text, content: 'Heading 1'}], }, { type: NodeType.Heading, level: 2, children: [{type: NodeType.Text, content: 'Heading 2'}], }, { type: NodeType.Heading, level: 3, children: [{type: NodeType.Text, content: 'Heading 3'}], }, { type: NodeType.Heading, level: 4, children: [{type: NodeType.Text, content: 'Heading 4'}], }, ]); }); test('malformed headings', () => { const input = '#Not a heading\n###### Too many hashes\n###No space'; const flags = ParserFlags.ALLOW_HEADINGS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ {type: NodeType.Text, content: '#Not a heading\n'}, {type: NodeType.Text, content: '###### Too many hashes\n'}, {type: NodeType.Text, content: '###No space'}, ]); }); test('heading after blank line', () => { const input = 'test\n\n# Test'; const flags = ParserFlags.ALLOW_HEADINGS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ {type: NodeType.Text, content: 'test\n'}, { type: NodeType.Heading, level: 1, children: [{type: NodeType.Text, content: 'Test'}], }, ]); }); test('heading before blank line', () => { const input = '# Test\n\ntest'; const flags = ParserFlags.ALLOW_HEADINGS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Heading, level: 1, children: [{type: NodeType.Text, content: 'Test'}], }, {type: NodeType.Text, content: 'test'}, ]); }); test('heading list spacing', () => { const input = '# Heading\n\n- Item 1\n- Item 2'; const flags = ParserFlags.ALLOW_LISTS | ParserFlags.ALLOW_HEADINGS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Heading, level: 1, children: [{type: NodeType.Text, content: 'Heading'}], }, { type: NodeType.List, ordered: false, items: [ {children: [{type: NodeType.Text, content: 'Item 1'}]}, {children: [{type: NodeType.Text, content: 'Item 2'}]}, ], }, ]); }); test('list heading spacing', () => { const input = '- Item 1\n- Item 2\n\n# Heading'; const flags = ParserFlags.ALLOW_LISTS | ParserFlags.ALLOW_HEADINGS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.List, ordered: false, items: [ {children: [{type: NodeType.Text, content: 'Item 1'}]}, {children: [{type: NodeType.Text, content: 'Item 2'}]}, ], }, { type: NodeType.Heading, level: 1, children: [{type: NodeType.Text, content: 'Heading'}], }, ]); }); test('blockquote', () => { const input = '> This is a blockquote.\n> It has two lines.'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'This is a blockquote.\nIt has two lines.'}], }, ]); }); test('blockquote with preserved > character and newline', () => { const input = '> fsdff\n> > fdfs'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'fsdff\n> fdfs'}], }, ]); }); test('blockquote disabled', () => { const input = '> This is a blockquote.\n> It has two lines.'; const flags = 0; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([{type: NodeType.Text, content: '> This is a blockquote.\n> It has two lines.'}]); }); test('multiline blockquote', () => { const input = ">>> This is an multiline blockquote.\nIt continues without '> ' prefix."; const flags = ParserFlags.ALLOW_MULTILINE_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [ {type: NodeType.Text, content: "This is an multiline blockquote.\nIt continues without '> ' prefix."}, ], }, ]); }); test('multiline blockquote disabled', () => { const input = ">>> This is an multiline blockquote.\nIt continues without '> ' prefix."; const flags = 0; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ {type: NodeType.Text, content: ">>> This is an multiline blockquote.\nIt continues without '> ' prefix."}, ]); }); test('code block', () => { const input = '```rust\nfn main() {\n println!("Hello, world!");\n}\n```'; const flags = ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toHaveLength(1); expect(ast[0]).toEqual({ type: NodeType.CodeBlock, language: 'rust', content: 'fn main() {\n println!("Hello, world!");\n}\n', }); }); test('code block edge cases', () => { const input = '```\nNo language specified\n```\n```invalid\nInvalid language\n```\n```rust no build\nWith extra tokens\n```'; const flags = ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toHaveLength(3); expect(ast[0]).toEqual({ type: NodeType.CodeBlock, language: undefined, content: 'No language specified\n', }); expect(ast[1]).toEqual({ type: NodeType.CodeBlock, language: 'invalid', content: 'Invalid language\n', }); expect(ast[2]).toEqual({ type: NodeType.CodeBlock, language: 'rust no build', content: 'With extra tokens\n', }); }); test('inline fenced block after text', () => { const input = 'a```b```'; const flags = ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ {type: NodeType.Text, content: 'a'}, {type: NodeType.CodeBlock, language: undefined, content: 'b'}, ]); }); test('inline fenced block with trailing text after closing fence', () => { const input = 'a```b```c'; const flags = ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ {type: NodeType.Text, content: 'a'}, {type: NodeType.CodeBlock, language: undefined, content: 'b'}, {type: NodeType.Text, content: 'c'}, ]); }); test('fenced block following inline text with multiline content', () => { const input = 'before ```\ncode line\n``` after'; const flags = ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ {type: NodeType.Text, content: 'before '}, {type: NodeType.CodeBlock, language: undefined, content: 'code line\n'}, {type: NodeType.Text, content: ' after'}, ]); }); test('captures trailing text after closing fence on its own line', () => { const input = '```\ncode line\n``` trailing'; const flags = ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ {type: NodeType.CodeBlock, language: undefined, content: 'code line\n'}, {type: NodeType.Text, content: ' trailing'}, ]); }); test('multiple blockquotes', () => { const input = '> First blockquote\n\n> Second blockquote\n> With multiple lines.'; const flags = ParserFlags.ALLOW_HEADINGS | ParserFlags.ALLOW_LISTS | ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'First blockquote'}], }, {type: NodeType.Text, content: '\n'}, { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'Second blockquote\nWith multiple lines.'}], }, ]); }); test('list followed by blank line and text', () => { const input = '- Test\n\nTest'; const flags = ParserFlags.ALLOW_LISTS | ParserFlags.ALLOW_HEADINGS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.List, ordered: false, items: [{children: [{type: NodeType.Text, content: 'Test'}]}], }, {type: NodeType.Text, content: '\nTest'}, ]); }); test('blockquote without continuation line prefix', () => { const input = '> **[Author](https://example.com)**\n> First line of quote.\nContinuation line without prefix.'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_MASKED_LINKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [ { type: NodeType.Strong, children: [ { type: NodeType.Link, text: {type: NodeType.Text, content: 'Author'}, url: 'https://example.com/', escaped: false, }, ], }, {type: NodeType.Text, content: '\nFirst line of quote.'}, ], }, {type: NodeType.Text, content: 'Continuation line without prefix.'}, ]); }); test('blockquote infinite loop', () => { const input = '> >>'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toBeDefined(); expect(ast.length).toBeGreaterThan(0); expect(ast.length).toBeLessThan(50); }); test('blockquote infinite loop alternate', () => { const input = '>> >'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toBeDefined(); expect(ast.length).toBeGreaterThan(0); expect(ast.length).toBeLessThan(50); }); test('blockquote list', () => { const input = '> - test'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_LISTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [ { type: NodeType.List, ordered: false, items: [{children: [{type: NodeType.Text, content: 'test'}]}], }, ], }, ]); }); test('multiline blockquote list', () => { const input = '>>> - test'; const flags = ParserFlags.ALLOW_MULTILINE_BLOCKQUOTES | ParserFlags.ALLOW_LISTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [ { type: NodeType.List, ordered: false, items: [{children: [{type: NodeType.Text, content: 'test'}]}], }, ], }, ]); }); test('blockquote with multiple lists', () => { const input = '> - test1\n> - test2'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_LISTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [ { type: NodeType.List, ordered: false, items: [ {children: [{type: NodeType.Text, content: 'test1'}]}, {children: [{type: NodeType.Text, content: 'test2'}]}, ], }, ], }, ]); }); test('blockquote with paragraph and list', () => { const input = '> This is a paragraph.\n>\n> - Item 1\n> - Item 2'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_LISTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'This is a paragraph.'}], }, {type: NodeType.Text, content: '>'}, { type: NodeType.Blockquote, children: [ { type: NodeType.List, ordered: false, items: [ {children: [{type: NodeType.Text, content: 'Item 1'}]}, {children: [{type: NodeType.Text, content: 'Item 2'}]}, ], }, ], }, ]); }); test('blockquote without continuation line prefix 2', () => { const input = '> This is in a blockquote.\n>\n> This is in another blockquote.'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'This is in a blockquote.'}], }, {type: NodeType.Text, content: '>'}, { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'This is in another blockquote.'}], }, ]); }); test('blockquote with blank lines', () => { const input = '> First paragraph\n> \n> Second paragraph'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'First paragraph\n\nSecond paragraph'}], }, ]); }); test('multiline blockquote with blank lines', () => { const input = '>>> First paragraph\n\nSecond paragraph'; const flags = ParserFlags.ALLOW_MULTILINE_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'First paragraph\n\nSecond paragraph'}], }, ]); }); test('blockquote with multiple blank lines should create consistent newlines', () => { const input = '> Line one\n> \n> \n> Line two'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'Line one\n\n\nLine two'}], }, ]); }); test('blockquote vs multiline blockquote with blank lines should behave the same', () => { const blockquoteInput = '> Test\n> \n> test'; const blockquoteFlags = ParserFlags.ALLOW_BLOCKQUOTES; const blockquoteParser = new Parser(blockquoteInput, blockquoteFlags); const {nodes: blockquoteAst} = blockquoteParser.parse(); const multilineInput = '>>> Test\n\ntest'; const multilineFlags = ParserFlags.ALLOW_MULTILINE_BLOCKQUOTES; const multilineParser = new Parser(multilineInput, multilineFlags); const {nodes: multilineAst} = multilineParser.parse(); const blockquoteContent = (blockquoteAst[0] as BlockquoteNode).children[0] as TextNode; const multilineContent = (multilineAst[0] as BlockquoteNode).children[0] as TextNode; expect(blockquoteContent.content).toBe('Test\n\ntest'); expect(multilineContent.content).toBe('Test\n\ntest'); }); test('blockquote with > without space should not be parsed as part of blockquote', () => { const input = '> First line\n>\n> Second line'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'First line'}], }, {type: NodeType.Text, content: '>'}, { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'Second line'}], }, ]); }); test('blockquote with multiple > spaces', () => { const input = '> First line\n> \n> \n> Second line'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'First line\n\n\nSecond line'}], }, ]); }); test('blockquote nested list', () => { const input = '> - Item 1\n> - Subitem 1\n> - Subitem 2\n> - Item 2'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_LISTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [ { type: NodeType.List, ordered: false, items: [ { children: [ {type: NodeType.Text, content: 'Item 1'}, { type: NodeType.List, ordered: false, items: [ {children: [{type: NodeType.Text, content: 'Subitem 1'}]}, {children: [{type: NodeType.Text, content: 'Subitem 2'}]}, ], }, ], }, {children: [{type: NodeType.Text, content: 'Item 2'}]}, ], }, ], }, ]); }); test('double arrow blockquote', () => { const input = '>> test'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([{type: NodeType.Text, content: '>> test'}]); }); test('double spaced arrow blockquote', () => { const input = '> > test'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: '> test'}], }, ]); }); test('multiple arrows blockquote', () => { const input = '> > > > deeply nested'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: '> > > deeply nested'}], }, ]); }); test('multiline blockquote with blockquote', () => { const input = 'test\n>>> test\ntest\n\ntest\n> test'; const flags = ParserFlags.ALLOW_MULTILINE_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ {type: NodeType.Text, content: 'test'}, { type: NodeType.Blockquote, children: [ {type: NodeType.Text, content: 'test\ntest\n\ntest'}, { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'test'}], }, ], }, ]); }); test('multiline blockquote with double nested blockquote', () => { const input = 'test\n>>> test\ntest\n\ntest\n> test\n> > should not nest'; const flags = ParserFlags.ALLOW_MULTILINE_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ {type: NodeType.Text, content: 'test'}, { type: NodeType.Blockquote, children: [ {type: NodeType.Text, content: 'test\ntest\n\ntest'}, { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'test\n> should not nest'}], }, ], }, ]); }); test('basic subtext', () => { const input = '-# This is subtext'; const flags = ParserFlags.ALLOW_SUBTEXT; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Subtext, children: [{type: NodeType.Text, content: 'This is subtext'}], }, ]); }); test('subtext disabled', () => { const input = '-# This is subtext'; const flags = 0; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([{type: NodeType.Text, content: '-# This is subtext'}]); }); test('subtext with formatting', () => { const input = '-# This is *formatted* and **bold** subtext'; const flags = ParserFlags.ALLOW_SUBTEXT; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Subtext, children: [ {type: NodeType.Text, content: 'This is '}, {type: NodeType.Emphasis, children: [{type: NodeType.Text, content: 'formatted'}]}, {type: NodeType.Text, content: ' and '}, {type: NodeType.Strong, children: [{type: NodeType.Text, content: 'bold'}]}, {type: NodeType.Text, content: ' subtext'}, ], }, ]); }); test('multiple subtexts', () => { const input = '-# First subtext\n-# Second subtext'; const flags = ParserFlags.ALLOW_SUBTEXT; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Subtext, children: [{type: NodeType.Text, content: 'First subtext'}], }, { type: NodeType.Subtext, children: [{type: NodeType.Text, content: 'Second subtext'}], }, ]); }); test('subtext with mixed content', () => { const input = '# Heading\n-# Subtext below heading\nNormal text\n-# Another subtext'; const flags = ParserFlags.ALLOW_SUBTEXT | ParserFlags.ALLOW_HEADINGS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Heading, level: 1, children: [{type: NodeType.Text, content: 'Heading'}], }, { type: NodeType.Subtext, children: [{type: NodeType.Text, content: 'Subtext below heading'}], }, {type: NodeType.Text, content: 'Normal text\n'}, { type: NodeType.Subtext, children: [{type: NodeType.Text, content: 'Another subtext'}], }, ]); }); test('malformed subtext', () => { const input = '-#No space\n-# Two spaces\n-## Extra hash'; const flags = ParserFlags.ALLOW_SUBTEXT; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ {type: NodeType.Text, content: '-#No space\n'}, {type: NodeType.Text, content: '-# Two spaces\n'}, {type: NodeType.Text, content: '-## Extra hash'}, ]); }); test('spoiler with block elements', () => { const input = '||\n# Heading\n- List item\n> Quote\n||'; const flags = ParserFlags.ALLOW_SPOILERS | ParserFlags.ALLOW_HEADINGS | ParserFlags.ALLOW_LISTS | ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Spoiler, isBlock: true, children: [ { type: NodeType.Heading, level: 1, children: [{type: NodeType.Text, content: 'Heading'}], }, { type: NodeType.List, ordered: false, items: [ { children: [{type: NodeType.Text, content: 'List item'}], }, ], }, { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'Quote'}], }, ], }, ]); }); test('spoiler with code blocks', () => { const input = '||\n```rust\nfn main() {}\n```\n||'; const flags = ParserFlags.ALLOW_SPOILERS | ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Spoiler, isBlock: true, children: [ { type: NodeType.CodeBlock, language: 'rust', content: 'fn main() {}\n', }, ], }, ]); }); test('spoiler with tables', () => { const input = '||\n| Header |\n|--------|\n| Cell |\n||'; const flags = ParserFlags.ALLOW_SPOILERS | ParserFlags.ALLOW_TABLES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Spoiler, isBlock: true, children: [ { type: NodeType.Table, header: { type: NodeType.TableRow, cells: [ { type: NodeType.TableCell, children: [{type: NodeType.Text, content: 'Header'}], }, ], }, alignments: [TableAlignment.None], rows: [ { type: NodeType.TableRow, cells: [ { type: NodeType.TableCell, children: [{type: NodeType.Text, content: 'Cell'}], }, ], }, ], }, ], }, ]); }); test('spoiler with alerts', () => { const input = '||\n> [!NOTE]\n> This is a note\n||'; const flags = ParserFlags.ALLOW_SPOILERS | ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_ALERTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Spoiler, isBlock: true, children: [ { type: NodeType.Alert, alertType: AlertType.Note, children: [{type: NodeType.Text, content: 'This is a note'}], }, ], }, ]); }); test('malformed spoiler blocks', () => { const inputs = ['|| incomplete spoiler\n# heading', '||\n# heading\n| incomplete', '|| # not a heading ||']; const flags = ParserFlags.ALLOW_SPOILERS | ParserFlags.ALLOW_HEADINGS; for (const input of inputs) { const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect( ast.every( (node) => node.type === NodeType.Text || node.type === NodeType.Heading || node.type === NodeType.Spoiler, ), ).toBe(true); } }); test('subtext with bracketed text followed by link', () => { const input = '-# [1] TL;DR: We use the [GNU Affero General Public License v3 (AGPLv3)](https://www.gnu.org/licenses/agpl-3.0.en.html) for our code.'; const flags = ParserFlags.ALLOW_SUBTEXT | ParserFlags.ALLOW_MASKED_LINKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Subtext, children: [ {type: NodeType.Text, content: '[1] TL;DR: We use the '}, { type: NodeType.Link, text: {type: NodeType.Text, content: 'GNU Affero General Public License v3 (AGPLv3)'}, url: 'https://www.gnu.org/licenses/agpl-3.0.en.html', escaped: false, }, {type: NodeType.Text, content: ' for our code.'}, ], }, ]); }); describe('Alert Parser', () => { test('basic note alert', () => { const input = '> [!NOTE]\n> This is a note'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_ALERTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Alert, alertType: AlertType.Note, children: [{type: NodeType.Text, content: 'This is a note'}], }, ]); }); test('alert with formatted content', () => { const input = '> [!WARNING]\n> This is **important** and *emphasized*'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_ALERTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Alert, alertType: AlertType.Warning, children: [ {type: NodeType.Text, content: 'This is '}, {type: NodeType.Strong, children: [{type: NodeType.Text, content: 'important'}]}, {type: NodeType.Text, content: ' and '}, {type: NodeType.Emphasis, children: [{type: NodeType.Text, content: 'emphasized'}]}, ], }, ]); }); test('multiline alert', () => { const input = '> [!IMPORTANT]\n> First line\n> Second line'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_ALERTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Alert, alertType: AlertType.Important, children: [{type: NodeType.Text, content: 'First line\nSecond line'}], }, ]); }); test('alert with nested content', () => { const input = '> [!TIP]\n> - List item 1\n> - List item 2\n> \n> Additional text'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_ALERTS | ParserFlags.ALLOW_LISTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Alert, alertType: AlertType.Tip, children: [ { type: NodeType.List, ordered: false, items: [ {children: [{type: NodeType.Text, content: 'List item 1'}]}, {children: [{type: NodeType.Text, content: 'List item 2'}]}, ], }, {type: NodeType.Text, content: '\nAdditional text'}, ], }, ]); }); test('invalid alert types', () => { const input = '> [!INVALID]\n> This should be a regular blockquote'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_ALERTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: '[!INVALID]\nThis should be a regular blockquote'}], }, ]); }); test('alert with blank lines', () => { const input = '> [!CAUTION]\n> First paragraph\n>\n> Second paragraph'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_ALERTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Alert, alertType: AlertType.Caution, children: [{type: NodeType.Text, content: 'First paragraph'}], }, {type: NodeType.Text, content: '>'}, { type: NodeType.Blockquote, children: [{type: NodeType.Text, content: 'Second paragraph'}], }, ]); }); test('alerts disabled with blockquotes', () => { const input = '> [!NOTE]\n> This should be a regular blockquote'; const flags = 0; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([{type: NodeType.Text, content: '> [!NOTE]\n> This should be a regular blockquote'}]); }); test('remove empty space between alerts', () => { const input = '> [!NOTE]\n> This is a note\n\n> [!WARNING]\n> This is a warning'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_ALERTS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Alert, alertType: AlertType.Note, children: [{type: NodeType.Text, content: 'This is a note'}], }, { type: NodeType.Alert, alertType: AlertType.Warning, children: [{type: NodeType.Text, content: 'This is a warning'}], }, ]); }); }); test('blockquote with bold, italic, nested quote and code block', () => { const input = '> **bold in quote**\n> *italic in quote*\n> > nested quote **inside**\n> ```code inside quote```'; const flags = ParserFlags.ALLOW_BLOCKQUOTES | ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.Blockquote, children: [ { type: NodeType.Strong, children: [{type: NodeType.Text, content: 'bold in quote'}], }, {type: NodeType.Text, content: '\n'}, { type: NodeType.Emphasis, children: [{type: NodeType.Text, content: 'italic in quote'}], }, {type: NodeType.Text, content: '\n> nested quote '}, { type: NodeType.Strong, children: [{type: NodeType.Text, content: 'inside'}], }, {type: NodeType.Text, content: '\n'}, { type: NodeType.CodeBlock, language: undefined, content: 'code inside quote', }, ], }, ]); }); test('inline code with backtick followed by code block', () => { const input = '`single backtick with ` inside`\n````md\nnested ```'; const flags = ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ {type: NodeType.InlineCode, content: 'single backtick with '}, {type: NodeType.Text, content: ' inside`\n'}, { type: NodeType.CodeBlock, language: 'md', content: 'nested ```\n', }, ]); }); test('single-line code block with backticks inside and inline code with triple backticks', () => { const input = '```code `inline inside` block```\n`inline ```block inside``` inline`'; const flags = ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([ { type: NodeType.CodeBlock, language: undefined, content: 'code `inline inside` block', }, {type: NodeType.InlineCode, content: 'inline ```block inside``` inline'}, ]); }); test('code block containing triple backticks with extra backtick', () => { const input = '```multi\n```multi\n````'; const flags = ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toHaveLength(2); expect(ast[0]).toEqual({ type: NodeType.CodeBlock, language: 'multi', content: '```multi\n', }); expect(ast[1]).toEqual({type: NodeType.Text, content: '`'}); }); describe('Edge cases and error handling', () => { test('invalid heading levels', () => { const input = '##### Too many hashes'; const flags = ParserFlags.ALLOW_HEADINGS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([{type: NodeType.Text, content: '##### Too many hashes'}]); }); test('multiline blockquote content handling', () => { const input = '>>> First line\nSecond line without prefix'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toHaveLength(1); expect(ast[0].type).toBe(NodeType.Text); }); test('invalid multiline blockquote start', () => { const input = '>> Not a multiline blockquote'; const flags = ParserFlags.ALLOW_BLOCKQUOTES; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toEqual([{type: NodeType.Text, content: '>> Not a multiline blockquote'}]); }); test('string cache performance optimization', () => { const repeatedInput = '# Same heading\n# Same heading\n# Same heading'; const flags = ParserFlags.ALLOW_HEADINGS; const parser = new Parser(repeatedInput, flags); const {nodes: ast} = parser.parse(); expect(ast).toHaveLength(3); expect(ast[0].type).toBe(NodeType.Heading); expect(ast[1].type).toBe(NodeType.Heading); expect(ast[2].type).toBe(NodeType.Heading); }); test('code block without closing fence', () => { const input = '```js\nconst x = 1;\n// no closing fence'; const flags = ParserFlags.ALLOW_CODE_BLOCKS; const parser = new Parser(input, flags); const {nodes: ast} = parser.parse(); expect(ast).toHaveLength(1); expect(ast[0].type).toBe(NodeType.CodeBlock); }); }); });