diff options
author | 2022-05-31 19:16:43 +0200 | |
---|---|---|
committer | 2022-05-31 12:16:43 -0500 | |
commit | 119ecf8d469f034eaf1b1217523954d29f492cb6 (patch) | |
tree | fe952c1ee9f3b2c35b033d0a80cad33d5b93b6e5 | |
parent | e02c72f4452de76c584e06baf4696191889114bd (diff) | |
download | astro-119ecf8d469f034eaf1b1217523954d29f492cb6.tar.gz astro-119ecf8d469f034eaf1b1217523954d29f492cb6.tar.zst astro-119ecf8d469f034eaf1b1217523954d29f492cb6.zip |
Fix components in markdown regressions (#3486)
Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
Diffstat (limited to '')
9 files changed, 171 insertions, 47 deletions
diff --git a/.changeset/large-berries-grow.md b/.changeset/large-berries-grow.md new file mode 100644 index 000000000..81589045d --- /dev/null +++ b/.changeset/large-berries-grow.md @@ -0,0 +1,6 @@ +--- +'astro': patch +'@astrojs/markdown-remark': patch +--- + +Fix components in markdown regressions diff --git a/packages/astro/test/astro-markdown.test.js b/packages/astro/test/astro-markdown.test.js index e5daa1b46..b001c6a83 100644 --- a/packages/astro/test/astro-markdown.test.js +++ b/packages/astro/test/astro-markdown.test.js @@ -82,7 +82,6 @@ describe('Astro Markdown', () => { // https://github.com/withastro/astro/issues/3254 it('Can handle scripts in markdown pages', async () => { const html = await fixture.readFile('/script/index.html'); - console.log(html); expect(html).not.to.match(new RegExp('/src/scripts/test.js')); }); @@ -273,4 +272,42 @@ describe('Astro Markdown', () => { expect($('code').eq(2).text()).to.contain('title: import.meta.env.TITLE'); expect($('blockquote').text()).to.contain('import.meta.env.TITLE'); }); + + it('Escapes HTML tags in code blocks', async () => { + const html = await fixture.readFile('/code-in-md/index.html'); + const $ = cheerio.load(html); + + expect($('code').eq(0).html()).to.equal('<script>'); + expect($('blockquote').length).to.equal(1); + expect($('code').eq(1).html()).to.equal('</script>'); + expect($('pre').html()).to.contain('>This should also work without any problems.<'); + }); + + it('Allows defining slot contents in component children', async () => { + const html = await fixture.readFile('/slots/index.html'); + const $ = cheerio.load(html); + + const slots = $('article').eq(0); + expect(slots.find('> .fragmentSlot > div').text()).to.contain('1:'); + expect(slots.find('> .fragmentSlot > div + p').text()).to.contain('2:'); + expect(slots.find('> .pSlot > p[title="hello"]').text()).to.contain('3:'); + expect(slots.find('> .defaultSlot').text().replace(/\s+/g, ' ')).to.equal(` + 4: Div in default slot + 5: Paragraph in fragment in default slot + 6: Regular text in default slot + `.replace(/\s+/g, ' ')); + + const nestedSlots = $('article').eq(1); + expect(nestedSlots.find('> .fragmentSlot').html()).to.contain('1:'); + expect(nestedSlots.find('> .pSlot > p').text()).to.contain('2:'); + expect(nestedSlots.find('> .defaultSlot > article').text().replace(/\s+/g, ' ')).to.equal(` + 3: nested fragmentSlot + 4: nested pSlot + 5: nested text in default slot + `.replace(/\s+/g, ' ')); + + expect($('article').eq(3).text().replace(/[^❌]/g, '')).to.equal('❌❌❌'); + + expect($('article').eq(4).text().replace(/[^❌]/g, '')).to.equal('❌❌❌'); + }); }); diff --git a/packages/astro/test/fixtures/astro-markdown/src/components/SlotComponent.astro b/packages/astro/test/fixtures/astro-markdown/src/components/SlotComponent.astro new file mode 100644 index 000000000..f0aa9fc1c --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown/src/components/SlotComponent.astro @@ -0,0 +1,13 @@ +<article> + <section class="fragmentSlot"> + <slot name="fragmentSlot">❌ Missing content for slot "fragmentSlot"</slot> + </section> + + <section class="pSlot"> + <slot name="pSlot">❌ Missing content for slot "pSlot"</slot> + </section> + + <section class="defaultSlot"> + <slot>❌ Missing content for default slot</slot> + </section> +</article> diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/code-in-md.md b/packages/astro/test/fixtures/astro-markdown/src/pages/code-in-md.md new file mode 100644 index 000000000..52a799ab1 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown/src/pages/code-in-md.md @@ -0,0 +1,16 @@ +# Inline code blocks + +`<script>` tags in **Astro** components are now built, +bundled and optimized by default. + +> Markdown formatting still works between tags in inline code blocks. + +We can also use closing `</script>` tags without any problems. + +# Fenced code blocks + +```html +<body> + <div>This should also work without any problems.</div> +</body> +``` diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/slots.md b/packages/astro/test/fixtures/astro-markdown/src/pages/slots.md new file mode 100644 index 000000000..c7e8367b8 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown/src/pages/slots.md @@ -0,0 +1,38 @@ +--- +layout: ../layouts/content.astro +setup: import SlotComponent from '../components/SlotComponent.astro'; +--- + +# Component with slot contents in children + +<SlotComponent> + <div>4: Div in default slot</div> + <Fragment slot="fragmentSlot"> + <div>1: Div in fragmentSlot</div> + <p>2: Paragraph in fragmentSlot</p> + </Fragment> + <Fragment><p>5: Paragraph in fragment in default slot</p></Fragment> + 6: Regular text in default slot + <p slot="pSlot" title="hello">3: p with title as pSlot</p> +</SlotComponent> + +# Component with nested component in children + +<SlotComponent> + <p slot="pSlot">2: pSlot</p> + <SlotComponent> + <p slot="pSlot">4: nested pSlot</p> + 5: nested text in default slot + <Fragment slot="fragmentSlot">3: nested fragmentSlot</Fragment> + </SlotComponent> + <Fragment slot="fragmentSlot">1: fragmentSlot</Fragment> +</SlotComponent> + +# Missing content due to empty children + +<SlotComponent> +</SlotComponent> + +# Missing content due to self-closing tag + +<SlotComponent/> diff --git a/packages/markdown/remark/src/rehype-escape.ts b/packages/markdown/remark/src/rehype-escape.ts index bb56166be..5cf463608 100644 --- a/packages/markdown/remark/src/rehype-escape.ts +++ b/packages/markdown/remark/src/rehype-escape.ts @@ -5,6 +5,11 @@ export default function rehypeEscape(): any { return visit(node, 'element', (el) => { if (el.tagName === 'code' || el.tagName === 'pre') { el.properties['is:raw'] = true; + // Visit all raw children and escape HTML tags to prevent Markdown code + // like "This is a `<script>` tag" from actually opening a script tag + visit(el, 'raw', (raw) => { + raw.value = raw.value.replace(/</g, '<').replace(/>/g, '>'); + }); } return el; }); diff --git a/packages/markdown/remark/src/rehype-jsx.ts b/packages/markdown/remark/src/rehype-jsx.ts index 8549b2624..46c200b70 100644 --- a/packages/markdown/remark/src/rehype-jsx.ts +++ b/packages/markdown/remark/src/rehype-jsx.ts @@ -1,51 +1,49 @@ -import { map } from 'unist-util-map'; +import { visit } from 'unist-util-visit'; -const MDX_ELEMENTS = new Set(['mdxJsxFlowElement', 'mdxJsxTextElement']); +const MDX_ELEMENTS = ['mdxJsxFlowElement', 'mdxJsxTextElement']; export default function rehypeJsx(): any { return function (node: any): any { - return map(node, (child: any) => { - if (child.type === 'element') { - return { ...child, tagName: `${child.tagName}` }; - } - if (MDX_ELEMENTS.has(child.type)) { - const attrs = child.attributes.reduce((acc: any[], entry: any) => { - let attr = entry.value; - if (attr && typeof attr === 'object') { - attr = `{${attr.value}}`; - } else if (attr && entry.type === 'mdxJsxExpressionAttribute') { - attr = `{${attr}}`; - } else if (attr === null) { - attr = ''; - } else if (typeof attr === 'string') { - attr = `"${attr}"`; - } - if (!entry.name) { - return acc + ` ${attr}`; - } - return acc + ` ${entry.name}${attr ? '=' : ''}${attr}`; - }, ''); - - if (child.children.length === 0) { - return { - type: 'raw', - value: `<${child.name}${attrs} />`, - }; + visit(node, 'element', (child: any) => { + child.tagName = `${child.tagName}`; + }); + visit(node, MDX_ELEMENTS, (child: any, index: number | null, parent: any) => { + if (index === null || !Boolean(parent)) + return; + + const attrs = child.attributes.reduce((acc: any[], entry: any) => { + let attr = entry.value; + if (attr && typeof attr === 'object') { + attr = `{${attr.value}}`; + } else if (attr && entry.type === 'mdxJsxExpressionAttribute') { + attr = `{${attr}}`; + } else if (attr === null) { + attr = ''; + } else if (typeof attr === 'string') { + attr = `"${attr}"`; } - child.children.splice(0, 0, { - type: 'raw', - value: `\n<${child.name}${attrs}>`, - }); - child.children.push({ - type: 'raw', - value: `</${child.name}>\n`, - }); - return { - ...child, - type: 'element', - tagName: `Fragment`, - }; + if (!entry.name) { + return acc + ` ${attr}`; + } + return acc + ` ${entry.name}${attr ? '=' : ''}${attr}`; + }, ''); + + if (child.children.length === 0) { + child.type = 'raw'; + child.value = `<${child.name}${attrs} />`; + return; } - return child; + + // Replace the current child node with its children + // wrapped by raw opening and closing tags + const openingTag = { + type: 'raw', + value: `\n<${child.name}${attrs}>`, + }; + const closingTag = { + type: 'raw', + value: `</${child.name}>\n`, + }; + parent.children.splice(index, 1, openingTag, ...child.children, closingTag); }); }; } diff --git a/packages/markdown/remark/test/components.test.js b/packages/markdown/remark/test/components.test.js index 6e8017aa0..59816b76e 100644 --- a/packages/markdown/remark/test/components.test.js +++ b/packages/markdown/remark/test/components.test.js @@ -51,7 +51,18 @@ describe('components', () => { chai .expect(code) - .to.equal(`<Fragment>\n<Component bool={true}>Hello world!</Component>\n</Fragment>`); + .to.equal(`\n<Component bool={true}>Hello world!</Component>\n`); + }); + + it('should be able to nest components', async () => { + const { code } = await renderMarkdown( + `<Component bool={true}><Component>Hello world!</Component></Component>`, + {} + ); + + chai + .expect(code) + .to.equal(`\n<Component bool={true}>\n<Component>Hello world!</Component>\n</Component>\n`); }); it('should allow markdown without many spaces', async () => { @@ -65,7 +76,7 @@ describe('components', () => { chai .expect(code) .to.equal( - `<Fragment>\n<Component><h1 id="hello-world">Hello world!</h1></Component>\n</Fragment>` + `\n<Component><h1 id="hello-world">Hello world!</h1></Component>\n` ); }); }); diff --git a/packages/markdown/remark/test/expressions.test.js b/packages/markdown/remark/test/expressions.test.js index 17674c543..c3c341acd 100644 --- a/packages/markdown/remark/test/expressions.test.js +++ b/packages/markdown/remark/test/expressions.test.js @@ -11,7 +11,7 @@ describe('expressions', () => { it('should be able to serialize expression inside component', async () => { const { code } = await renderMarkdown(`<Component>{a}</Component>`, {}); - chai.expect(code).to.equal(`<Fragment>\n<Component>{a}</Component>\n</Fragment>`); + chai.expect(code).to.equal(`\n<Component>{a}</Component>\n`); }); it('should be able to serialize expression inside markdown', async () => { |