diff --git a/packages/comark/SPEC/HTML/README.md b/packages/comark/SPEC/HTML/README.md index 64bc938f7..bc45e3313 100644 --- a/packages/comark/SPEC/HTML/README.md +++ b/packages/comark/SPEC/HTML/README.md @@ -1,6 +1,6 @@ # HTML parsing contract -Behavioral contract for embedded HTML. Fixtures in this directory are the user-facing cases. `test/html-block.test.ts` covers the option matrix and the cases deferred below. +Behavioral contract for embedded HTML. Fixtures in this directory are the user-facing cases. `test/html-tags.test.ts` covers tag parsing and rendering; `test/html-block.test.ts` covers the option matrix and the cases deferred below. HTML tags become element nodes with `$: { html: 1, block: 0 | 1 }`. A node without `$` is markdown-originated (`` from `**bold**` is `['strong', {}, 'bold']`). @@ -15,6 +15,8 @@ HTML tags become element nodes with `$: { html: 1, block: 0 | 1 }`. A node witho Void tags (`img`, `br`, …) are always `block: 0`. +A non-void open tag that follows text in its paragraph (`GET /apps/`, `- **a** c`) is prose, not a container. It ends with its paragraph, `block: 0`, unless a later paragraph of the same container holds its close tag. A close tag in a nested container (`::card`, a blockquote) or past the end of the container does not count. A tag that starts its paragraph keeps collecting the blocks after it. + Consecutive same-paragraph tags stay inline even if indented on following lines: ```md @@ -41,7 +43,13 @@ Blank-line bodies parse as markdown in both modes. CommonMark ends the HTML bloc `' + ) + + expect(result.nodes).toEqual([ + ['textarea', { $: block }, 'a < b'], + ['script', { $: block }, 'a < b'], + ['style', { $: block }, 'a < b'], + ]) + }) + + it('writes the decoded text of
 back as references', async () => {
+    const source = '
a <b>c</b> &amp; <!-- x -->
' + const first = await parseMarkdown(source) + const rendered = await renderMarkdown(first) + + expect(rendered).toBe('
a <b>c</b> &amp; <!-- x -->
') + expect((await parseMarkdown(rendered)).nodes).toEqual(first.nodes) + }) +}) + +describe('literal text round trip', () => { + async function roundTrip(source: string, options?: Parameters[1]) { + const first = await parseMarkdown(source, options) + const markdown = await renderMarkdown(first) + return { first, markdown, again: await parseMarkdown(markdown, options), html: await renderHtmlForTest(first) } + } + + it('shows an encoded reference in
, ',
+      '
&lt;b&gt;
', + ]) { + const { first, markdown, again, html: output } = await roundTrip(source, literal) + + expect(output).toContain('&lt;b&gt;') + expect(output).not.toContain('<b>') + expect(again.nodes).toEqual(first.nodes) + expect(markdown).toContain('&lt;b&gt;') + } + }) + + it('writes decoded < and & in a literal block back as references', async () => { + const { first, markdown, again, html: output } = await roundTrip('
\n<b> & x\n
', literal) + + expect(first.nodes).toEqual([['div', { $: block }, ' & x']]) + expect(markdown).toBe('
\n<b> & x\n
') + expect(again.nodes).toEqual(first.nodes) + expect(output).toBe('
\n <b> & x\n
') + }) + + it('keeps reference-looking text and a decoded AT&T']]) + expect(again.nodes).toEqual(first.nodes) + expect(output).toBe('
\n &foo; &copy &amp; &#65; <!-- c --> AT&T\n
') + expect(markdown).not.toContain(' a < b
', '
ac
']) { + const { first, markdown, again, html: output } = await roundTrip(source) + + expect(markdown).toBe(source) + expect(output).toBe(source) + expect(again.nodes).toEqual(first.nodes) + } + }) + + it('keeps a ') + + expect(first.nodes).toEqual([['textarea', { $: block }, 'a & b']]) + expect(markdown).toBe('') + expect(again.nodes).toEqual(first.nodes) + }) + + it('does not read markdown in a
 that follows text', async () => {
+    const source = 'before 
**x** < y
after' + const { first, markdown, again, html: output } = await roundTrip(source) + + expect(first.nodes).toEqual([['p', {}, 'before ', ['pre', { $: inline }, '**x** < y'], ' after']]) + expect(markdown).toBe(source) + expect(output).toBe('

before

**x** < y
after

') + expect(again.nodes).toEqual(first.nodes) + }) + + it('shows the decoded text of an inline tag as the text it spells', async () => { + const first = await parseMarkdown('
<b> &lt;
', literal) + + expect(await renderHtmlForTest(first)).toBe('
\n <b> &lt;\n
') + }) + + it('keeps a
 after text unindented in the paragraph', async () => {
+    const { html: output } = await roundTrip('before 
\nline\n  indented
after') + + expect(output).toBe('

before

\nline\n  indented
after

') + }) +}) + +describe('large inputs', () => { + // Correctness at a size where a quadratic scan would time out the suite; scaling is measured by hand. + it('parses many unpaired raw openers in one paragraph', async () => { + const result = await parseMarkdown('a