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
|
// https://github.com/withastro/astro/issues/3254
|
||||||
it('Can handle scripts in markdown pages', async () => {
|
it('Can handle scripts in markdown pages', async () => {
|
||||||
const html = await fixture.readFile('/script/index.html');
|
const html = await fixture.readFile('/script/index.html');
|
||||||
console.log(html);
|
|
||||||
expect(html).not.to.match(new RegExp('/src/scripts/test.js'));
|
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($('code').eq(2).text()).to.contain('title: import.meta.env.TITLE');
|
||||||
expect($('blockquote').text()).to.contain('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) => {
|
return visit(node, 'element', (el) => {
|
||||||
if (el.tagName === 'code' || el.tagName === 'pre') {
|
if (el.tagName === 'code' || el.tagName === 'pre') {
|
||||||
el.properties['is:raw'] = true;
|
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;
|
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 {
|
export default function rehypeJsx(): any {
|
||||||
return function (node: any): any {
|
return function (node: any): any {
|
||||||
return map(node, (child: any) => {
|
visit(node, 'element', (child: any) => {
|
||||||
if (child.type === 'element') {
|
child.tagName = `${child.tagName}`;
|
||||||
return { ...child, tagName: `${child.tagName}` };
|
});
|
||||||
}
|
visit(node, MDX_ELEMENTS, (child: any, index: number | null, parent: any) => {
|
||||||
if (MDX_ELEMENTS.has(child.type)) {
|
if (index === null || !Boolean(parent))
|
||||||
const attrs = child.attributes.reduce((acc: any[], entry: any) => {
|
return;
|
||||||
let attr = entry.value;
|
|
||||||
if (attr && typeof attr === 'object') {
|
const attrs = child.attributes.reduce((acc: any[], entry: any) => {
|
||||||
attr = `{${attr.value}}`;
|
let attr = entry.value;
|
||||||
} else if (attr && entry.type === 'mdxJsxExpressionAttribute') {
|
if (attr && typeof attr === 'object') {
|
||||||
attr = `{${attr}}`;
|
attr = `{${attr.value}}`;
|
||||||
} else if (attr === null) {
|
} else if (attr && entry.type === 'mdxJsxExpressionAttribute') {
|
||||||
attr = '';
|
attr = `{${attr}}`;
|
||||||
} else if (typeof attr === 'string') {
|
} else if (attr === null) {
|
||||||
attr = `"${attr}"`;
|
attr = '';
|
||||||
}
|
} else if (typeof attr === 'string') {
|
||||||
if (!entry.name) {
|
attr = `"${attr}"`;
|
||||||
return acc + ` ${attr}`;
|
|
||||||
}
|
|
||||||
return acc + ` ${entry.name}${attr ? '=' : ''}${attr}`;
|
|
||||||
}, '');
|
|
||||||
|
|
||||||
if (child.children.length === 0) {
|
|
||||||
return {
|
|
||||||
type: 'raw',
|
|
||||||
value: `<${child.name}${attrs} />`,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
child.children.splice(0, 0, {
|
if (!entry.name) {
|
||||||
type: 'raw',
|
return acc + ` ${attr}`;
|
||||||
value: `\n<${child.name}${attrs}>`,
|
}
|
||||||
});
|
return acc + ` ${entry.name}${attr ? '=' : ''}${attr}`;
|
||||||
child.children.push({
|
}, '');
|
||||||
type: 'raw',
|
|
||||||
value: `</${child.name}>\n`,
|
if (child.children.length === 0) {
|
||||||
});
|
child.type = 'raw';
|
||||||
return {
|
child.value = `<${child.name}${attrs} />`;
|
||||||
...child,
|
return;
|
||||||
type: 'element',
|
|
||||||
tagName: `Fragment`,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
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
|
chai
|
||||||
.expect(code)
|
.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 () => {
|
it('should allow markdown without many spaces', async () => {
|
||||||
|
@ -65,7 +76,7 @@ describe('components', () => {
|
||||||
chai
|
chai
|
||||||
.expect(code)
|
.expect(code)
|
||||||
.to.equal(
|
.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 () => {
|
it('should be able to serialize expression inside component', async () => {
|
||||||
const { code } = await renderMarkdown(`<Component>{a}</Component>`, {});
|
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 () => {
|
it('should be able to serialize expression inside markdown', async () => {
|
||||||
|
|
Loading…
Reference in a new issue