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 // https://astro.build/config
export default defineConfig({ export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [preact({ compat: true })], integrations: [preact({ compat: true })],
}); });

View file

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

View file

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

View file

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

View file

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

View file

@ -527,27 +527,6 @@ export interface AstroUserConfig {
*/ */
drafts?: boolean; 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 * @docs
* @name markdown.shikiConfig * @name markdown.shikiConfig
@ -716,6 +695,16 @@ export interface AstroUserConfig {
buildOptions?: never; buildOptions?: never;
/** @deprecated `devOptions` has been renamed to `server` */ /** @deprecated `devOptions` has been renamed to `server` */
devOptions?: never; 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 // NOTE(fks): We choose to keep our hand-generated AstroUserConfig interface so that

View file

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

View file

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

View file

@ -50,6 +50,9 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
rehypePlugins: [], rehypePlugins: [],
}, },
vite: {}, vite: {},
legacy: {
astroFlavoredMarkdown: false,
}
}; };
async function resolvePostcssConfig(inlineOptions: any, root: URL): Promise<PostCSSConfigResult> { async function resolvePostcssConfig(inlineOptions: any, root: URL): Promise<PostCSSConfigResult> {
@ -172,9 +175,6 @@ export const AstroConfigSchema = z.object({
.default({}), .default({}),
markdown: z markdown: z
.object({ .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), drafts: z.boolean().default(false),
syntaxHighlight: z syntaxHighlight: z
.union([z.literal('shiki'), z.literal('prism'), z.literal(false)]) .union([z.literal('shiki'), z.literal('prism'), z.literal(false)])
@ -212,6 +212,12 @@ export const AstroConfigSchema = z.object({
vite: z vite: z
.custom<ViteUserConfig>((data) => data instanceof Object && !Array.isArray(data)) .custom<ViteUserConfig>((data) => data instanceof Object && !Array.isArray(data))
.default(ASTRO_CONFIG_DEFAULTS.vite), .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 */ /** Turn raw config values into normalized values */

View file

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

View file

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

View file

@ -19,6 +19,9 @@ let consoleFilterRefs = 0;
export async function renderJSX(result: SSRResult, vnode: any): Promise<any> { export async function renderJSX(result: SSRResult, vnode: any): Promise<any> {
switch (true) { switch (true) {
case vnode instanceof HTMLString: case vnode instanceof HTMLString:
if (vnode.toString().trim() === '') {
return '';
}
return vnode; return vnode;
case typeof vnode === 'string': case typeof vnode === 'string':
return markHTMLString(escapeHTML(vnode)); return markHTMLString(escapeHTML(vnode));
@ -55,6 +58,9 @@ export async function renderJSX(result: SSRResult, vnode: any): Promise<any> {
} }
if (vnode.type) { 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']) { if (typeof vnode.type === 'function' && vnode.props['server:root']) {
const output = await vnode.type(vnode.props ?? {}); const output = await vnode.type(vnode.props ?? {});
return await renderJSX(result, output); 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 { children = null, ...props } = vnode.props ?? {};
const slots: Record<string, any> = { const _slots: Record<string, any> = {
default: [], default: [],
}; };
function extractSlots(child: any): any { 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)); return child.map((c) => extractSlots(c));
} }
if (!isVNode(child)) { if (!isVNode(child)) {
return slots.default.push(child); _slots.default.push(child);
return
} }
if ('slot' in child.props) { 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; delete child.props.slot;
return; return;
} }
slots.default.push(child); _slots.default.push(child);
} }
extractSlots(children); extractSlots(children);
for (const [key, value] of Object.entries(slots)) { for (const [key, value] of Object.entries(props)) {
slots[key] = () => renderJSX(result, value); 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>; let output: string | AsyncIterable<string>;
if (vnode.type === ClientOnlyPlaceholder && vnode.props['client:only']) { 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 filename = normalizeFilename(id);
const source = await fs.promises.readFile(filename, 'utf8'); const source = await fs.promises.readFile(filename, 'utf8');
const renderOpts = config.markdown; const renderOpts = config.markdown;
const isMDX = renderOpts.mode === 'mdx'; const isAstroFlavoredMd = config.legacy.astroFlavoredMarkdown;
const fileUrl = new URL(`file://${filename}`); const fileUrl = new URL(`file://${filename}`);
const isPage = fileUrl.pathname.startsWith(resolvePages(config).pathname); 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 // Turn HTML comments into JS comments while preventing nested `*/` sequences
// from ending the JS comment by injecting a zero-width space // from ending the JS comment by injecting a zero-width space
// Inside code blocks, this is removed during renderMarkdown by the remark-escape plugin. // Inside code blocks, this is removed during renderMarkdown by the remark-escape plugin.
if (isMDX) { if (isAstroFlavoredMd) {
markdownContent = markdownContent.replace( markdownContent = markdownContent.replace(
/<\s*!--([^-->]*)(.*?)-->/gs, /<\s*!--([^-->]*)(.*?)-->/gs,
(whole) => `{/*${whole.replace(/\*\//g, '*\u200b/')}*/}` (whole) => `{/*${whole.replace(/\*\//g, '*\u200b/')}*/}`
@ -159,6 +159,7 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
let renderResult = await renderMarkdown(markdownContent, { let renderResult = await renderMarkdown(markdownContent, {
...renderOpts, ...renderOpts,
fileURL: fileUrl, fileURL: fileUrl,
isAstroFlavoredMd,
} as any); } as any);
let { code: astroResult, metadata } = renderResult; let { code: astroResult, metadata } = renderResult;
const { layout = '', components = '', setup = '', ...content } = frontmatter; const { layout = '', components = '', setup = '', ...content } = frontmatter;
@ -168,9 +169,9 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
const prelude = `--- const prelude = `---
import Slugger from 'github-slugger'; import Slugger from 'github-slugger';
${layout ? `import Layout from '${layout}';` : ''} ${layout ? `import Layout from '${layout}';` : ''}
${isMDX && components ? `import * from '${components}';` : ''} ${isAstroFlavoredMd && components ? `import * from '${components}';` : ''}
${hasInjectedScript ? `import '${PAGE_SSR_SCRIPT_ID}';` : ''} ${hasInjectedScript ? `import '${PAGE_SSR_SCRIPT_ID}';` : ''}
${isMDX ? setup : ''} ${isAstroFlavoredMd ? setup : ''}
const slugger = new Slugger(); const slugger = new Slugger();
function $$slug(value) { function $$slug(value) {
@ -178,7 +179,7 @@ function $$slug(value) {
} }
const $$content = ${JSON.stringify( const $$content = ${JSON.stringify(
isMDX isAstroFlavoredMd
? content ? content
: // Avoid stripping "setup" and "components" : // Avoid stripping "setup" and "components"
// in plain MD mode // in plain MD mode
@ -186,11 +187,11 @@ const $$content = ${JSON.stringify(
)} )}
---`; ---`;
const imports = `${layout ? `import Layout from '${layout}';` : ''} const imports = `${layout ? `import Layout from '${layout}';` : ''}
${isMDX ? setup : ''}`.trim(); ${isAstroFlavoredMd ? setup : ''}`.trim();
// Wrap with set:html fragment to skip // Wrap with set:html fragment to skip
// JSX expressions and components in "plain" md mode // JSX expressions and components in "plain" md mode
if (!isMDX) { if (!isAstroFlavoredMd) {
astroResult = `<Fragment set:html={${JSON.stringify(astroResult)}} />`; 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/', root: './fixtures/astro-markdown-md-mode/',
markdown: { markdown: {
syntaxHighlight: 'prism', syntaxHighlight: 'prism',
mode: 'md',
}, },
}); });
await fixture.build(); await fixture.build();

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@astrojs/mdx": "workspace:*",
"@astrojs/preact": "workspace:*", "@astrojs/preact": "workspace:*",
"astro": "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 { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import react from '@astrojs/react'; import react from '@astrojs/react';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [react()], legacy: {
}); astroFlavoredMarkdown: true,
},
integrations: [react(), mdx()],
});

View file

@ -3,6 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@astrojs/mdx": "workspace:*",
"@astrojs/react": "workspace:*", "@astrojs/react": "workspace:*",
"astro": "workspace:*", "astro": "workspace:*",
"react": "^18.1.0", "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 { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import solid from '@astrojs/solid-js'; import solid from '@astrojs/solid-js';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [solid()], legacy: {
}); astroFlavoredMarkdown: true,
},
integrations: [solid(), mdx()],
});

View file

@ -3,6 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@astrojs/mdx": "workspace:*",
"@astrojs/solid-js": "workspace:*", "@astrojs/solid-js": "workspace:*",
"astro": "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 { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import svelte from '@astrojs/svelte'; import svelte from '@astrojs/svelte';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [svelte()], legacy: {
}); astroFlavoredMarkdown: true,
},
integrations: [svelte(), mdx()],
});

