Feat: new legacy.astroFlavoredMarkdown flag (#4016)

* refactor: add legacy.jsxInMarkdown flag to config

* refactor: jsxInMarkdown -> astroFlavoredMarkdown

* refactor: remove `markdown.mode`

* feat: wire up legacy.astroFlavoredMarkdown

* test: add legacy to astro-markdown fixture

* test: remark autolinking

* test: remark components

* test: remark expressions

* test: remark strictness

* chore: remove "mode" from md component

* chore: remove "mode: md" from tests

* Fixing legacy MD tests, adding named slots tests for MDX pages

* chore: update lock file

* WIP: debugging named slots in MDX

* fix: handle named slots in MDX properly

* chore: re-enabling slots tests for MDX pages

* fixing test validation for svelte & vue

* removing unused Tailwind test

* legacy flag for Markdown component tests

* adding is:raw to Markdown component test

* adding is:raw to all Markdown component test fixtures

* can't use is:raw when nesting markdown components

* another nested test can't use is:raw

* one more <Markdown> test fix

* fixing another JSX markdown component test

* chore: add changeset

* e2e tests were missing the legacy flag

* removing the broken tailwind E2E markdown page

Co-authored-by: Tony Sullivan <tony.f.sullivan@outlook.com>
Co-authored-by: Nate Moore <nate@astro.build>
This commit is contained in:
Ben Holmes 2022-07-22 17:45:16 -05:00 committed by GitHub
parent c17efc1ad9
commit 00fab4ce13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 481 additions and 223 deletions

View file

@ -0,0 +1,20 @@
---
'astro': minor
'@astrojs/markdown-component': minor
'@astrojs/markdown-remark': minor
---
The use of components and JSX expressions in Markdown are no longer supported by default.
For long term support, migrate to the `@astrojs/mdx` integration for MDX support (including `.mdx` pages!).
Not ready to migrate to MDX? Add the legacy flag to your Astro config to re-enable the previous Markdown support.
```js
// https://astro.build/config
export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
}
});
```

View file

@ -3,5 +3,8 @@ import preact from '@astrojs/preact';
// https://astro.build/config
export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [preact({ compat: true })],
});

View file

@ -4,5 +4,8 @@ import mdx from '@astrojs/mdx';
// https://astro.build/config
export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [preact(), mdx()],
});

View file

@ -4,5 +4,8 @@ import mdx from '@astrojs/mdx';
// https://astro.build/config
export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [react(), mdx()],
});

View file

@ -4,5 +4,8 @@ import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [solid(), mdx()],
});

View file

@ -4,5 +4,8 @@ import mdx from '@astrojs/mdx';
// https://astro.build/config
export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [svelte(), mdx()],
});

View file

@ -1,11 +0,0 @@
---
title: "Markdown + Tailwind"
setup: |
import Button from '../components/Button.astro';
import Complex from '../components/Complex.astro';
---
<div class="grid place-items-center h-screen content-center">
<Button>Tailwind Button in Markdown!</Button>
<Complex />
</div>

View file

