|
| 1 | +import { describe, expect, test } from 'vitest' |
| 2 | + |
| 3 | +import { renderContentToHast } from '@/content-render/index' |
| 4 | +import { renderUnified, renderUnifiedToHast } from '@/content-render/unified/index' |
| 5 | +import type { Context } from '@/types' |
| 6 | + |
| 7 | +// A corpus that exercises the parts of the pipeline most likely to differ |
| 8 | +// between "stringify the processed vfile" (today) and "stringify the hast tree |
| 9 | +// we stopped at" (the new hast path): headings (slug + anchor links), code |
| 10 | +// blocks (highlight + code-header), tables (several rewrite plugins), alerts, |
| 11 | +// raw inline HTML (rehype-raw), and images. |
| 12 | +const fixtures: Array<{ name: string; template: string }> = [ |
| 13 | + { name: 'paragraph', template: 'Hello **world**, this is a [link](https://github.com).' }, |
| 14 | + { |
| 15 | + name: 'headings', |
| 16 | + template: '# Title\n\n## Section one\n\nSome text.\n\n### Subsection\n\nMore text.', |
| 17 | + }, |
| 18 | + { |
| 19 | + name: 'fenced code block', |
| 20 | + template: '```js\nconst x = 1\nconsole.log(x)\n```', |
| 21 | + }, |
| 22 | + { |
| 23 | + name: 'table', |
| 24 | + template: '| Col A | Col B |\n| --- | --- |\n| one | two |\n| three | four |', |
| 25 | + }, |
| 26 | + { |
| 27 | + name: 'ordered list with nesting', |
| 28 | + template: '1. First\n1. Second\n - nested a\n - nested b\n1. Third', |
| 29 | + }, |
| 30 | + { |
| 31 | + name: 'alert', |
| 32 | + template: '> [!NOTE]\n> This is an alert body with a [link](/foo).', |
| 33 | + }, |
| 34 | + { |
| 35 | + name: 'inline raw html', |
| 36 | + template: 'A paragraph with <kbd>Ctrl</kbd> and <em>emphasis</em> inline.', |
| 37 | + }, |
| 38 | + { |
| 39 | + name: 'blockquote and emphasis', |
| 40 | + template: '> A quote\n>\n> spanning lines with _emphasis_ and `code`.', |
| 41 | + }, |
| 42 | + { |
| 43 | + name: 'asset image (png -> picture)', |
| 44 | + template: '', |
| 45 | + }, |
| 46 | + { |
| 47 | + name: 'external image', |
| 48 | + template: '', |
| 49 | + }, |
| 50 | +] |
| 51 | + |
| 52 | +describe('renderUnifiedToHast', () => { |
| 53 | + test.each(fixtures)('derived html matches renderUnified for: $name', async ({ template }) => { |
| 54 | + const context = {} as Context |
| 55 | + const stringOutput = await renderUnified(template, structuredClone(context)) |
| 56 | + const { html, hast } = await renderUnifiedToHast(template, structuredClone(context)) |
| 57 | + |
| 58 | + expect(html).toBe(stringOutput) |
| 59 | + expect(hast).toBeTruthy() |
| 60 | + expect(hast.type).toBe('root') |
| 61 | + expect(Array.isArray(hast.children)).toBe(true) |
| 62 | + }) |
| 63 | + |
| 64 | + test('returns a hast root with element children for a heading', async () => { |
| 65 | + const { hast } = await renderUnifiedToHast('# Hello', {} as Context) |
| 66 | + const firstElement = hast.children.find((node) => node.type === 'element') |
| 67 | + expect(firstElement).toBeTruthy() |
| 68 | + expect(firstElement && 'tagName' in firstElement && firstElement.tagName).toBe('h1') |
| 69 | + }) |
| 70 | + |
| 71 | + test('strips position metadata from every node', async () => { |
| 72 | + const { hast } = await renderUnifiedToHast( |
| 73 | + '# Title\n\nA paragraph with a [link](/foo) and `code`.', |
| 74 | + {} as Context, |
| 75 | + ) |
| 76 | + const hasPosition = (node: unknown): boolean => { |
| 77 | + if (!node || typeof node !== 'object') return false |
| 78 | + if ('position' in node) return true |
| 79 | + const children = (node as { children?: unknown[] }).children |
| 80 | + return Array.isArray(children) && children.some(hasPosition) |
| 81 | + } |
| 82 | + expect(hasPosition(hast)).toBe(false) |
| 83 | + }) |
| 84 | +}) |
| 85 | + |
| 86 | +describe('renderContentToHast', () => { |
| 87 | + test('renders liquid before the unified pass', async () => { |
| 88 | + const context = { page: { foo: 'bar' } } as unknown as Context |
| 89 | + const { html, hast } = await renderContentToHast('A {{ page.foo }} value.', context) |
| 90 | + |
| 91 | + expect(html).toContain('A bar value.') |
| 92 | + expect(hast).toBeTruthy() |
| 93 | + expect(hast?.type).toBe('root') |
| 94 | + }) |
| 95 | + |
| 96 | + test('derived html matches the inner renderUnified path after liquid', async () => { |
| 97 | + const context = { page: { foo: 'bar' } } as unknown as Context |
| 98 | + const template = '# {{ page.foo }}\n\nWith a [link](/foo).' |
| 99 | + const { html } = await renderContentToHast(template, structuredClone(context)) |
| 100 | + const expected = await renderUnified('# bar\n\nWith a [link](/foo).', {} as Context) |
| 101 | + |
| 102 | + expect(html).toBe(expected) |
| 103 | + }) |
| 104 | + |
| 105 | + test('returns null hast for an empty template', async () => { |
| 106 | + const { html, hast } = await renderContentToHast('', {} as Context) |
| 107 | + expect(html).toBe('') |
| 108 | + expect(hast).toBeNull() |
| 109 | + }) |
| 110 | + |
| 111 | + test('throws when markdownRequested is set', async () => { |
| 112 | + await expect( |
| 113 | + renderContentToHast('# Hello', { markdownRequested: true } as Context), |
| 114 | + ).rejects.toThrow(/does not support markdownRequested/) |
| 115 | + }) |
| 116 | +}) |
0 commit comments