View file

@ -3,6 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@astrojs/mdx": "workspace:*",
"@astrojs/svelte": "workspace:*", "@astrojs/svelte": "workspace:*",
"astro": "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 { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import vue from '@astrojs/vue'; import vue from '@astrojs/vue';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [vue()], legacy: {
}); astroFlavoredMarkdown: true,
},
integrations: [vue(), mdx()],
});

View file

@ -3,6 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@astrojs/mdx": "workspace:*",
"@astrojs/vue": "workspace:*", "@astrojs/vue": "workspace:*",
"astro": "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 // https://astro.build/config
export default defineConfig({ export default defineConfig({
legacy: {
astroFlavoredMarkdown: true,
},
integrations: [tailwind()], integrations: [tailwind()],
vite: { vite: {
build: { build: {
assetsInlineLimit: 0, assetsInlineLimit: 0,
}, },
}, },
}); });

View file

@ -53,4 +53,24 @@ describe('Slots: Preact', () => {
expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case'); 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'); 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'); 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'); 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'); 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) { if (content) {
htmlContent = await (Astro as any).__renderMarkdown(content, { htmlContent = await (Astro as any).__renderMarkdown(content, {
mode: 'md',
$: { $: {
scopedClassName: className, scopedClassName: className,
}, },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,91 +2,107 @@ import { renderMarkdown } from '../dist/index.js';
import chai from 'chai'; import chai from 'chai';
describe('autolinking', () => { describe('autolinking', () => {
it('autolinks URLs starting with a protocol in plain text', async () => { describe('plain md', () => {
const { code } = await renderMarkdown(`See https://example.com for more.`, {}); 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, '')) chai
.to.equal(`<p>See <a href="https://example.com">https://example.com</a> for more.</p>`); .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.`, {}); it('autolinks URLs starting with "www." in plain text', async () => {
const { code } = await renderMarkdown(`See www.example.com for more.`, {});
chai
.expect(code.trim()) chai
.to.equal(`<p>See <a href="http://www.example.com">www.example.com</a> for more.</p>`); .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( it('does not autolink URLs in code blocks', async () => {
'See `https://example.com` or `www.example.com` for more.', 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>`
); );
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 () => { describe('astro-flavored md', () => {
const { code } = await renderMarkdown( const renderAstroMd = text => renderMarkdown(text, { isAstroFlavoredMd: true });
'Example:\n```\nGo to https://example.com or www.example.com now.\n```',
{}
);
chai it('does not autolink URLs in code blocks', async () => {
.expect(code) const { code } = await renderAstroMd(
.to.contain(`<pre is:raw`) 'See `https://example.com` or `www.example.com` for more.',
.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>`
); );
});
chai
it('does not autolink URLs starting with "www." when nested inside links', async () => { .expect(code.trim())
const { code } = await renderMarkdown( .to.equal(
`See [www.example.com](https://www.example.com) or ` + `<p>See <code is:raw>https://example.com</code> or ` +
`<a test href="https://www.example.com">www.example.com</a>`, `<code is:raw>www.example.com</code> for more.</p>`
{} );
); });
chai it('does not autolink URLs in fenced code blocks', async () => {
.expect(code.replace(/\n/g, '')) const { code } = await renderAstroMd(
.to.equal( 'Example:\n```\nGo to https://example.com or www.example.com now.\n```'
`<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
it('does not autolink URLs when nested several layers deep inside links', async () => { .expect(code)
const { code } = await renderMarkdown( .to.contain(`<pre is:raw`)
`<a href="https://www.example.com">**Visit _our www.example.com or ` + .to.contain(`Go to https://example.com or www.example.com now.`);
`http://localhost pages_ for more!**</a>`, });
{}
); it('does not autolink URLs starting with a protocol when nested inside links', async () => {
const { code } = await renderAstroMd(
chai `See [http://example.com](http://example.com) or ` +
.expect(code.replace(/\n/g, '')) `<a test href="https://example.com">https://example.com</a>`
.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.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'; import chai from 'chai';
describe('components', () => { describe('components', () => {
const renderAstroMd = (text) => renderMarkdown(text, { isAstroFlavoredMd: true });
it('should be able to serialize string', async () => { 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!" />`); chai.expect(code).to.equal(`<Component str="cool!" />`);
}); });
it('should be able to serialize boolean attribute', async () => { 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} />`); chai.expect(code).to.equal(`<Component bool={true} />`);
}); });
it('should be able to serialize array', async () => { 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"]} />`); chai.expect(code).to.equal(`<Component prop={["a", "b", "c"]} />`);
}); });
it('should be able to serialize object', async () => { 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 }} />`); chai.expect(code).to.equal(`<Component prop={{ a: 0, b: 1, c: 2 }} />`);
}); });
it('should be able to serialize empty attribute', async () => { 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 />`); chai.expect(code).to.equal(`<Component empty />`);
}); });
@ -35,25 +37,25 @@ describe('components', () => {
// Notable omission: shorthand attribute // Notable omission: shorthand attribute
it('should be able to serialize spread attribute', async () => { 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} />`); chai.expect(code).to.equal(`<Component {...spread} />`);
}); });
it('should allow client:* directives', async () => { 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 />`); chai.expect(code).to.equal(`<Component client:load />`);
}); });
it('should normalize children', async () => { 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>`); chai.expect(code).to.equal(`<Component bool={true}>Hello world!</Component>`);
}); });
it('should be able to nest components', async () => { it('should be able to nest components', async () => {
const { code } = await renderMarkdown( const { code } = await renderAstroMd(
`<Component bool={true}><Component>Hello world!</Component></Component>`, `<Component bool={true}><Component>Hello world!</Component></Component>`,
{} {}
); );
@ -64,7 +66,7 @@ describe('components', () => {
}); });
it('should allow markdown without many spaces', async () => { it('should allow markdown without many spaces', async () => {
const { code } = await renderMarkdown( const { code } = await renderAstroMd(
`<Component> `<Component>
# Hello world! # Hello world!
</Component>`, </Component>`,

View file

@ -1,21 +1,23 @@
import { renderMarkdown } from '../dist/index.js'; import { renderMarkdown } from '../dist/index.js';
import chai, { expect } from 'chai'; import chai from 'chai';
describe('expressions', () => { describe('expressions', () => {
const renderAstroMd = (text, opts) => renderMarkdown(text, { isAstroFlavoredMd: true, ...opts });
it('should be able to serialize bare expression', async () => { 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}`); chai.expect(code).to.equal(`{a}`);
}); });
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 renderAstroMd(`<Component>{a}</Component>`, {});
chai.expect(code).to.equal(`<Component>{a}</Component>`); chai.expect(code).to.equal(`<Component>{a}</Component>`);
}); });
it('should be able to serialize expression inside markdown', async () => { it('should be able to serialize expression inside markdown', async () => {
const { code } = await renderMarkdown(`# {frontmatter.title}`, {}); const { code } = await renderAstroMd(`# {frontmatter.title}`, {});
chai chai
.expect(code) .expect(code)
@ -23,7 +25,7 @@ describe('expressions', () => {
}); });
it('should be able to serialize complex expression inside markdown', async () => { 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 chai
.expect(code) .expect(code)
@ -31,7 +33,7 @@ describe('expressions', () => {
}); });
it('should be able to serialize complex expression with markup inside markdown', async () => { 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 chai
.expect(code) .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 () => { 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 chai
.expect(code) .expect(code)
@ -49,7 +51,7 @@ describe('expressions', () => {
}); });
it('should be able to avoid evaluating JSX-like expressions in inline codes', async () => { 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 chai
.expect(code) .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 () => { 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>`, `###### \`{}\` 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 () => { 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.', '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 () => { it('should be able to encode ampersand characters in fenced code blocks', async () => {
const { code } = await renderMarkdown(` const { code } = await renderAstroMd(`
\`\`\`md \`\`\`md
The ampersand in \`&nbsp;\` must be encoded in code blocks. 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 () => { 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>)}`, `{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 () => { 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>'); chai.expect(code).to.equal('<p><code is:raw>&lt;!-- HTML comment --&gt;</code></p>');
}); });
it('should unwrap HTML comments in code fences', async () => { it('should unwrap HTML comments in code fences', async () => {
const { code } = await renderMarkdown( const { code } = await renderAstroMd(
` `
\`\`\` \`\`\`
<!-- HTML comment --> <!-- HTML comment -->

View file

@ -1,9 +1,11 @@
import { renderMarkdown } from '../dist/index.js'; import { renderMarkdown } from '../dist/index.js';
import chai from 'chai'; 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 () => { 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">`, `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 () => { 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>`); chai.expect(code.trim()).to.equal(`<div :class="open ? '' : 'hidden'">Test</div>`);
}); });
it('should allow attribute names starting with ":" after local element names', async () => { 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>`); 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 () => { 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" />`); chai.expect(code.trim()).to.equal(`<input type="text" disabled :placeholder="hi" />`);
}); });
it('should allow attribute names starting with ":" after local attribute names', async () => { 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">`, `<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 () => { 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" />`); chai.expect(code.trim()).to.equal(`<input type="text" :placeholder="placeholder" />`);
}); });
it('should allow attribute names starting with "@" after element names', async () => { 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>`); chai.expect(code.trim()).to.equal(`<button @click="handleClick">Test</button>`);
}); });
it('should allow attribute names starting with "@" after local element names', async () => { 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>`, `<button.local @click="handleClick">Test</button.local>`,
{} {}
); );
@ -65,7 +67,7 @@ describe('strictness', () => {
}); });
it('should allow attribute names starting with "@" after attribute names', async () => { 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>`, `<button disabled @click="handleClick">Test</button>`,
{} {}
); );
@ -74,7 +76,7 @@ describe('strictness', () => {
}); });
it('should allow attribute names starting with "@" after local attribute names', async () => { 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>`, `<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 () => { 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>`, `<button type="submit" @click="handleClick">Test</button>`,
{} {}
); );
@ -92,7 +94,7 @@ describe('strictness', () => {
}); });
it('should allow attribute names containing dots', async () => { 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" />`); 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: packages/astro/test/fixtures/slots-preact:
specifiers: specifiers:
'@astrojs/mdx': workspace:*
'@astrojs/preact': workspace:* '@astrojs/preact': workspace:*
astro: workspace:* astro: workspace:*
dependencies: dependencies:
'@astrojs/mdx': link:../../../../integrations/mdx
'@astrojs/preact': link:../../../../integrations/preact '@astrojs/preact': link:../../../../integrations/preact
astro: link:../../.. astro: link:../../..
packages/astro/test/fixtures/slots-react: packages/astro/test/fixtures/slots-react:
specifiers: specifiers:
'@astrojs/mdx': workspace:*
'@astrojs/react': workspace:* '@astrojs/react': workspace:*
astro: workspace:* astro: workspace:*
react: ^18.1.0 react: ^18.1.0
react-dom: ^18.1.0 react-dom: ^18.1.0
dependencies: dependencies:
'@astrojs/mdx': link:../../../../integrations/mdx
'@astrojs/react': link:../../../../integrations/react '@astrojs/react': link:../../../../integrations/react
astro: link:../../.. astro: link:../../..
react: 18.2.0 react: 18.2.0
@ -1734,25 +1738,31 @@ importers:
packages/astro/test/fixtures/slots-solid: packages/astro/test/fixtures/slots-solid:
specifiers: specifiers:
'@astrojs/mdx': workspace:*
'@astrojs/solid-js': workspace:* '@astrojs/solid-js': workspace:*
astro: workspace:* astro: workspace:*
dependencies: dependencies:
'@astrojs/mdx': link:../../../../integrations/mdx
'@astrojs/solid-js': link:../../../../integrations/solid '@astrojs/solid-js': link:../../../../integrations/solid
astro: link:../../.. astro: link:../../..
packages/astro/test/fixtures/slots-svelte: packages/astro/test/fixtures/slots-svelte:
specifiers: specifiers:
'@astrojs/mdx': workspace:*
'@astrojs/svelte': workspace:* '@astrojs/svelte': workspace:*
astro: workspace:* astro: workspace:*
dependencies: dependencies:
'@astrojs/mdx': link:../../../../integrations/mdx
'@astrojs/svelte': link:../../../../integrations/svelte '@astrojs/svelte': link:../../../../integrations/svelte
astro: link:../../.. astro: link:../../..
packages/astro/test/fixtures/slots-vue: packages/astro/test/fixtures/slots-vue:
specifiers: specifiers:
'@astrojs/mdx': workspace:*
'@astrojs/vue': workspace:* '@astrojs/vue': workspace:*
astro: workspace:* astro: workspace:*
dependencies: dependencies:
'@astrojs/mdx': link:../../../../integrations/mdx
'@astrojs/vue': link:../../../../integrations/vue '@astrojs/vue': link:../../../../integrations/vue
astro: link:../../.. astro: link:../../..