@ -4,6 +4,9 @@ import mdx from '@astrojs/mdx';
// https://astro.build/config
export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [
mdx(),
vue({

View file

@ -527,27 +527,6 @@ export interface AstroUserConfig {
*/
drafts?: boolean;
/**
* @docs
* @name markdown.mode
* @type {'md' | 'mdx'}
* @default `mdx`
* @description
* Control whether Markdown processing is done using MDX or not.
*
* MDX processing enables you to use JSX inside your Markdown files. However, there may be instances where you don't want this behavior, and would rather use a "vanilla" Markdown processor. This field allows you to control that behavior.
*
* ```js
* {
* markdown: {
* // Example: Use non-MDX processor for Markdown files
* mode: 'md',
* }
* }
* ```
*/
mode?: 'md' | 'mdx';
/**
* @docs
* @name markdown.shikiConfig
@ -716,6 +695,16 @@ export interface AstroUserConfig {
buildOptions?: never;
/** @deprecated `devOptions` has been renamed to `server` */
devOptions?: never;
legacy?: {
/**
* Enable components and JSX expressions in markdown
* Consider our MDX integration before applying this flag!
* @see https://docs.astro.build/en/guides/integrations-guide/mdx/
* Default: false
*/
astroFlavoredMarkdown?: boolean;
};
}
// NOTE(fks): We choose to keep our hand-generated AstroUserConfig interface so that

View file

@ -213,7 +213,10 @@ async function generatePath(
adapterName: undefined,
links,
logging,
markdown: astroConfig.markdown,
markdown: {
...astroConfig.markdown,
isAstroFlavoredMd: astroConfig.legacy.astroFlavoredMarkdown,
},
mod,
mode: opts.mode,
origin,

View file

@ -153,7 +153,10 @@ function buildManifest(
routes,
site: astroConfig.site,
base: astroConfig.base,
markdown: astroConfig.markdown,
markdown: {
...astroConfig.markdown,
isAstroFlavoredMd: astroConfig.legacy.astroFlavoredMarkdown,
},
pageMap: null as any,
renderers: [],
entryModules,

View file

@ -50,6 +50,9 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
rehypePlugins: [],
},
vite: {},
legacy: {
astroFlavoredMarkdown: false,
}
};
async function resolvePostcssConfig(inlineOptions: any, root: URL): Promise<PostCSSConfigResult> {
@ -172,9 +175,6 @@ export const AstroConfigSchema = z.object({
.default({}),
markdown: z
.object({
// NOTE: "mdx" allows us to parse/compile Astro components in markdown.
// TODO: This should probably be updated to something more like "md" | "astro"
mode: z.enum(['md', 'mdx']).default('mdx'),
drafts: z.boolean().default(false),
syntaxHighlight: z
.union([z.literal('shiki'), z.literal('prism'), z.literal(false)])
@ -212,6 +212,12 @@ export const AstroConfigSchema = z.object({
vite: z
.custom<ViteUserConfig>((data) => data instanceof Object && !Array.isArray(data))
.default(ASTRO_CONFIG_DEFAULTS.vite),
legacy: z
.object({
astroFlavoredMarkdown: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.legacy.astroFlavoredMarkdown),
})
.optional()
.default({}),
});
/** Turn raw config values into normalized values */

View file

@ -173,7 +173,10 @@ export async function render(
links,
styles,
logging,
markdown: astroConfig.markdown,
markdown: {
...astroConfig.markdown,
isAstroFlavoredMd: astroConfig.legacy.astroFlavoredMarkdown
},
mod,
mode,
origin,

View file

@ -29,23 +29,24 @@ export function transformSlots(vnode: AstroVNode) {
delete child.props.slot;
delete vnode.props.children;
}
if (!Array.isArray(vnode.props.children)) return;
// Handle many children with slot attributes
vnode.props.children = vnode.props.children
.map((child) => {
if (!isVNode(child)) return child;
if (!('slot' in child.props)) return child;
const name = toSlotName(child.props.slot);
if (Array.isArray(slots[name])) {
slots[name].push(child);
} else {
slots[name] = [child];
slots[name]['$$slot'] = true;
}
delete child.props.slot;
return Empty;
})
.filter((v) => v !== Empty);
if (Array.isArray(vnode.props.children)) {
// Handle many children with slot attributes
vnode.props.children = vnode.props.children
.map((child) => {
if (!isVNode(child)) return child;
if (!('slot' in child.props)) return child;
const name = toSlotName(child.props.slot);
if (Array.isArray(slots[name])) {
slots[name].push(child);
} else {
slots[name] = [child];
slots[name]['$$slot'] = true;
}
delete child.props.slot;
return Empty;
})
.filter((v) => v !== Empty);
}
Object.assign(vnode.props, slots);
}

View file

@ -19,6 +19,9 @@ let consoleFilterRefs = 0;
export async function renderJSX(result: SSRResult, vnode: any): Promise<any> {
switch (true) {
case vnode instanceof HTMLString:
if (vnode.toString().trim() === '') {
return '';
}
return vnode;
case typeof vnode === 'string':
return markHTMLString(escapeHTML(vnode));
@ -55,6 +58,9 @@ export async function renderJSX(result: SSRResult, vnode: any): Promise<any> {
}
if (vnode.type) {
if (typeof vnode.type === 'function' && (vnode.type as any)['astro:renderer']) {
skipAstroJSXCheck.add(vnode.type)
}
if (typeof vnode.type === 'function' && vnode.props['server:root']) {
const output = await vnode.type(vnode.props ?? {});
return await renderJSX(result, output);
@ -76,7 +82,7 @@ export async function renderJSX(result: SSRResult, vnode: any): Promise<any> {
}
const { children = null, ...props } = vnode.props ?? {};
const slots: Record<string, any> = {
const _slots: Record<string, any> = {
default: [],
};
function extractSlots(child: any): any {
@ -84,19 +90,32 @@ export async function renderJSX(result: SSRResult, vnode: any): Promise<any> {
return child.map((c) => extractSlots(c));
}
if (!isVNode(child)) {
return slots.default.push(child);
_slots.default.push(child);
return
}
if ('slot' in child.props) {
slots[child.props.slot] = [...(slots[child.props.slot] ?? []), child];
_slots[child.props.slot] = [...(_slots[child.props.slot] ?? []), child];
delete child.props.slot;
return;
}
slots.default.push(child);
_slots.default.push(child);
}
extractSlots(children);
for (const [key, value] of Object.entries(slots)) {
slots[key] = () => renderJSX(result, value);
for (const [key, value] of Object.entries(props)) {
if (value['$$slot']) {
_slots[key] = value;
delete props[key];
}
}
const slotPromises = [];
const slots: Record<string, any> = {};
for (const [key, value] of Object.entries(_slots)) {
slotPromises.push(renderJSX(result, value).then(output => {
if (output.toString().trim().length === 0) return;
slots[key] = () => output;
}))
}
await Promise.all(slotPromises);
let output: string | AsyncIterable<string>;
if (vnode.type === ClientOnlyPlaceholder && vnode.props['client:only']) {

View file

@ -137,7 +137,7 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
const filename = normalizeFilename(id);
const source = await fs.promises.readFile(filename, 'utf8');
const renderOpts = config.markdown;
const isMDX = renderOpts.mode === 'mdx';
const isAstroFlavoredMd = config.legacy.astroFlavoredMarkdown;
const fileUrl = new URL(`file://${filename}`);
const isPage = fileUrl.pathname.startsWith(resolvePages(config).pathname);
@ -149,7 +149,7 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
// Turn HTML comments into JS comments while preventing nested `*/` sequences
// from ending the JS comment by injecting a zero-width space
// Inside code blocks, this is removed during renderMarkdown by the remark-escape plugin.
if (isMDX) {
if (isAstroFlavoredMd) {
markdownContent = markdownContent.replace(
/<\s*!--([^-->]*)(.*?)-->/gs,
(whole) => `{/*${whole.replace(/\*\//g, '*\u200b/')}*/}`
@ -159,6 +159,7 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
let renderResult = await renderMarkdown(markdownContent, {
...renderOpts,
fileURL: fileUrl,
isAstroFlavoredMd,
} as any);
let { code: astroResult, metadata } = renderResult;
const { layout = '', components = '', setup = '', ...content } = frontmatter;
@ -168,9 +169,9 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
const prelude = `---
import Slugger from 'github-slugger';
${layout ? `import Layout from '${layout}';` : ''}
${isMDX && components ? `import * from '${components}';` : ''}
${isAstroFlavoredMd && components ? `import * from '${components}';` : ''}
${hasInjectedScript ? `import '${PAGE_SSR_SCRIPT_ID}';` : ''}
${isMDX ? setup : ''}
${isAstroFlavoredMd ? setup : ''}
const slugger = new Slugger();
function $$slug(value) {
@ -178,7 +179,7 @@ function $$slug(value) {
}
const $$content = ${JSON.stringify(
isMDX
isAstroFlavoredMd
? content
: // Avoid stripping "setup" and "components"
// in plain MD mode
@ -186,11 +187,11 @@ const $$content = ${JSON.stringify(
)}
---`;
const imports = `${layout ? `import Layout from '${layout}';` : ''}
${isMDX ? setup : ''}`.trim();
${isAstroFlavoredMd ? setup : ''}`.trim();
// Wrap with set:html fragment to skip
// JSX expressions and components in "plain" md mode
if (!isMDX) {
if (!isAstroFlavoredMd) {
astroResult = `<Fragment set:html={${JSON.stringify(astroResult)}} />`;
}

View file

@ -38,7 +38,6 @@ describe('Astro Markdown - plain MD mode', () => {
root: './fixtures/astro-markdown-md-mode/',
markdown: {
syntaxHighlight: 'prism',
mode: 'md',
},
});
await fixture.build();

View file

@ -2,5 +2,8 @@ import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: []
});

View file

@ -3,9 +3,6 @@ import svelte from "@astrojs/svelte";
// https://astro.build/config
export default defineConfig({
markdown: {
mode: 'md',
},
integrations: [svelte()],
site: 'https://astro.build/',
});

View file

@ -6,4 +6,7 @@ import svelte from "@astrojs/svelte";
export default defineConfig({
integrations: [preact(), svelte()],
site: 'https://astro.build/',
legacy: {
astroFlavoredMarkdown: true,
}
});

View file

@ -0,0 +1,9 @@
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: []
});

View file

@ -1,7 +1,11 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import preact from '@astrojs/preact';
// https://astro.build/config
export default defineConfig({
integrations: [preact()],
});
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [preact(), mdx()],
});

View file

@ -3,6 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/mdx": "workspace:*",
"@astrojs/preact": "workspace:*",
"astro": "workspace:*"
}

View file

@ -0,0 +1,7 @@
import Counter from '../components/Counter.jsx'
# Slots: Preact
<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
<Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
<Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>

View file

@ -1,7 +1,11 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
integrations: [react()],
});
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [react(), mdx()],
});

View file

@ -3,6 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/mdx": "workspace:*",
"@astrojs/react": "workspace:*",
"astro": "workspace:*",
"react": "^18.1.0",

View file

@ -0,0 +1,7 @@
import Counter from '../components/Counter.jsx'
# Slots: React
<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
<Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
<Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>

View file

@ -1,7 +1,11 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import solid from '@astrojs/solid-js';
// https://astro.build/config
export default defineConfig({
integrations: [solid()],
});
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [solid(), mdx()],
});

View file

@ -3,6 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/mdx": "workspace:*",
"@astrojs/solid-js": "workspace:*",
"astro": "workspace:*"
}

View file

@ -0,0 +1,7 @@
import Counter from '../components/Counter.jsx'
# Slots: Solid
<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
<Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
<Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>

View file

@ -1,7 +1,11 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import svelte from '@astrojs/svelte';
// https://astro.build/config
export default defineConfig({
integrations: [svelte()],
});
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [svelte(), mdx()],
});

View file

@ -3,6 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/mdx": "workspace:*",
"@astrojs/svelte": "workspace:*",
"astro": "workspace:*"
}

View file

@ -0,0 +1,7 @@
import Counter from '../components/Counter.svelte'
# Slots: Svelte
<Counter id="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
<Counter id="named" client:visible><h1 slot="named"> / Named</h1></Counter>
<Counter id="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>

View file

@ -1,7 +1,11 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import vue from '@astrojs/vue';
// https://astro.build/config
export default defineConfig({
integrations: [vue()],
});
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [vue(), mdx()],
});

View file

@ -3,6 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/mdx": "workspace:*",
"@astrojs/vue": "workspace:*",
"astro": "workspace:*"
}

View file

@ -0,0 +1,7 @@
import Counter from '../components/Counter.vue'
# Slots: Vue
<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
<Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
<Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>

View file

@ -3,10 +3,13 @@ import tailwind from '@astrojs/tailwind';
// https://astro.build/config
export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [tailwind()],
vite: {
build: {
assetsInlineLimit: 0,
},
},
});
});

View file

@ -53,4 +53,24 @@ describe('Slots: Preact', () => {
expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
});
});
describe('For MDX Pages', () => {
it('Renders default slot', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#content').text().trim()).to.equal('Hello world!');
});
it('Renders named slot', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#named').text().trim()).to.equal('Fallback / Named');
});
it('Converts dash-case slot to camelCase', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
});
});
});

View file

@ -53,4 +53,24 @@ describe('Slots: React', () => {
expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
});
});
describe('For MDX Pages', () => {
it('Renders default slot', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#content').text().trim()).to.equal('Hello world!');
});
it('Renders named slot', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#named').text().trim()).to.equal('Fallback / Named');
});
it('Converts dash-case slot to camelCase', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
});
});
});

View file

@ -53,4 +53,24 @@ describe('Slots: Solid', () => {
expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
});
});
describe('For MDX Pages', () => {
it('Renders default slot', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#content').text().trim()).to.equal('Hello world!');
});
it('Renders named slot', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#named').text().trim()).to.equal('Fallback / Named');
});
it('Converts dash-case slot to camelCase', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
});
});
});

View file

@ -53,4 +53,24 @@ describe('Slots: Svelte', () => {
expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
});
});
describe('For MDX Pages', () => {
it('Renders default slot', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#content').text().trim()).to.equal('Hello world!');
});
it('Renders named slot', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#named').text().trim()).to.equal('Fallback / Named');
});
it('Preserves dash-case slot', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
});
});
});

View file

@ -53,4 +53,24 @@ describe('Slots: Vue', () => {
expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
});
});
describe('For MDX Pages', () => {
it('Renders default slot', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#content').text().trim()).to.equal('Hello world!');
});
it('Renders named slot', async () => {
const html = await fixture.readFile('/mdx/index.html');
const $ = cheerio.load(html);
expect($('#named').text().trim()).to.equal('Fallback / Named');
});
it('Converts dash-case slot to camelCase', async () => {
const html = await fixture.readFile('/markdown/index.html');
const $ = cheerio.load(html);
expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
});
});
});

View file

@ -41,7 +41,6 @@ if (!content) {
if (content) {
htmlContent = await (Astro as any).__renderMarkdown(content, {
mode: 'md',
$: {
scopedClassName: className,
},

View file

@ -4,7 +4,7 @@ import Layout from '../layouts/content.astro';
---
<Layout>
<Markdown>
<Markdown is:raw>
# Hello world
</Markdown>
</Layout>

View file

@ -4,7 +4,7 @@ import Layout from '../layouts/content.astro';
---
<Layout>
<Markdown>
<Markdown is:raw>
# Hello world
```rinfo

View file

@ -4,7 +4,7 @@ import Layout from '../layouts/content.astro';
---
<Layout>
<Markdown>
<Markdown is:raw>
# Hello world
```

View file

@ -4,7 +4,7 @@ import Layout from '../layouts/content.astro';
---
<Layout>
<Markdown>
<Markdown is:raw>
# Hello world
```js

View file

@ -4,7 +4,7 @@ import Layout from '../layouts/content.astro';
---
<Layout>
<Markdown>
<Markdown is:raw>
# Hello world
```js

View file

@ -4,7 +4,7 @@ import Layout from '../layouts/content.astro';
---
<Layout>
<Markdown>
<Markdown is:raw>
# Hello world
```

View file

@ -4,7 +4,7 @@ import Layout from '../layouts/content.astro';
---
<Layout>
<Markdown>
<Markdown is:raw>
# Hello world
```

View file

@ -4,7 +4,7 @@ import Layout from '../layouts/content.astro';
---
<Layout>
<Markdown>
<Markdown is:raw>
# Hello world
```

View file

@ -4,6 +4,9 @@ import svelte from "@astrojs/svelte";
// https://astro.build/config
export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [preact(), svelte()],
site: 'https://astro.build/',
});

View file

@ -4,7 +4,7 @@ const title = 'My Blog Post';
const description = 'This is a post about some stuff.';
---
<Markdown>
<Markdown is:raw>
## Interesting Topic
```js

View file

@ -2,7 +2,6 @@
import Markdown from '@astrojs/markdown-component';
import Layout from '../layouts/content.astro';
import Hello from '../components/Hello.jsx';
import Counter from '../components/Counter.jsx';
const title = 'My Blog Post';
const description = 'This is a post about some stuff.';
@ -13,6 +12,5 @@ const description = 'This is a post about some stuff.';
## Interesting Topic
<Hello name={`world`} />
<Counter client:load />
</Markdown>
</Layout>

View file

@ -10,19 +10,19 @@ const description = 'This is a post about some stuff.';
<div id="deep">
<section class="a">
<Markdown>
<Markdown is:raw>
## A
</Markdown>
</section>
<section class="b">
<Markdown>
<Markdown is:raw>
## B
</Markdown>
</section>
<section class="c">
<Markdown>
<Markdown is:raw>
## C
</Markdown>
</section>

View file

@ -20,7 +20,7 @@ const content = `
<body>
<h1>Welcome to <a href="https://astro.build/">Astro</a></h1>
<div id="target">
<Markdown>
<Markdown is:raw>
- list
- nested list

View file

@ -2,4 +2,4 @@
import Markdown from '@astrojs/markdown-component';
---
<Markdown></Markdown>
<Markdown is:raw></Markdown>

View file

@ -29,24 +29,23 @@ export const DEFAULT_REHYPE_PLUGINS = [];
/** Shared utility for rendering markdown */
export async function renderMarkdown(
content: string,
opts: MarkdownRenderingOptions = {}
opts: MarkdownRenderingOptions,
): Promise<MarkdownRenderingResult> {
let {
fileURL,
mode = 'mdx',
syntaxHighlight = 'shiki',
shikiConfig = {},
remarkPlugins = [],
rehypePlugins = [],
isAstroFlavoredMd = false,
} = opts;
const input = new VFile({ value: content, path: fileURL });
const scopedClassName = opts.$?.scopedClassName;
const isMDX = mode === 'mdx';
const { headers, rehypeCollectHeaders } = createCollectHeaders();
let parser = unified()
.use(markdown)
.use(isMDX ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
.use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
if (remarkPlugins.length === 0 && rehypePlugins.length === 0) {
remarkPlugins = [...DEFAULT_REMARK_PLUGINS];
@ -75,7 +74,7 @@ export async function renderMarkdown(
markdownToHtml as any,
{
allowDangerousHtml: true,
passThrough: isMDX
passThrough: isAstroFlavoredMd
? [
'raw',
'mdxFlowExpression',
@ -94,7 +93,7 @@ export async function renderMarkdown(
parser
.use(
isMDX
isAstroFlavoredMd
? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeCollectHeaders]
: [rehypeCollectHeaders, rehypeRaw]
)

View file

@ -41,6 +41,7 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
$?: {
scopedClassName: string | null;
};
isAstroFlavoredMd?: boolean;
}
export interface MarkdownHeader {

View file

@ -2,91 +2,107 @@ import { renderMarkdown } from '../dist/index.js';
import chai from 'chai';
describe('autolinking', () => {
it('autolinks URLs starting with a protocol in plain text', async () => {
const { code } = await renderMarkdown(`See https://example.com for more.`, {});
chai
.expect(code.replace(/\n/g, ''))
.to.equal(`<p>See <a href="https://example.com">https://example.com</a> for more.</p>`);
});
it('autolinks URLs starting with "www." in plain text', async () => {
const { code } = await renderMarkdown(`See www.example.com for more.`, {});
chai
.expect(code.trim())
.to.equal(`<p>See <a href="http://www.example.com">www.example.com</a> for more.</p>`);
});
it('does not autolink URLs in code blocks', async () => {
const { code } = await renderMarkdown(
'See `https://example.com` or `www.example.com` for more.',
{}
);
chai
.expect(code.trim())
.to.equal(
`<p>See <code is:raw>https://example.com</code> or ` +
`<code is:raw>www.example.com</code> for more.</p>`
describe('plain md', () => {
it('autolinks URLs starting with a protocol in plain text', async () => {
const { code } = await renderMarkdown(`See https://example.com for more.`, {});
chai
.expect(code.replace(/\n/g, ''))
.to.equal(`<p>See <a href="https://example.com">https://example.com</a> for more.</p>`);
});
it('autolinks URLs starting with "www." in plain text', async () => {
const { code } = await renderMarkdown(`See www.example.com for more.`, {});
chai
.expect(code.trim())
.to.equal(`<p>See <a href="http://www.example.com">www.example.com</a> for more.</p>`);
});
it('does not autolink URLs in code blocks', async () => {
const { code } = await renderMarkdown(
'See `https://example.com` or `www.example.com` for more.',
{}
);
chai
.expect(code.trim())
.to.equal(
`<p>See <code>https://example.com</code> or ` +
`<code>www.example.com</code> for more.</p>`
);
});
});
it('does not autolink URLs in fenced code blocks', async () => {
const { code } = await renderMarkdown(
'Example:\n```\nGo to https://example.com or www.example.com now.\n```',
{}
);
describe('astro-flavored md', () => {
const renderAstroMd = text => renderMarkdown(text, { isAstroFlavoredMd: true });
chai
.expect(code)
.to.contain(`<pre is:raw`)
.to.contain(`Go to https://example.com or www.example.com now.`);
});
it('does not autolink URLs starting with a protocol when nested inside links', async () => {
const { code } = await renderMarkdown(
`See [http://example.com](http://example.com) or ` +
`<a test href="https://example.com">https://example.com</a>`,
{}
);
chai
.expect(code.replace(/\n/g, ''))
.to.equal(
`<p>See <a href="http://example.com">http://example.com</a> or ` +
`<a test href="https://example.com">https://example.com</a></p>`
it('does not autolink URLs in code blocks', async () => {
const { code } = await renderAstroMd(
'See `https://example.com` or `www.example.com` for more.',
{}
);
});
it('does not autolink URLs starting with "www." when nested inside links', async () => {
const { code } = await renderMarkdown(
`See [www.example.com](https://www.example.com) or ` +
`<a test href="https://www.example.com">www.example.com</a>`,
{}
);
chai
.expect(code.replace(/\n/g, ''))
.to.equal(
`<p>See <a href="https://www.example.com">www.example.com</a> or ` +
`<a test href="https://www.example.com">www.example.com</a></p>`
chai
.expect(code.trim())
.to.equal(
`<p>See <code is:raw>https://example.com</code> or ` +
`<code is:raw>www.example.com</code> for more.</p>`
);
});
it('does not autolink URLs in fenced code blocks', async () => {
const { code } = await renderAstroMd(
'Example:\n```\nGo to https://example.com or www.example.com now.\n```'
);
});
it('does not autolink URLs when nested several layers deep inside links', async () => {
const { code } = await renderMarkdown(
`<a href="https://www.example.com">**Visit _our www.example.com or ` +
`http://localhost pages_ for more!**</a>`,
{}
);
chai
.expect(code.replace(/\n/g, ''))
.to.equal(
`<a href="https://www.example.com"><strong>` +
`Visit <em>our www.example.com or http://localhost pages</em> for more!` +
`</strong></a>`
chai
.expect(code)
.to.contain(`<pre is:raw`)
.to.contain(`Go to https://example.com or www.example.com now.`);
});
it('does not autolink URLs starting with a protocol when nested inside links', async () => {
const { code } = await renderAstroMd(
`See [http://example.com](http://example.com) or ` +
`<a test href="https://example.com">https://example.com</a>`
);
});
chai
.expect(code.replace(/\n/g, ''))
.to.equal(
`<p>See <a href="http://example.com">http://example.com</a> or ` +
`<a test href="https://example.com">https://example.com</a></p>`
);
});
it('does not autolink URLs starting with "www." when nested inside links', async () => {
const { code } = await renderAstroMd(
`See [www.example.com](https://www.example.com) or ` +
`<a test href="https://www.example.com">www.example.com</a>`
);
chai
.expect(code.replace(/\n/g, ''))
.to.equal(
`<p>See <a href="https://www.example.com">www.example.com</a> or ` +
`<a test href="https://www.example.com">www.example.com</a></p>`
);
});
it('does not autolink URLs when nested several layers deep inside links', async () => {
const { code } = await renderAstroMd(
`<a href="https://www.example.com">**Visit _our www.example.com or ` +
`http://localhost pages_ for more!**</a>`
);
chai
.expect(code.replace(/\n/g, ''))
.to.equal(
`<a href="https://www.example.com"><strong>` +
`Visit <em>our www.example.com or http://localhost pages</em> for more!` +
`</strong></a>`
);
});
})
});

View file

@ -2,32 +2,34 @@ import { renderMarkdown } from '../dist/index.js';
import chai from 'chai';
describe('components', () => {
const renderAstroMd = (text) => renderMarkdown(text, { isAstroFlavoredMd: true });
it('should be able to serialize string', async () => {
const { code } = await renderMarkdown(`<Component str="cool!" />`, {});
const { code } = await renderAstroMd(`<Component str="cool!" />`);
chai.expect(code).to.equal(`<Component str="cool!" />`);
});
it('should be able to serialize boolean attribute', async () => {
const { code } = await renderMarkdown(`<Component bool={true} />`, {});
const { code } = await renderAstroMd(`<Component bool={true} />`);
chai.expect(code).to.equal(`<Component bool={true} />`);
});
it('should be able to serialize array', async () => {
const { code } = await renderMarkdown(`<Component prop={["a", "b", "c"]} />`, {});
const { code } = await renderAstroMd(`<Component prop={["a", "b", "c"]} />`);
chai.expect(code).to.equal(`<Component prop={["a", "b", "c"]} />`);
});
it('should be able to serialize object', async () => {
const { code } = await renderMarkdown(`<Component prop={{ a: 0, b: 1, c: 2 }} />`, {});
const { code } = await renderAstroMd(`<Component prop={{ a: 0, b: 1, c: 2 }} />`);
chai.expect(code).to.equal(`<Component prop={{ a: 0, b: 1, c: 2 }} />`);
});
it('should be able to serialize empty attribute', async () => {
const { code } = await renderMarkdown(`<Component empty />`, {});
const { code } = await renderAstroMd(`<Component empty />`);
chai.expect(code).to.equal(`<Component empty />`);
});
@ -35,25 +37,25 @@ describe('components', () => {
// Notable omission: shorthand attribute
it('should be able to serialize spread attribute', async () => {
const { code } = await renderMarkdown(`<Component {...spread} />`, {});
const { code } = await renderAstroMd(`<Component {...spread} />`);
chai.expect(code).to.equal(`<Component {...spread} />`);
});
it('should allow client:* directives', async () => {
const { code } = await renderMarkdown(`<Component client:load />`, {});
const { code } = await renderAstroMd(`<Component client:load />`);
chai.expect(code).to.equal(`<Component client:load />`);
});
it('should normalize children', async () => {
const { code } = await renderMarkdown(`<Component bool={true}>Hello world!</Component>`, {});
const { code } = await renderAstroMd(`<Component bool={true}>Hello world!</Component>`);
chai.expect(code).to.equal(`<Component bool={true}>Hello world!</Component>`);
});
it('should be able to nest components', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
`<Component bool={true}><Component>Hello world!</Component></Component>`,
{}
);
@ -64,7 +66,7 @@ describe('components', () => {
});
it('should allow markdown without many spaces', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
`<Component>
# Hello world!
</Component>`,

View file

@ -1,21 +1,23 @@
import { renderMarkdown } from '../dist/index.js';
import chai, { expect } from 'chai';
import chai from 'chai';
describe('expressions', () => {
const renderAstroMd = (text, opts) => renderMarkdown(text, { isAstroFlavoredMd: true, ...opts });
it('should be able to serialize bare expression', async () => {
const { code } = await renderMarkdown(`{a}`, {});
const { code } = await renderAstroMd(`{a}`, {});
chai.expect(code).to.equal(`{a}`);
});
it('should be able to serialize expression inside component', async () => {
const { code } = await renderMarkdown(`<Component>{a}</Component>`, {});
const { code } = await renderAstroMd(`<Component>{a}</Component>`, {});
chai.expect(code).to.equal(`<Component>{a}</Component>`);
});
it('should be able to serialize expression inside markdown', async () => {
const { code } = await renderMarkdown(`# {frontmatter.title}`, {});
const { code } = await renderAstroMd(`# {frontmatter.title}`, {});
chai
.expect(code)
@ -23,7 +25,7 @@ describe('expressions', () => {
});
it('should be able to serialize complex expression inside markdown', async () => {
const { code } = await renderMarkdown(`# Hello {frontmatter.name}`, {});
const { code } = await renderAstroMd(`# Hello {frontmatter.name}`, {});
chai
.expect(code)
@ -31,7 +33,7 @@ describe('expressions', () => {
});
it('should be able to serialize complex expression with markup inside markdown', async () => {
const { code } = await renderMarkdown(`# Hello <span>{frontmatter.name}</span>`, {});
const { code } = await renderAstroMd(`# Hello <span>{frontmatter.name}</span>`, {});
chai
.expect(code)
@ -41,7 +43,7 @@ describe('expressions', () => {
});
it('should be able to avoid evaluating JSX-like expressions in an inline code & generate a slug for id', async () => {
const { code } = await renderMarkdown(`# \`{frontmatter.title}\``, {});
const { code } = await renderAstroMd(`# \`{frontmatter.title}\``, {});
chai
.expect(code)
@ -49,7 +51,7 @@ describe('expressions', () => {
});
it('should be able to avoid evaluating JSX-like expressions in inline codes', async () => {
const { code } = await renderMarkdown(`# \`{ foo }\` is a shorthand for \`{ foo: foo }\``, {});
const { code } = await renderAstroMd(`# \`{ foo }\` is a shorthand for \`{ foo: foo }\``, {});
chai
.expect(code)
@ -59,7 +61,7 @@ describe('expressions', () => {
});
it('should be able to avoid evaluating JSX-like expressions & escape HTML tag characters in inline codes', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
`###### \`{}\` is equivalent to \`Record<never, never>\` <small>(at TypeScript v{frontmatter.version})</small>`,
{}
);
@ -72,7 +74,7 @@ describe('expressions', () => {
});
it('should be able to encode ampersand characters in code blocks', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
'The ampersand in `&nbsp;` must be encoded in code blocks.',
{}
);
@ -85,7 +87,7 @@ describe('expressions', () => {
});
it('should be able to encode ampersand characters in fenced code blocks', async () => {
const { code } = await renderMarkdown(`
const { code } = await renderAstroMd(`
\`\`\`md
The ampersand in \`&nbsp;\` must be encoded in code blocks.
\`\`\`
@ -95,7 +97,7 @@ describe('expressions', () => {
});
it('should be able to serialize function expression', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
`{frontmatter.list.map(item => <p id={item}>{item}</p>)}`,
{}
);
@ -104,13 +106,13 @@ describe('expressions', () => {
});
it('should unwrap HTML comments in inline code blocks', async () => {
const { code } = await renderMarkdown(`\`{/*<!-- HTML comment -->*/}\``);
const { code } = await renderAstroMd(`\`{/*<!-- HTML comment -->*/}\``);
chai.expect(code).to.equal('<p><code is:raw>&lt;!-- HTML comment --&gt;</code></p>');
});
it('should unwrap HTML comments in code fences', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
`
\`\`\`
<!-- HTML comment -->

View file

@ -1,9 +1,11 @@
import { renderMarkdown } from '../dist/index.js';
import chai from 'chai';
describe('strictness', () => {
describe('strictness in Astro-flavored markdown', () => {
const renderAstroMd = (text, opts) => renderMarkdown(text, { isAstroFlavoredMd: true, ...opts });
it('should allow self-closing HTML tags (void elements)', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
`Use self-closing void elements<br>like word<wbr>break and images: <img src="hi.jpg">`,
{}
);
@ -17,25 +19,25 @@ describe('strictness', () => {
});
it('should allow attribute names starting with ":" after element names', async () => {
const { code } = await renderMarkdown(`<div :class="open ? '' : 'hidden'">Test</div>`, {});
const { code } = await renderAstroMd(`<div :class="open ? '' : 'hidden'">Test</div>`, {});
chai.expect(code.trim()).to.equal(`<div :class="open ? '' : 'hidden'">Test</div>`);
});
it('should allow attribute names starting with ":" after local element names', async () => {
const { code } = await renderMarkdown(`<div.abc :class="open ? '' : 'hidden'">x</div.abc>`, {});
const { code } = await renderAstroMd(`<div.abc :class="open ? '' : 'hidden'">x</div.abc>`, {});
chai.expect(code.trim()).to.equal(`<div.abc :class="open ? '' : 'hidden'">x</div.abc>`);
});
it('should allow attribute names starting with ":" after attribute names', async () => {
const { code } = await renderMarkdown(`<input type="text" disabled :placeholder="hi">`, {});
const { code } = await renderAstroMd(`<input type="text" disabled :placeholder="hi">`, {});
chai.expect(code.trim()).to.equal(`<input type="text" disabled :placeholder="hi" />`);
});
it('should allow attribute names starting with ":" after local attribute names', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
`<input type="text" x-test:disabled :placeholder="hi">`,
{}
);
@ -44,19 +46,19 @@ describe('strictness', () => {
});
it('should allow attribute names starting with ":" after attribute values', async () => {
const { code } = await renderMarkdown(`<input type="text" :placeholder="placeholder">`, {});
const { code } = await renderAstroMd(`<input type="text" :placeholder="placeholder">`, {});
chai.expect(code.trim()).to.equal(`<input type="text" :placeholder="placeholder" />`);
});
it('should allow attribute names starting with "@" after element names', async () => {
const { code } = await renderMarkdown(`<button @click="handleClick">Test</button>`, {});
const { code } = await renderAstroMd(`<button @click="handleClick">Test</button>`, {});
chai.expect(code.trim()).to.equal(`<button @click="handleClick">Test</button>`);
});
it('should allow attribute names starting with "@" after local element names', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
`<button.local @click="handleClick">Test</button.local>`,
{}
);
@ -65,7 +67,7 @@ describe('strictness', () => {
});
it('should allow attribute names starting with "@" after attribute names', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
`<button disabled @click="handleClick">Test</button>`,
{}
);
@ -74,7 +76,7 @@ describe('strictness', () => {
});
it('should allow attribute names starting with "@" after local attribute names', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
`<button x-test:disabled @click="handleClick">Test</button>`,
{}
);
@ -83,7 +85,7 @@ describe('strictness', () => {
});
it('should allow attribute names starting with "@" after attribute values', async () => {
const { code } = await renderMarkdown(
const { code } = await renderAstroMd(
`<button type="submit" @click="handleClick">Test</button>`,
{}
);
@ -92,7 +94,7 @@ describe('strictness', () => {
});
it('should allow attribute names containing dots', async () => {
const { code } = await renderMarkdown(`<input x-on:input.debounce.500ms="fetchResults">`, {});
const { code } = await renderAstroMd(`<input x-on:input.debounce.500ms="fetchResults">`, {});
chai.expect(code.trim()).to.equal(`<input x-on:input.debounce.500ms="fetchResults" />`);
});

View file

@ -1714,19 +1714,23 @@ importers:
packages/astro/test/fixtures/slots-preact:
specifiers:
'@astrojs/mdx': workspace:*
'@astrojs/preact': workspace:*
astro: workspace:*
dependencies:
'@astrojs/mdx': link:../../../../integrations/mdx
'@astrojs/preact': link:../../../../integrations/preact
astro: link:../../..
packages/astro/test/fixtures/slots-react:
specifiers:
'@astrojs/mdx': workspace:*
'@astrojs/react': workspace:*
astro: workspace:*
react: ^18.1.0
react-dom: ^18.1.0
dependencies:
'@astrojs/mdx': link:../../../../integrations/mdx
'@astrojs/react': link:../../../../integrations/react
astro: link:../../..
react: 18.2.0
@ -1734,25 +1738,31 @@ importers:
packages/astro/test/fixtures/slots-solid:
specifiers:
'@astrojs/mdx': workspace:*
'@astrojs/solid-js': workspace:*
astro: workspace:*
dependencies:
'@astrojs/mdx': link:../../../../integrations/mdx
'@astrojs/solid-js': link:../../../../integrations/solid
astro: link:../../..
packages/astro/test/fixtures/slots-svelte:
specifiers:
'@astrojs/mdx': workspace:*
'@astrojs/svelte': workspace:*
astro: workspace:*
dependencies:
'@astrojs/mdx': link:../../../../integrations/mdx
'@astrojs/svelte': link:../../../../integrations/svelte
astro: link:../../..
packages/astro/test/fixtures/slots-vue:
specifiers:
'@astrojs/mdx': workspace:*
'@astrojs/vue': workspace:*
astro: workspace:*
dependencies:
'@astrojs/mdx': link:../../../../integrations/mdx
'@astrojs/vue': link:../../../../integrations/vue
astro: link:../../..