Fix components in markdown regressions (#3486)
Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
This commit is contained in:
parent
e02c72f445
commit
119ecf8d46
9 changed files with 171 additions and 47 deletions
6
.changeset/large-berries-grow.md
Normal file
6
.changeset/large-berries-grow.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'astro': patch
|
||||
'@astrojs/markdown-remark': patch
|
||||
---
|
||||
|
||||
Fix components in markdown regressions
|
|
@ -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('❌❌❌');
|
||||
});
|
||||
});
|
||||
|
|
13
packages/astro/test/fixtures/astro-markdown/src/components/SlotComponent.astro
vendored
Normal file
13
packages/astro/test/fixtures/astro-markdown/src/components/SlotComponent.astro
vendored
Normal file
|
@ -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>
|
16
packages/astro/test/fixtures/astro-markdown/src/pages/code-in-md.md
vendored
Normal file
16
packages/astro/test/fixtures/astro-markdown/src/pages/code-in-md.md
vendored
Normal file
|
@ -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>
|
||||
```
|
38
packages/astro/test/fixtures/astro-markdown/src/pages/slots.md
vendored
Normal file
38
packages/astro/test/fixtures/astro-markdown/src/pages/slots.md
vendored
Normal file
|
@ -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/>
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
Loading…
Reference in a new issue