UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

229 lines 9.34 kB
import { describe, expect, it } from 'vitest'; import { parseInline, parseMarkdown, toggleCheckbox } from './markdown-parser.js'; describe('parseInline', () => { it('should parse plain text', () => { const result = parseInline('hello world'); expect(result).toEqual([{ type: 'text', content: 'hello world' }]); }); it('should parse inline code', () => { const result = parseInline('use `const x = 1` here'); expect(result).toEqual([ { type: 'text', content: 'use ' }, { type: 'code', content: 'const x = 1' }, { type: 'text', content: ' here' }, ]); }); it('should parse bold with **', () => { const result = parseInline('this is **bold** text'); expect(result).toEqual([ { type: 'text', content: 'this is ' }, { type: 'bold', children: [{ type: 'text', content: 'bold' }] }, { type: 'text', content: ' text' }, ]); }); it('should parse italic with *', () => { const result = parseInline('this is *italic* text'); expect(result).toEqual([ { type: 'text', content: 'this is ' }, { type: 'italic', children: [{ type: 'text', content: 'italic' }] }, { type: 'text', content: ' text' }, ]); }); it('should parse bold+italic with ***', () => { const result = parseInline('this is ***bold italic*** text'); expect(result).toEqual([ { type: 'text', content: 'this is ' }, { type: 'bold', children: [{ type: 'italic', children: [{ type: 'text', content: 'bold italic' }] }] }, { type: 'text', content: ' text' }, ]); }); it('should parse links', () => { const result = parseInline('click [here](https://example.com) now'); expect(result).toEqual([ { type: 'text', content: 'click ' }, { type: 'link', href: 'https://example.com', children: [{ type: 'text', content: 'here' }] }, { type: 'text', content: ' now' }, ]); }); it('should parse images', () => { const result = parseInline('see ![alt text](image.png) here'); expect(result).toEqual([ { type: 'text', content: 'see ' }, { type: 'image', src: 'image.png', alt: 'alt text' }, { type: 'text', content: ' here' }, ]); }); it('should parse nested bold in link', () => { const result = parseInline('[**bold link**](url)'); expect(result).toEqual([ { type: 'link', href: 'url', children: [{ type: 'bold', children: [{ type: 'text', content: 'bold link' }] }], }, ]); }); it('should handle empty string', () => { expect(parseInline('')).toEqual([]); }); }); describe('parseMarkdown', () => { it('should return empty array for empty input', () => { expect(parseMarkdown('')).toEqual([]); }); it('should parse headings level 1–6', () => { const md = '# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6'; const result = parseMarkdown(md); expect(result).toHaveLength(6); for (let i = 0; i < 6; i++) { expect(result[i]).toMatchObject({ type: 'heading', level: i + 1 }); } }); it('should parse a paragraph', () => { const result = parseMarkdown('Hello world\nthis is a paragraph.'); expect(result).toEqual([ { type: 'paragraph', children: [{ type: 'text', content: 'Hello world this is a paragraph.' }], }, ]); }); it('should split paragraphs on blank lines', () => { const result = parseMarkdown('First paragraph.\n\nSecond paragraph.'); expect(result).toHaveLength(2); expect(result[0]).toMatchObject({ type: 'paragraph' }); expect(result[1]).toMatchObject({ type: 'paragraph' }); }); it('should parse fenced code blocks', () => { const md = '```typescript\nconst x = 1\nconsole.log(x)\n```'; const result = parseMarkdown(md); expect(result).toEqual([ { type: 'codeBlock', language: 'typescript', content: 'const x = 1\nconsole.log(x)', }, ]); }); it('should parse fenced code blocks without language', () => { const md = '```\nhello\n```'; const result = parseMarkdown(md); expect(result).toEqual([ { type: 'codeBlock', language: undefined, content: 'hello', }, ]); }); it('should parse horizontal rules', () => { for (const rule of ['---', '***', '___', '----', '****']) { const result = parseMarkdown(rule); expect(result).toEqual([{ type: 'horizontalRule' }]); } }); it('should parse unordered lists', () => { const md = '- Item 1\n- Item 2\n- Item 3'; const result = parseMarkdown(md); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ type: 'list', ordered: false }); const list = result[0]; expect(list.items).toHaveLength(3); }); it('should parse ordered lists', () => { const md = '1. First\n2. Second\n3. Third'; const result = parseMarkdown(md); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ type: 'list', ordered: true }); const list = result[0]; expect(list.items).toHaveLength(3); }); it('should parse checkboxes in unordered lists', () => { const md = '- [x] Done task\n- [ ] Todo task\n- Regular item'; const result = parseMarkdown(md); expect(result).toHaveLength(1); const list = result[0]; expect(list.items[0].checkbox).toBe('checked'); expect(list.items[1].checkbox).toBe('unchecked'); expect(list.items[2].checkbox).toBeUndefined(); }); it('should track sourceLineIndex on list items', () => { const md = '- [x] Done\n- [ ] Todo'; const result = parseMarkdown(md); const list = result[0]; expect(list.items[0].sourceLineIndex).toBe(0); expect(list.items[1].sourceLineIndex).toBe(1); }); it('should parse blockquotes', () => { const md = '> This is a quote\n> Second line'; const result = parseMarkdown(md); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ type: 'blockquote' }); const bq = result[0]; expect(bq.children).toHaveLength(1); expect(bq.children[0]).toMatchObject({ type: 'paragraph' }); }); it('should parse a complex document', () => { const md = [ '# Title', '', 'A paragraph with **bold** and *italic*.', '', '## List Section', '', '- [x] Task done', '- [ ] Task pending', '', '> A blockquote', '', '---', '', '```js', 'console.log("hi")', '```', ].join('\n'); const result = parseMarkdown(md); expect(result[0]).toMatchObject({ type: 'heading', level: 1 }); expect(result[1]).toMatchObject({ type: 'paragraph' }); expect(result[2]).toMatchObject({ type: 'heading', level: 2 }); expect(result[3]).toMatchObject({ type: 'list', ordered: false }); expect(result[4]).toMatchObject({ type: 'blockquote' }); expect(result[5]).toMatchObject({ type: 'horizontalRule' }); expect(result[6]).toMatchObject({ type: 'codeBlock', language: 'js' }); }); }); describe('toggleCheckbox', () => { it('should toggle an unchecked checkbox to checked', () => { const source = '- [ ] Todo item'; const result = toggleCheckbox(source, 0); expect(result).toBe('- [x] Todo item'); }); it('should toggle a checked checkbox to unchecked', () => { const source = '- [x] Done item'; const result = toggleCheckbox(source, 0); expect(result).toBe('- [ ] Done item'); }); it('should toggle the correct line in a multi-line document', () => { const source = '- [x] Done\n- [ ] Todo\n- [ ] Another'; const result = toggleCheckbox(source, 1); expect(result).toBe('- [x] Done\n- [x] Todo\n- [ ] Another'); }); it('should return original string for out-of-bounds index', () => { const source = '- [ ] Todo'; expect(toggleCheckbox(source, 5)).toBe(source); expect(toggleCheckbox(source, -1)).toBe(source); }); it('should return original string for non-checkbox line', () => { const source = 'Regular text'; expect(toggleCheckbox(source, 0)).toBe(source); }); it('should not toggle [ ] that appears outside a list item', () => { const source = 'This line has [ ] in it but is not a list item'; expect(toggleCheckbox(source, 0)).toBe(source); }); it('should toggle checkboxes with * list marker', () => { const source = '* [ ] Star item'; expect(toggleCheckbox(source, 0)).toBe('* [x] Star item'); }); }); //# sourceMappingURL=markdown-parser.spec.js.map