From cfae9760b252052b6189e96398b819a4337634a8 Mon Sep 17 00:00:00 2001
From: Nate Moore
Date: Tue, 24 May 2022 17:02:11 -0500
Subject: [PATCH] Improve Markdown + Components usage (#3410)
* feat: use internal MDX tooling for markdown + components
* fix: improve MD + component tests
* chore: add changeset
* fix: make tsc happy
* fix(#3319): add regression test for component children
* fix(markdown): support HTML comments in markdown
* fix(#2474): ensure namespaced components are properly handled in markdown pages
* fix(#3220): ensure html in markdown pages does not have extra surrounding space
* fix(#3264): ensure that remark files pass in file information
* fix(#3254): enable experimentalStaticExtraction for `.md` pages
* fix: revert parsing change
* fix: remove `markdown.mode` option
---
.changeset/wicked-adults-pull.md | 6 ++
packages/astro/src/@types/astro.ts | 10 ---
.../astro/src/vite-plugin-markdown/index.ts | 13 ++-
packages/astro/test/astro-markdown.test.js | 49 ++++++++++-
.../src/components/TextBlock.jsx | 20 +++++
.../astro-markdown/src/components/index.js | 5 ++
.../src/content/code-element.md | 3 +
.../astro-markdown/src/pages/children.md | 12 +++
.../src/pages/code-element.astro | 7 ++
.../astro-markdown/src/pages/comment.md | 2 +
.../src/pages/jsx-expressions.md | 4 +-
.../astro-markdown/src/pages/namespace.md | 7 ++
.../astro-markdown/src/pages/script.md | 7 ++
.../fixtures/astro-markdown/src/pages/slug.md | 7 ++
.../astro-markdown/src/scripts/test.js | 1 +
packages/markdown/remark/package.json | 13 ++-
packages/markdown/remark/src/index.ts | 30 ++++---
.../markdown/remark/src/mdast-util-mdxish.ts | 18 +++++
.../remark/src/rehype-collect-headers.ts | 31 ++++++-
.../markdown/remark/src/rehype-expressions.ts | 8 +-
packages/markdown/remark/src/rehype-jsx.ts | 38 +++++++--
.../markdown/remark/src/remark-expressions.ts | 25 ------
packages/markdown/remark/src/remark-jsx.ts | 31 -------
.../remark/src/remark-mark-and-unravel.ts | 81 +++++++++++++++++++
packages/markdown/remark/src/remark-mdxish.ts | 15 ++++
packages/markdown/remark/src/remark-shiki.ts | 2 +-
packages/markdown/remark/src/types.ts | 20 ++---
.../markdown/remark/test/components.test.js | 62 ++++++++++++++
.../markdown/remark/test/expressions.test.js | 40 +++++++++
packages/markdown/remark/test/plugins.test.js | 26 ++++++
pnpm-lock.yaml | 57 ++++++++++++-
31 files changed, 542 insertions(+), 108 deletions(-)
create mode 100644 .changeset/wicked-adults-pull.md
create mode 100644 packages/astro/test/fixtures/astro-markdown/src/components/TextBlock.jsx
create mode 100644 packages/astro/test/fixtures/astro-markdown/src/components/index.js
create mode 100644 packages/astro/test/fixtures/astro-markdown/src/content/code-element.md
create mode 100644 packages/astro/test/fixtures/astro-markdown/src/pages/children.md
create mode 100644 packages/astro/test/fixtures/astro-markdown/src/pages/code-element.astro
create mode 100644 packages/astro/test/fixtures/astro-markdown/src/pages/comment.md
create mode 100644 packages/astro/test/fixtures/astro-markdown/src/pages/namespace.md
create mode 100644 packages/astro/test/fixtures/astro-markdown/src/pages/script.md
create mode 100644 packages/astro/test/fixtures/astro-markdown/src/pages/slug.md
create mode 100644 packages/astro/test/fixtures/astro-markdown/src/scripts/test.js
create mode 100644 packages/markdown/remark/src/mdast-util-mdxish.ts
delete mode 100644 packages/markdown/remark/src/remark-expressions.ts
delete mode 100644 packages/markdown/remark/src/remark-jsx.ts
create mode 100644 packages/markdown/remark/src/remark-mark-and-unravel.ts
create mode 100644 packages/markdown/remark/src/remark-mdxish.ts
create mode 100644 packages/markdown/remark/test/components.test.js
create mode 100644 packages/markdown/remark/test/expressions.test.js
create mode 100644 packages/markdown/remark/test/plugins.test.js
diff --git a/.changeset/wicked-adults-pull.md b/.changeset/wicked-adults-pull.md
new file mode 100644
index 000000000..f36d0a054
--- /dev/null
+++ b/.changeset/wicked-adults-pull.md
@@ -0,0 +1,6 @@
+---
+'astro': patch
+'@astrojs/markdown-remark': minor
+---
+
+Significantally more stable behavior for "Markdown + Components" usage, which now handles component serialization much more similarly to MDX. Also supports switching between Components and Markdown without extra newlines, removes wrapping `` tags from standalone components, and improves JSX expression handling.
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 642fe9ffb..ada2427ad 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -513,16 +513,6 @@ export interface AstroUserConfig {
*/
drafts?: boolean;
- /**
- * @docs
- * @name markdown.mode
- * @type {'md' | 'mdx'}
- * @default `mdx`
- * @description
- * Control wheater to allow components inside markdown files ('mdx') or not ('md').
- */
- mode?: 'md' | 'mdx';
-
/**
* @docs
* @name markdown.shikiConfig
diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts
index d75351ca3..d7c02192c 100644
--- a/packages/astro/src/vite-plugin-markdown/index.ts
+++ b/packages/astro/src/vite-plugin-markdown/index.ts
@@ -84,7 +84,7 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
const source = await fs.promises.readFile(fileId, 'utf8');
const { data: frontmatter } = matter(source);
return {
- code: `
+ code: `
// Static
export const frontmatter = ${JSON.stringify(frontmatter)};
export const file = ${JSON.stringify(fileId)};
@@ -122,12 +122,17 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
const hasInjectedScript = isPage && config._ctx.scripts.some((s) => s.stage === 'page-ssr');
// Extract special frontmatter keys
- const { data: frontmatter, content: markdownContent } = matter(source);
- let renderResult = await renderMarkdown(markdownContent, renderOpts);
+ let { data: frontmatter, content: markdownContent } = matter(source);
+
+ // Turn HTML comments into JS comments
+ markdownContent = markdownContent.replace(/<\s*!--([^-->]*)(.*?)-->/gs, (whole) => `{/*${whole}*/}`)
+
+ let renderResult = await renderMarkdown(markdownContent, { ...renderOpts, fileURL: fileUrl } as any);
let { code: astroResult, metadata } = renderResult;
const { layout = '', components = '', setup = '', ...content } = frontmatter;
content.astro = metadata;
const prelude = `---
+import { slug as $$slug } from '@astrojs/markdown-remark';
${layout ? `import Layout from '${layout}';` : ''}
${components ? `import * from '${components}';` : ''}
${hasInjectedScript ? `import '${PAGE_SSR_SCRIPT_ID}';` : ''}
@@ -151,6 +156,8 @@ ${setup}`.trim();
site: config.site ? new URL(config.base, config.site).toString() : undefined,
sourcefile: id,
sourcemap: 'inline',
+ // TODO: baseline flag
+ experimentalStaticExtraction: true,
internalURL: `/@fs${prependForwardSlash(
viteID(new URL('../runtime/server/index.js', import.meta.url))
)}`,
diff --git a/packages/astro/test/astro-markdown.test.js b/packages/astro/test/astro-markdown.test.js
index cfbf33b0a..0f9d28c86 100644
--- a/packages/astro/test/astro-markdown.test.js
+++ b/packages/astro/test/astro-markdown.test.js
@@ -28,12 +28,57 @@ describe('Astro Markdown', () => {
const $ = cheerio.load(html);
expect($('h2').html()).to.equal('Blog Post with JSX expressions');
- expect($('p').first().html()).to.equal('JSX at the start of the line!');
+
+ expect(html).to.contain('JSX at the start of the line!');
for (let listItem of ['test-1', 'test-2', 'test-3']) {
- expect($(`#${listItem}`).html()).to.equal(`\n${listItem}\n`);
+ expect($(`#${listItem}`).html()).to.equal(`${listItem}`);
}
});
+ it('Can handle slugs with JSX expressions in markdown pages', async () => {
+ const html = await fixture.readFile('/slug/index.html');
+ const $ = cheerio.load(html);
+
+ expect($('h1').attr("id")).to.equal('my-blog-post');
+ });
+
+ it('Can handle code elements without extra spacing', async () => {
+ const html = await fixture.readFile('/code-element/index.html');
+ const $ = cheerio.load(html);
+
+ $('code').each((_, el) => {
+ expect($(el).html()).to.equal($(el).html().trim())
+ });
+ });
+
+ it('Can handle namespaced components in markdown', async () => {
+ const html = await fixture.readFile('/namespace/index.html');
+ const $ = cheerio.load(html);
+
+ expect($('h1').text()).to.equal('Hello Namespace!');
+ expect($('button').length).to.equal(1);
+ });
+
+ it('Correctly handles component children in markdown pages (#3319)', async () => {
+ const html = await fixture.readFile('/children/index.html');
+
+ expect(html).not.to.contain('
');
+ });
+
+ it('Can handle HTML comments in markdown pages', async () => {
+ const html = await fixture.readFile('/comment/index.html');
+ const $ = cheerio.load(html);
+
+ expect($('h1').text()).to.equal('It works!');
+ });
+
+ // https://github.com/withastro/astro/issues/3254
+ it('Can handle scripts in markdown pages', async () => {
+ const html = await fixture.readFile('/script/index.html');
+ console.log(html);
+ expect(html).not.to.match(new RegExp("\/src\/scripts\/test\.js"));
+ });
+
it('Can load more complex jsxy stuff', async () => {
const html = await fixture.readFile('/complex/index.html');
const $ = cheerio.load(html);
diff --git a/packages/astro/test/fixtures/astro-markdown/src/components/TextBlock.jsx b/packages/astro/test/fixtures/astro-markdown/src/components/TextBlock.jsx
new file mode 100644
index 000000000..d9ea2534f
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-markdown/src/components/TextBlock.jsx
@@ -0,0 +1,20 @@
+import { h } from 'preact';
+
+const TextBlock = ({
+ title,
+ children,
+ noPadding = false,
+}) => {
+ return (
+
+ );
+};
+
+export default TextBlock;
diff --git a/packages/astro/test/fixtures/astro-markdown/src/components/index.js b/packages/astro/test/fixtures/astro-markdown/src/components/index.js
new file mode 100644
index 000000000..e7cc94c58
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-markdown/src/components/index.js
@@ -0,0 +1,5 @@
+import Counter from './Counter';
+
+export default {
+ Counter
+}
diff --git a/packages/astro/test/fixtures/astro-markdown/src/content/code-element.md b/packages/astro/test/fixtures/astro-markdown/src/content/code-element.md
new file mode 100644
index 000000000..b091decc0
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-markdown/src/content/code-element.md
@@ -0,0 +1,3 @@
+This should have `nospace` around it.
+
+This should have nospace
around it.
diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/children.md b/packages/astro/test/fixtures/astro-markdown/src/pages/children.md
new file mode 100644
index 000000000..a22ee5f96
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-markdown/src/pages/children.md
@@ -0,0 +1,12 @@
+---
+setup: import TextBlock from '../components/TextBlock'
+---
+{/* https://github.com/withastro/astro/issues/3319 */}
+
+
+
+
diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/code-element.astro b/packages/astro/test/fixtures/astro-markdown/src/pages/code-element.astro
new file mode 100644
index 000000000..43ca0bfc5
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-markdown/src/pages/code-element.astro
@@ -0,0 +1,7 @@
+---
+const content = await Astro.glob('../content/*.md');
+---
+
+
+ {content.map(({ Content }) => )}
+
diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/comment.md b/packages/astro/test/fixtures/astro-markdown/src/pages/comment.md
new file mode 100644
index 000000000..39a916351
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-markdown/src/pages/comment.md
@@ -0,0 +1,2 @@
+
+# It works!
diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/jsx-expressions.md b/packages/astro/test/fixtures/astro-markdown/src/pages/jsx-expressions.md
index 2f038fdad..b87efbb2d 100644
--- a/packages/astro/test/fixtures/astro-markdown/src/pages/jsx-expressions.md
+++ b/packages/astro/test/fixtures/astro-markdown/src/pages/jsx-expressions.md
@@ -8,4 +8,6 @@ list: ['test-1', 'test-2', 'test-3']
{frontmatter.paragraph}
-{frontmatter.list.map(item => {item}
)}
+
+ {frontmatter.list.map(item => - {item}
)}
+
diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/namespace.md b/packages/astro/test/fixtures/astro-markdown/src/pages/namespace.md
new file mode 100644
index 000000000..abbe26a3b
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-markdown/src/pages/namespace.md
@@ -0,0 +1,7 @@
+---
+setup: import ns from '../components/index.js';
+---
+
+# Hello Namespace!
+
+Click me!
diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/script.md b/packages/astro/test/fixtures/astro-markdown/src/pages/script.md
new file mode 100644
index 000000000..f2b8bca88
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-markdown/src/pages/script.md
@@ -0,0 +1,7 @@
+# Test
+
+## Let's try a script...
+
+This should work!
+
+
diff --git a/packages/astro/test/fixtures/astro-markdown/src/pages/slug.md b/packages/astro/test/fixtures/astro-markdown/src/pages/slug.md
new file mode 100644
index 000000000..77599b347
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-markdown/src/pages/slug.md
@@ -0,0 +1,7 @@
+---
+title: My Blog Post
+---
+
+# {frontmatter.title}
+
+Hello world
diff --git a/packages/astro/test/fixtures/astro-markdown/src/scripts/test.js b/packages/astro/test/fixtures/astro-markdown/src/scripts/test.js
new file mode 100644
index 000000000..b179ee953
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-markdown/src/scripts/test.js
@@ -0,0 +1 @@
+console.log("Hello world");
diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json
index 2868c0ffa..58be5e9d1 100644
--- a/packages/markdown/remark/package.json
+++ b/packages/markdown/remark/package.json
@@ -20,7 +20,8 @@
"build": "astro-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"postbuild": "astro-scripts copy \"src/**/*.js\"",
- "dev": "astro-scripts dev \"src/**/*.ts\""
+ "dev": "astro-scripts dev \"src/**/*.ts\"",
+ "test": "mocha --exit --timeout 20000"
},
"dependencies": {
"@astrojs/prism": "^0.4.1",
@@ -30,6 +31,7 @@
"mdast-util-mdx-jsx": "^1.2.0",
"mdast-util-to-string": "^3.1.0",
"micromark-extension-mdx-jsx": "^1.0.3",
+ "micromark-extension-mdxjs": "^1.0.0",
"prismjs": "^1.28.0",
"rehype-raw": "^6.1.1",
"rehype-stringify": "^9.0.3",
@@ -40,14 +42,19 @@
"shiki": "^0.10.1",
"unified": "^10.1.2",
"unist-util-map": "^3.1.1",
- "unist-util-visit": "^4.1.0"
+ "unist-util-visit": "^4.1.0",
+ "vfile": "^5.3.2"
},
"devDependencies": {
+ "@types/chai": "^4.3.1",
"@types/github-slugger": "^1.3.0",
"@types/hast": "^2.3.4",
"@types/mdast": "^3.0.10",
+ "@types/mocha": "^9.1.1",
"@types/prismjs": "^1.26.0",
"@types/unist": "^2.0.6",
- "astro-scripts": "workspace:*"
+ "astro-scripts": "workspace:*",
+ "chai": "^4.3.6",
+ "mocha": "^9.2.2"
}
}
diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts
index bf660a508..d942bf7bf 100644
--- a/packages/markdown/remark/src/index.ts
+++ b/packages/markdown/remark/src/index.ts
@@ -2,10 +2,10 @@ import type { MarkdownRenderingOptions, MarkdownRenderingResult } from './types'
import createCollectHeaders from './rehype-collect-headers.js';
import scopedStyles from './remark-scoped-styles.js';
-import { remarkExpressions, loadRemarkExpressions } from './remark-expressions.js';
import rehypeExpressions from './rehype-expressions.js';
import rehypeIslands from './rehype-islands.js';
-import { remarkJsx, loadRemarkJsx } from './remark-jsx.js';
+import remarkMdxish from './remark-mdxish.js';
+import remarkMarkAndUnravel from './remark-mark-and-unravel.js';
import rehypeJsx from './rehype-jsx.js';
import rehypeEscape from './rehype-escape.js';
import remarkPrism from './remark-prism.js';
@@ -18,27 +18,33 @@ import markdown from 'remark-parse';
import markdownToHtml from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import rehypeRaw from 'rehype-raw';
+import Slugger from 'github-slugger';
+import { VFile } from 'vfile';
export * from './types.js';
export const DEFAULT_REMARK_PLUGINS = ['remark-gfm', 'remark-smartypants'];
export const DEFAULT_REHYPE_PLUGINS = [];
+const slugger = new Slugger();
+export function slug(value: string): string {
+ return slugger.slug(value);
+}
+
/** Shared utility for rendering markdown */
export async function renderMarkdown(
content: string,
- opts: MarkdownRenderingOptions
+ opts: MarkdownRenderingOptions = {}
): Promise {
- let { mode, syntaxHighlight, shikiConfig, remarkPlugins, rehypePlugins } = opts;
+ let { fileURL, mode = 'mdx', syntaxHighlight = 'shiki', shikiConfig = {}, remarkPlugins = [], rehypePlugins = [] } = opts;
+ const input = new VFile({ value: content, path: fileURL })
const scopedClassName = opts.$?.scopedClassName;
const isMDX = mode === 'mdx';
const { headers, rehypeCollectHeaders } = createCollectHeaders();
- await Promise.all([loadRemarkExpressions(), loadRemarkJsx()]); // Vite bug: dynamically import() these because of CJS interop (this will cache)
-
let parser = unified()
.use(markdown)
- .use(isMDX ? [remarkJsx, remarkExpressions] : [])
+ .use(isMDX ? [remarkMdxish, remarkMarkAndUnravel] : [])
.use([remarkUnwrap]);
if (remarkPlugins.length === 0 && rehypePlugins.length === 0) {
@@ -68,7 +74,13 @@ export async function renderMarkdown(
markdownToHtml as any,
{
allowDangerousHtml: true,
- passThrough: ['raw', 'mdxTextExpression', 'mdxJsxTextElement', 'mdxJsxFlowElement'],
+ passThrough: [
+ 'raw',
+ 'mdxFlowExpression',
+ 'mdxJsxFlowElement',
+ 'mdxJsxTextElement',
+ 'mdxTextExpression',
+ ],
},
],
]);
@@ -87,7 +99,7 @@ export async function renderMarkdown(
const vfile = await parser
.use([rehypeCollectHeaders])
.use(rehypeStringify, { allowDangerousHtml: true })
- .process(content);
+ .process(input);
result = vfile.toString();
} catch (err) {
console.error(err);
diff --git a/packages/markdown/remark/src/mdast-util-mdxish.ts b/packages/markdown/remark/src/mdast-util-mdxish.ts
new file mode 100644
index 000000000..52a99deeb
--- /dev/null
+++ b/packages/markdown/remark/src/mdast-util-mdxish.ts
@@ -0,0 +1,18 @@
+import {
+ mdxExpressionFromMarkdown,
+ mdxExpressionToMarkdown
+} from 'mdast-util-mdx-expression'
+import {mdxJsxFromMarkdown, mdxJsxToMarkdown} from 'mdast-util-mdx-jsx'
+
+export function mdxFromMarkdown(): any {
+ return [mdxExpressionFromMarkdown, mdxJsxFromMarkdown]
+}
+
+export function mdxToMarkdown(): any {
+ return {
+ extensions: [
+ mdxExpressionToMarkdown,
+ mdxJsxToMarkdown,
+ ]
+ }
+}
diff --git a/packages/markdown/remark/src/rehype-collect-headers.ts b/packages/markdown/remark/src/rehype-collect-headers.ts
index 927f96590..77126ab7e 100644
--- a/packages/markdown/remark/src/rehype-collect-headers.ts
+++ b/packages/markdown/remark/src/rehype-collect-headers.ts
@@ -17,15 +17,38 @@ export default function createCollectHeaders() {
if (!level) return;
const depth = Number.parseInt(level);
+ let raw = '';
let text = '';
-
- visit(node, 'text', (child) => {
- text += child.value;
+ let isJSX = false;
+ visit(node, (child) => {
+ if (child.type === 'element') {
+ return;
+ }
+ if (child.type === 'raw') {
+ // HACK: serialized JSX from internal plugins, ignore these for slug
+ if (child.value.startsWith('\n<') || child.value.endsWith('>\n')) {
+ raw += child.value.replace(/^\n|\n$/g, '');
+ return;
+ }
+ }
+ if (child.type === 'text' || child.type === 'raw') {
+ raw += child.value;
+ text += child.value;
+ isJSX = isJSX || child.value.includes('{');
+ }
});
+
node.properties = node.properties || {};
if (typeof node.properties.id !== 'string') {
- node.properties.id = slugger.slug(text);
+ if (isJSX) {
+ // HACK: for ids that have JSX content, use $$slug helper to generate slug at runtime
+ node.properties.id = `$$slug(\`${text.replace(/\{/g, '${')}\`)`;
+ (node as any).type = 'raw';
+ (node as any).value = `<${node.tagName} id={${node.properties.id}}>${raw}${node.tagName}>`;
+ } else {
+ node.properties.id = slugger.slug(text);
+ }
}
headers.push({ depth, slug: node.properties.id, text });
diff --git a/packages/markdown/remark/src/rehype-expressions.ts b/packages/markdown/remark/src/rehype-expressions.ts
index 26d04623d..f06f242e2 100644
--- a/packages/markdown/remark/src/rehype-expressions.ts
+++ b/packages/markdown/remark/src/rehype-expressions.ts
@@ -3,8 +3,14 @@ import { map } from 'unist-util-map';
export default function rehypeExpressions(): any {
return function (node: any): any {
return map(node, (child) => {
+ if (child.type === 'text') {
+ return { ...child, type: 'raw' };
+ }
if (child.type === 'mdxTextExpression') {
- return { type: 'text', value: `{${(child as any).value}}` };
+ return { type: 'raw', value: `{${(child as any).value}}` };
+ }
+ if (child.type === 'mdxFlowExpression') {
+ return { type: 'raw', value: `{${(child as any).value}}` };
}
return child;
});
diff --git a/packages/markdown/remark/src/rehype-jsx.ts b/packages/markdown/remark/src/rehype-jsx.ts
index cccbd5548..62eb977c0 100644
--- a/packages/markdown/remark/src/rehype-jsx.ts
+++ b/packages/markdown/remark/src/rehype-jsx.ts
@@ -8,19 +8,41 @@ export default function rehypeJsx(): any {
return { ...child, tagName: `${child.tagName}` };
}
if (MDX_ELEMENTS.has(child.type)) {
- return {
- ...child,
- type: 'element',
- tagName: `${child.name}`,
- properties: child.attributes.reduce((acc: any[], entry: any) => {
+ const attrs = child.attributes.reduce((acc: any[], entry: any) => {
let attr = entry.value;
if (attr && typeof attr === 'object') {
attr = `{${attr.value}}`;
+ } else if (attr && entry.type === 'mdxJsxExpressionAttribute') {
+ attr = `{${attr}}`
} else if (attr === null) {
- attr = `{true}`;
+ attr = "";
+ } else if (typeof attr === 'string') {
+ attr = `"${attr}"`;
}
- return Object.assign(acc, { [entry.name]: attr });
- }, {}),
+ if (!entry.name) {
+ return acc + ` ${attr}`;
+ }
+ return acc + ` ${entry.name}${attr ? '=' : ''}${attr}`;
+ }, '');
+
+ if (child.children.length === 0) {
+ return {
+ type: 'raw',
+ value: `<${child.name}${attrs} />`
+ };
+ }
+ child.children.splice(0, 0, {
+ type: 'raw',
+ value: `\n<${child.name}${attrs}>`
+ })
+ child.children.push({
+ type: 'raw',
+ value: `${child.name}>\n`
+ })
+ return {
+ ...child,
+ type: 'element',
+ tagName: `Fragment`,
};
}
return child;
diff --git a/packages/markdown/remark/src/remark-expressions.ts b/packages/markdown/remark/src/remark-expressions.ts
deleted file mode 100644
index 8e7af19f3..000000000
--- a/packages/markdown/remark/src/remark-expressions.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// Vite bug: dynamically import() modules needed for CJS. Cache in memory to keep side effects
-let mdxExpressionFromMarkdown: any;
-let mdxExpressionToMarkdown: any;
-
-export function remarkExpressions(this: any, options: any) {
- let settings = options || {};
- let data = this.data();
-
- add('fromMarkdownExtensions', mdxExpressionFromMarkdown);
- add('toMarkdownExtensions', mdxExpressionToMarkdown);
-
- function add(field: any, value: any) {
- /* istanbul ignore if - other extensions. */
- if (data[field]) data[field].push(value);
- else data[field] = [value];
- }
-}
-
-export async function loadRemarkExpressions() {
- if (!mdxExpressionFromMarkdown || !mdxExpressionToMarkdown) {
- const mdastUtilMdxExpression = await import('mdast-util-mdx-expression');
- mdxExpressionFromMarkdown = mdastUtilMdxExpression.mdxExpressionFromMarkdown;
- mdxExpressionToMarkdown = mdastUtilMdxExpression.mdxExpressionToMarkdown;
- }
-}
diff --git a/packages/markdown/remark/src/remark-jsx.ts b/packages/markdown/remark/src/remark-jsx.ts
deleted file mode 100644
index 637bac9ee..000000000
--- a/packages/markdown/remark/src/remark-jsx.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-// Vite bug: dynamically import() modules needed for CJS. Cache in memory to keep side effects
-let mdxJsx: any;
-let mdxJsxFromMarkdown: any;
-let mdxJsxToMarkdown: any;
-
-export function remarkJsx(this: any, options: any) {
- let settings = options || {};
- let data = this.data();
-
- // TODO this seems to break adding slugs, no idea why add('micromarkExtensions', mdxJsx({}));
- add('fromMarkdownExtensions', mdxJsxFromMarkdown);
- add('toMarkdownExtensions', mdxJsxToMarkdown);
-
- function add(field: any, value: any) {
- /* istanbul ignore if - other extensions. */
- if (data[field]) data[field].push(value);
- else data[field] = [value];
- }
-}
-
-export async function loadRemarkJsx() {
- if (!mdxJsx) {
- const micromarkMdxJsx = await import('micromark-extension-mdx-jsx');
- mdxJsx = micromarkMdxJsx.mdxJsx;
- }
- if (!mdxJsxFromMarkdown || !mdxJsxToMarkdown) {
- const mdastUtilMdxJsx = await import('mdast-util-mdx-jsx');
- mdxJsxFromMarkdown = mdastUtilMdxJsx.mdxJsxFromMarkdown;
- mdxJsxToMarkdown = mdastUtilMdxJsx.mdxJsxToMarkdown;
- }
-}
diff --git a/packages/markdown/remark/src/remark-mark-and-unravel.ts b/packages/markdown/remark/src/remark-mark-and-unravel.ts
new file mode 100644
index 000000000..4490e4a93
--- /dev/null
+++ b/packages/markdown/remark/src/remark-mark-and-unravel.ts
@@ -0,0 +1,81 @@
+// https://github.com/mdx-js/mdx/blob/main/packages/mdx/lib/plugin/remark-mark-and-unravel.js
+/**
+ * @typedef {import('mdast').Root} Root
+ * @typedef {import('mdast').Content} Content
+ * @typedef {Root|Content} Node
+ * @typedef {Extract} Parent
+ *
+ * @typedef {import('remark-mdx')} DoNotTouchAsThisImportItIncludesMdxInTree
+ */
+
+import {visit} from 'unist-util-visit'
+
+/**
+ * A tiny plugin that unravels `x
` but also
+ * `
` (so it has no knowledge of “HTML”).
+ * It also marks JSX as being explicitly JSX, so when a user passes a `h1`
+ * component, it is used for `# heading` but not for `heading
`.
+ *
+ * @type {import('unified').Plugin, Root>}
+ */
+export default function remarkMarkAndUnravel() {
+ return (tree: any) => {
+ visit(tree, (node, index, parent_) => {
+ const parent = /** @type {Parent} */ (parent_)
+ let offset = -1
+ let all = true
+ /** @type {boolean|undefined} */
+ let oneOrMore
+
+ if (parent && typeof index === 'number' && node.type === 'paragraph') {
+ const children = node.children
+
+ while (++offset < children.length) {
+ const child = children[offset]
+
+ if (
+ child.type === 'mdxJsxTextElement' ||
+ child.type === 'mdxTextExpression'
+ ) {
+ oneOrMore = true
+ } else if (
+ child.type === 'text' &&
+ /^[\t\r\n ]+$/.test(String(child.value))
+ ) {
+ // Empty.
+ } else {
+ all = false
+ break
+ }
+ }
+
+ if (all && oneOrMore) {
+ offset = -1
+
+ while (++offset < children.length) {
+ const child = children[offset]
+
+ if (child.type === 'mdxJsxTextElement') {
+ child.type = 'mdxJsxFlowElement'
+ }
+
+ if (child.type === 'mdxTextExpression') {
+ child.type = 'mdxFlowExpression'
+ }
+ }
+
+ parent.children.splice(index, 1, ...children)
+ return index
+ }
+ }
+
+ if (
+ node.type === 'mdxJsxFlowElement' ||
+ node.type === 'mdxJsxTextElement'
+ ) {
+ const data = node.data || (node.data = {})
+ data._mdxExplicitJsx = true
+ }
+ })
+ }
+}
diff --git a/packages/markdown/remark/src/remark-mdxish.ts b/packages/markdown/remark/src/remark-mdxish.ts
new file mode 100644
index 000000000..b5d41d228
--- /dev/null
+++ b/packages/markdown/remark/src/remark-mdxish.ts
@@ -0,0 +1,15 @@
+import {mdxjs} from 'micromark-extension-mdxjs'
+import { mdxFromMarkdown, mdxToMarkdown } from './mdast-util-mdxish.js'
+
+export default function remarkMdxish(this: any, options = {}) {
+ const data = this.data()
+
+ add('micromarkExtensions', mdxjs(options))
+ add('fromMarkdownExtensions', mdxFromMarkdown())
+ add('toMarkdownExtensions', mdxToMarkdown())
+
+ function add(field: string, value: unknown) {
+ const list = data[field] ? data[field] : (data[field] = [])
+ list.push(value)
+ }
+}
diff --git a/packages/markdown/remark/src/remark-shiki.ts b/packages/markdown/remark/src/remark-shiki.ts
index e00156cb5..0b51f07ff 100644
--- a/packages/markdown/remark/src/remark-shiki.ts
+++ b/packages/markdown/remark/src/remark-shiki.ts
@@ -11,7 +11,7 @@ import type { ShikiConfig } from './types.js';
const highlighterCacheAsync = new Map>();
const remarkShiki = async (
- { langs, theme, wrap }: ShikiConfig,
+ { langs = [], theme = 'github-dark', wrap = false }: ShikiConfig,
scopedClassName?: string | null
) => {
const cacheID: string = typeof theme === 'string' ? theme : theme.name;
diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts
index 3aef31710..af9778c9a 100644
--- a/packages/markdown/remark/src/types.ts
+++ b/packages/markdown/remark/src/types.ts
@@ -20,21 +20,23 @@ export type RehypePlugin = unified.Plugi
export type RehypePlugins = (string | [string, any] | RehypePlugin | [RehypePlugin, any])[];
export interface ShikiConfig {
- langs: ILanguageRegistration[];
- theme: Theme | IThemeRegistration;
- wrap: boolean | null;
+ langs?: ILanguageRegistration[];
+ theme?: Theme | IThemeRegistration;
+ wrap?: boolean | null;
}
export interface AstroMarkdownOptions {
- mode: 'md' | 'mdx';
- drafts: boolean;
- syntaxHighlight: 'shiki' | 'prism' | false;
- shikiConfig: ShikiConfig;
- remarkPlugins: RemarkPlugins;
- rehypePlugins: RehypePlugins;
+ mode?: 'md' | 'mdx';
+ drafts?: boolean;
+ syntaxHighlight?: 'shiki' | 'prism' | false;
+ shikiConfig?: ShikiConfig;
+ remarkPlugins?: RemarkPlugins;
+ rehypePlugins?: RehypePlugins;
}
export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
+ /** @internal */
+ fileURL?: URL;
/** @internal */
$?: {
scopedClassName: string | null;
diff --git a/packages/markdown/remark/test/components.test.js b/packages/markdown/remark/test/components.test.js
new file mode 100644
index 000000000..0f3418c24
--- /dev/null
+++ b/packages/markdown/remark/test/components.test.js
@@ -0,0 +1,62 @@
+import { renderMarkdown } from '../dist/index.js';
+import chai from 'chai';
+
+describe('components', () => {
+ it('should be able to serialize string', async () => {
+ const { code } = await renderMarkdown(``, {});
+
+ chai.expect(code).to.equal(``);
+ });
+
+ it('should be able to serialize boolean attribute', async () => {
+ const { code } = await renderMarkdown(``, {});
+
+ chai.expect(code).to.equal(``);
+ });
+
+ it('should be able to serialize array', async () => {
+ const { code } = await renderMarkdown(``, {});
+
+ chai.expect(code).to.equal(``);
+ });
+
+ it('should be able to serialize object', async () => {
+ const { code } = await renderMarkdown(``, {});
+
+ chai.expect(code).to.equal(``);
+ });
+
+ it('should be able to serialize empty attribute', async () => {
+ const { code } = await renderMarkdown(``, {});
+
+ chai.expect(code).to.equal(``);
+ });
+
+ // Notable omission: shorthand attribute
+
+ it('should be able to serialize spread attribute', async () => {
+ const { code } = await renderMarkdown(``, {});
+
+ chai.expect(code).to.equal(``);
+ });
+
+ it('should allow client:* directives', async () => {
+ const { code } = await renderMarkdown(``, {});
+
+ chai.expect(code).to.equal(``);
+ });
+
+ it('should normalize children', async () => {
+ const { code } = await renderMarkdown(`Hello world!`, {});
+
+ chai.expect(code).to.equal(`\nHello world!\n`);
+ });
+
+ it('should allow markdown without many spaces', async () => {
+ const { code } = await renderMarkdown(`
+# Hello world!
+`, {});
+
+ chai.expect(code).to.equal(`\nHello world!
\n`);
+ });
+});
diff --git a/packages/markdown/remark/test/expressions.test.js b/packages/markdown/remark/test/expressions.test.js
new file mode 100644
index 000000000..bcc95cbed
--- /dev/null
+++ b/packages/markdown/remark/test/expressions.test.js
@@ -0,0 +1,40 @@
+import { renderMarkdown } from '../dist/index.js';
+import chai from 'chai';
+
+describe('expressions', () => {
+ it('should be able to serialize bare expession', async () => {
+ const { code } = await renderMarkdown(`{a}`, {});
+
+ chai.expect(code).to.equal(`{a}`);
+ });
+
+ it('should be able to serialize expression inside component', async () => {
+ const { code } = await renderMarkdown(`{a}`, {});
+
+ chai.expect(code).to.equal(`\n{a}\n`);
+ });
+
+ it('should be able to serialize expression inside markdown', async () => {
+ const { code } = await renderMarkdown(`# {frontmatter.title}`, {});
+
+ chai.expect(code).to.equal(`{frontmatter.title}
`);
+ });
+
+ it('should be able to serialize complex expression inside markdown', async () => {
+ const { code } = await renderMarkdown(`# Hello {frontmatter.name}`, {});
+
+ chai.expect(code).to.equal(`Hello {frontmatter.name}
`);
+ });
+
+ it('should be able to serialize complex expression with markup inside markdown', async () => {
+ const { code } = await renderMarkdown(`# Hello {frontmatter.name}`, {});
+
+ chai.expect(code).to.equal(`Hello {frontmatter.name}
`);
+ });
+
+ it('should be able to serialize function expression', async () => {
+ const { code } = await renderMarkdown(`{frontmatter.list.map(item => {item}
)}` , {});
+
+ chai.expect(code).to.equal(`{frontmatter.list.map(item => {item}
)}`);
+ })
+});
diff --git a/packages/markdown/remark/test/plugins.test.js b/packages/markdown/remark/test/plugins.test.js
new file mode 100644
index 000000000..4954047b5
--- /dev/null
+++ b/packages/markdown/remark/test/plugins.test.js
@@ -0,0 +1,26 @@
+import { renderMarkdown } from '../dist/index.js';
+import chai from 'chai';
+
+import { fileURLToPath } from 'node:url';
+
+describe('plugins', () => {
+ // https://github.com/withastro/astro/issues/3264
+ it('should be able to get file path when passing fileURL', async () => {
+ let context;
+ await renderMarkdown(`test`, {
+ fileURL: new URL('virtual.md', import.meta.url),
+ remarkPlugins: [
+ function () {
+ const transformer = (tree, file) => {
+ context = file;
+ };
+
+ return transformer;
+ }
+ ]
+ });
+
+ chai.expect(typeof context).to.equal('object');
+ chai.expect(context.path).to.equal(fileURLToPath(new URL('virtual.md', import.meta.url)));
+ });
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 11d4a719e..a2b98c3d5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1690,18 +1690,23 @@ importers:
packages/markdown/remark:
specifiers:
'@astrojs/prism': ^0.4.1
+ '@types/chai': ^4.3.1
'@types/github-slugger': ^1.3.0
'@types/hast': ^2.3.4
'@types/mdast': ^3.0.10
+ '@types/mocha': ^9.1.1
'@types/prismjs': ^1.26.0
'@types/unist': ^2.0.6
assert: ^2.0.0
astro-scripts: workspace:*
+ chai: ^4.3.6
github-slugger: ^1.4.0
mdast-util-mdx-expression: ^1.2.0
mdast-util-mdx-jsx: ^1.2.0
mdast-util-to-string: ^3.1.0
micromark-extension-mdx-jsx: ^1.0.3
+ micromark-extension-mdxjs: ^1.0.0
+ mocha: ^9.2.2
prismjs: ^1.28.0
rehype-raw: ^6.1.1
rehype-stringify: ^9.0.3
@@ -1713,6 +1718,7 @@ importers:
unified: ^10.1.2
unist-util-map: ^3.1.1
unist-util-visit: ^4.1.0
+ vfile: ^5.3.2
dependencies:
'@astrojs/prism': link:../../astro-prism
assert: 2.0.0
@@ -1721,6 +1727,7 @@ importers:
mdast-util-mdx-jsx: 1.2.0
mdast-util-to-string: 3.1.0
micromark-extension-mdx-jsx: 1.0.3
+ micromark-extension-mdxjs: 1.0.0
prismjs: 1.28.0
rehype-raw: 6.1.1
rehype-stringify: 9.0.3
@@ -1732,13 +1739,18 @@ importers:
unified: 10.1.2
unist-util-map: 3.1.1
unist-util-visit: 4.1.0
+ vfile: 5.3.2
devDependencies:
+ '@types/chai': 4.3.1
'@types/github-slugger': 1.3.0
'@types/hast': 2.3.4
'@types/mdast': 3.0.10
+ '@types/mocha': 9.1.1
'@types/prismjs': 1.26.0
'@types/unist': 2.0.6
astro-scripts: link:../../../scripts
+ chai: 4.3.6
+ mocha: 9.2.2
packages/telemetry:
specifiers:
@@ -6976,7 +6988,6 @@ packages:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
acorn: 8.7.1
- dev: true
/acorn-node/1.8.2:
resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==}
@@ -10343,6 +10354,18 @@ packages:
micromark-util-types: 1.0.2
dev: false
+ /micromark-extension-mdx-expression/1.0.3:
+ resolution: {integrity: sha512-TjYtjEMszWze51NJCZmhv7MEBcgYRgb3tJeMAJ+HQCAaZHHRBaDCccqQzGizR/H4ODefP44wRTgOn2vE5I6nZA==}
+ dependencies:
+ micromark-factory-mdx-expression: 1.0.6
+ micromark-factory-space: 1.0.0
+ micromark-util-character: 1.1.0
+ micromark-util-events-to-acorn: 1.1.0
+ micromark-util-symbol: 1.0.1
+ micromark-util-types: 1.0.2
+ uvu: 0.5.3
+ dev: false
+
/micromark-extension-mdx-jsx/1.0.3:
resolution: {integrity: sha512-VfA369RdqUISF0qGgv2FfV7gGjHDfn9+Qfiv5hEwpyr1xscRj/CiVRkU7rywGFCO7JwJ5L0e7CJz60lY52+qOA==}
dependencies:
@@ -10357,6 +10380,38 @@ packages:
vfile-message: 3.1.2
dev: false
+ /micromark-extension-mdx-md/1.0.0:
+ resolution: {integrity: sha512-xaRAMoSkKdqZXDAoSgp20Azm0aRQKGOl0RrS81yGu8Hr/JhMsBmfs4wR7m9kgVUIO36cMUQjNyiyDKPrsv8gOw==}
+ dependencies:
+ micromark-util-types: 1.0.2
+ dev: false
+
+ /micromark-extension-mdxjs-esm/1.0.3:
+ resolution: {integrity: sha512-2N13ol4KMoxb85rdDwTAC6uzs8lMX0zeqpcyx7FhS7PxXomOnLactu8WI8iBNXW8AVyea3KIJd/1CKnUmwrK9A==}
+ dependencies:
+ micromark-core-commonmark: 1.0.6
+ micromark-util-character: 1.1.0
+ micromark-util-events-to-acorn: 1.1.0
+ micromark-util-symbol: 1.0.1
+ micromark-util-types: 1.0.2
+ unist-util-position-from-estree: 1.1.1
+ uvu: 0.5.3
+ vfile-message: 3.1.2
+ dev: false
+
+ /micromark-extension-mdxjs/1.0.0:
+ resolution: {integrity: sha512-TZZRZgeHvtgm+IhtgC2+uDMR7h8eTKF0QUX9YsgoL9+bADBpBY6SiLvWqnBlLbCEevITmTqmEuY3FoxMKVs1rQ==}
+ dependencies:
+ acorn: 8.7.1
+ acorn-jsx: 5.3.2_acorn@8.7.1
+ micromark-extension-mdx-expression: 1.0.3
+ micromark-extension-mdx-jsx: 1.0.3
+ micromark-extension-mdx-md: 1.0.0
+ micromark-extension-mdxjs-esm: 1.0.3
+ micromark-util-combine-extensions: 1.0.0
+ micromark-util-types: 1.0.2
+ dev: false
+
/micromark-factory-destination/1.0.0:
resolution: {integrity: sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==}
dependencies: