@astrojs/mdx 0.9.0 → 0.10.2-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +20 -0
- package/README.md +20 -3
- package/dist/astro-data-utils.js +4 -2
- package/dist/index.js +11 -10
- package/dist/remark-shiki.d.ts +3 -0
- package/dist/remark-shiki.js +58 -0
- package/package.json +3 -3
- package/src/astro-data-utils.ts +8 -2
- package/src/index.ts +15 -15
- package/src/remark-shiki.ts +85 -0
- package/test/fixtures/mdx-frontmatter/src/layouts/Base.astro +4 -0
- package/test/fixtures/mdx-page/src/pages/index.mdx +2 -0
- package/test/fixtures/mdx-page/src/styles.css +3 -0
- package/test/mdx-frontmatter.test.js +5 -1
- package/test/mdx-page.test.js +9 -0
- package/test/mdx-syntax-highlighting.test.js +26 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
[
|
|
2
|
-
[
|
|
3
|
-
[
|
|
4
|
-
[
|
|
5
|
-
[
|
|
1
|
+
[35m@astrojs/mdx:build: [0mcache hit, replaying output [2m9bf2042a57eb9fcc[0m
|
|
2
|
+
[35m@astrojs/mdx:build: [0m
|
|
3
|
+
[35m@astrojs/mdx:build: [0m> @astrojs/mdx@0.10.2-next.0 build /home/runner/work/astro/astro/packages/integrations/mdx
|
|
4
|
+
[35m@astrojs/mdx:build: [0m> astro-scripts build "src/**/*.ts" && tsc
|
|
5
|
+
[35m@astrojs/mdx:build: [0m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @astrojs/mdx
|
|
2
2
|
|
|
3
|
+
## 0.10.2-next.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#4423](https://github.com/withastro/astro/pull/4423) [`d4cd7a59f`](https://github.com/withastro/astro/commit/d4cd7a59fd38d411c442a818cfaab40f74106628) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Update Markdown type signature to match new markdown plugin,and update top-level layout props for better alignment
|
|
8
|
+
|
|
9
|
+
## 0.10.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#4443](https://github.com/withastro/astro/pull/4443) [`adb207979`](https://github.com/withastro/astro/commit/adb20797962c280d4d38f335f577fd52a1b48d4b) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix MDX style imports when layout is not applied
|
|
14
|
+
|
|
15
|
+
* [#4428](https://github.com/withastro/astro/pull/4428) [`a2414bf59`](https://github.com/withastro/astro/commit/a2414bf59e2e2cd633aece68e724401c4ad281b9) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix dev server reload performance when globbing from an MDX layout
|
|
16
|
+
|
|
17
|
+
## 0.10.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- [#4292](https://github.com/withastro/astro/pull/4292) [`f1a52c18a`](https://github.com/withastro/astro/commit/f1a52c18afe66e6d310743ae6884be76f69be265) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Switch from Shiki Twoslash to Astro's Shiki Markdown highlighter
|
|
22
|
+
|
|
3
23
|
## 0.9.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -26,11 +26,11 @@ The `astro add` command-line tool automates the installation for you. Run one of
|
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
28
|
# Using NPM
|
|
29
|
-
|
|
29
|
+
npm run astro add mdx
|
|
30
30
|
# Using Yarn
|
|
31
31
|
yarn astro add mdx
|
|
32
32
|
# Using PNPM
|
|
33
|
-
|
|
33
|
+
pnpm astro add mdx
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Then, restart the dev server by typing `CTRL-C` and then `npm run astro dev` in the terminal window that was running Astro.
|
|
@@ -253,7 +253,7 @@ const { title, fancyJsHelper } = Astro.props;
|
|
|
253
253
|
|
|
254
254
|
The MDX integration respects [your project's `markdown.syntaxHighlight` configuration](https://docs.astro.build/en/guides/markdown-content/#syntax-highlighting).
|
|
255
255
|
|
|
256
|
-
We will highlight your code blocks with [Shiki](https://github.com/shikijs/shiki) by default
|
|
256
|
+
We will highlight your code blocks with [Shiki](https://github.com/shikijs/shiki) by default. You can customize this highlighter using the `markdown.shikiConfig` option in your `astro.config`. For example, you can apply a different built-in theme like so:
|
|
257
257
|
|
|
258
258
|
```js
|
|
259
259
|
// astro.config.mjs
|
|
@@ -285,6 +285,23 @@ export default {
|
|
|
285
285
|
|
|
286
286
|
This applies a minimal Prism renderer with added support for `astro` code blocks. Visit [our "Prism configuration" docs](https://docs.astro.build/en/guides/markdown-content/#prism-configuration) for more on using Prism with Astro.
|
|
287
287
|
|
|
288
|
+
#### Switch to a custom syntax highlighter
|
|
289
|
+
|
|
290
|
+
You may want to apply your own syntax highlighter too. If your highlighter offers a remark or rehype plugin, you can flip off our syntax highlighting by setting `markdown.syntaxHighlight: false` and wiring up your plugin. For example, say you want to apply [Shiki Twoslash's remark plugin](https://www.npmjs.com/package/remark-shiki-twoslash):
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
// astro.config.mjs
|
|
294
|
+
import shikiTwoslash from 'remark-shiki-twoslash';
|
|
295
|
+
|
|
296
|
+
export default {
|
|
297
|
+
markdown: {
|
|
298
|
+
syntaxHighlight: false,
|
|
299
|
+
},
|
|
300
|
+
integrations: [mdx({
|
|
301
|
+
remarkPlugins: [shikiTwoslash, { /* Shiki Twoslash config */ }],
|
|
302
|
+
})],
|
|
303
|
+
```
|
|
304
|
+
|
|
288
305
|
## Configuration
|
|
289
306
|
|
|
290
307
|
### remarkPlugins
|
package/dist/astro-data-utils.js
CHANGED
|
@@ -18,9 +18,9 @@ function rehypeApplyFrontmatterExport(pageFrontmatter) {
|
|
|
18
18
|
exportNodes.unshift(
|
|
19
19
|
jsToTreeNode(
|
|
20
20
|
`import { jsx as layoutJsx } from 'astro/jsx-runtime';
|
|
21
|
-
import Layout from ${JSON.stringify(frontmatter.layout)};
|
|
22
21
|
|
|
23
|
-
export default function ({ children }) {
|
|
22
|
+
export default async function ({ children }) {
|
|
23
|
+
const Layout = (await import(${JSON.stringify(frontmatter.layout)})).default;
|
|
24
24
|
const { layout, ...content } = frontmatter;
|
|
25
25
|
content.file = file;
|
|
26
26
|
content.url = url;
|
|
@@ -41,6 +41,8 @@ function rehypeApplyFrontmatterExport(pageFrontmatter) {
|
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
43
|
return layoutJsx(Layout, {
|
|
44
|
+
file,
|
|
45
|
+
url,
|
|
44
46
|
content,
|
|
45
47
|
frontmatter: content,
|
|
46
48
|
headings: getHeadings(),
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,12 @@ import mdxPlugin from "@mdx-js/rollup";
|
|
|
3
3
|
import { parse as parseESM } from "es-module-lexer";
|
|
4
4
|
import rehypeRaw from "rehype-raw";
|
|
5
5
|
import remarkGfm from "remark-gfm";
|
|
6
|
-
import remarkShikiTwoslash from "remark-shiki-twoslash";
|
|
7
6
|
import remarkSmartypants from "remark-smartypants";
|
|
8
7
|
import { VFile } from "vfile";
|
|
9
8
|
import { rehypeApplyFrontmatterExport, remarkInitializeAstroData } from "./astro-data-utils.js";
|
|
10
9
|
import rehypeCollectHeadings from "./rehype-collect-headings.js";
|
|
11
10
|
import remarkPrism from "./remark-prism.js";
|
|
11
|
+
import remarkShiki from "./remark-shiki.js";
|
|
12
12
|
import { getFileInfo, parseFrontmatter } from "./utils.js";
|
|
13
13
|
const DEFAULT_REMARK_PLUGINS = [
|
|
14
14
|
remarkGfm,
|
|
@@ -22,14 +22,13 @@ function handleExtends(config, defaults = []) {
|
|
|
22
22
|
return config;
|
|
23
23
|
return [...defaults, ...(config == null ? void 0 : config.extends) ?? []];
|
|
24
24
|
}
|
|
25
|
-
function getRemarkPlugins(mdxOptions, config) {
|
|
25
|
+
async function getRemarkPlugins(mdxOptions, config) {
|
|
26
26
|
let remarkPlugins = [
|
|
27
27
|
remarkInitializeAstroData,
|
|
28
28
|
...handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS)
|
|
29
29
|
];
|
|
30
30
|
if (config.markdown.syntaxHighlight === "shiki") {
|
|
31
|
-
|
|
32
|
-
remarkPlugins.push([shikiTwoslash, config.markdown.shikiConfig]);
|
|
31
|
+
remarkPlugins.push([await remarkShiki(config.markdown.shikiConfig)]);
|
|
33
32
|
}
|
|
34
33
|
if (config.markdown.syntaxHighlight === "prism") {
|
|
35
34
|
remarkPlugins.push(remarkPrism);
|
|
@@ -37,10 +36,10 @@ function getRemarkPlugins(mdxOptions, config) {
|
|
|
37
36
|
return remarkPlugins;
|
|
38
37
|
}
|
|
39
38
|
function getRehypePlugins(mdxOptions, config) {
|
|
40
|
-
let rehypePlugins =
|
|
41
|
-
|
|
42
|
-
rehypePlugins
|
|
43
|
-
|
|
39
|
+
let rehypePlugins = [
|
|
40
|
+
[rehypeRaw, { passThrough: nodeTypes }],
|
|
41
|
+
...handleExtends(mdxOptions.rehypePlugins, DEFAULT_REHYPE_PLUGINS)
|
|
42
|
+
];
|
|
44
43
|
rehypePlugins.unshift(rehypeCollectHeadings);
|
|
45
44
|
return rehypePlugins;
|
|
46
45
|
}
|
|
@@ -48,10 +47,10 @@ function mdx(mdxOptions = {}) {
|
|
|
48
47
|
return {
|
|
49
48
|
name: "@astrojs/mdx",
|
|
50
49
|
hooks: {
|
|
51
|
-
"astro:config:setup": ({ updateConfig, config, addPageExtension, command }) => {
|
|
50
|
+
"astro:config:setup": async ({ updateConfig, config, addPageExtension, command }) => {
|
|
52
51
|
addPageExtension(".mdx");
|
|
53
52
|
const mdxPluginOpts = {
|
|
54
|
-
remarkPlugins: getRemarkPlugins(mdxOptions, config),
|
|
53
|
+
remarkPlugins: await getRemarkPlugins(mdxOptions, config),
|
|
55
54
|
rehypePlugins: getRehypePlugins(mdxOptions, config),
|
|
56
55
|
jsx: true,
|
|
57
56
|
jsxImportSource: "astro",
|
|
@@ -86,6 +85,8 @@ function mdx(mdxOptions = {}) {
|
|
|
86
85
|
transform(code, id) {
|
|
87
86
|
if (!id.endsWith(".mdx"))
|
|
88
87
|
return;
|
|
88
|
+
code += `
|
|
89
|
+
MDXContent[Symbol.for('astro.needsHeadRendering')] = !Boolean(frontmatter.layout);`;
|
|
89
90
|
const [, moduleExports] = parseESM(code);
|
|
90
91
|
const { fileUrl, fileId } = getFileInfo(id, config);
|
|
91
92
|
if (!moduleExports.includes("url")) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getHighlighter } from "shiki";
|
|
2
|
+
import { visit } from "unist-util-visit";
|
|
3
|
+
const highlighterCacheAsync = /* @__PURE__ */ new Map();
|
|
4
|
+
const remarkShiki = async ({ langs = [], theme = "github-dark", wrap = false }) => {
|
|
5
|
+
const cacheID = typeof theme === "string" ? theme : theme.name;
|
|
6
|
+
let highlighterAsync = highlighterCacheAsync.get(cacheID);
|
|
7
|
+
if (!highlighterAsync) {
|
|
8
|
+
highlighterAsync = getHighlighter({ theme });
|
|
9
|
+
highlighterCacheAsync.set(cacheID, highlighterAsync);
|
|
10
|
+
}
|
|
11
|
+
const highlighter = await highlighterAsync;
|
|
12
|
+
for (const lang of langs) {
|
|
13
|
+
await highlighter.loadLanguage(lang);
|
|
14
|
+
}
|
|
15
|
+
return () => (tree) => {
|
|
16
|
+
visit(tree, "code", (node) => {
|
|
17
|
+
let lang;
|
|
18
|
+
if (typeof node.lang === "string") {
|
|
19
|
+
const langExists = highlighter.getLoadedLanguages().includes(node.lang);
|
|
20
|
+
if (langExists) {
|
|
21
|
+
lang = node.lang;
|
|
22
|
+
} else {
|
|
23
|
+
console.warn(`The language "${node.lang}" doesn't exist, falling back to plaintext.`);
|
|
24
|
+
lang = "plaintext";
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
lang = "plaintext";
|
|
28
|
+
}
|
|
29
|
+
let html = highlighter.codeToHtml(node.value, { lang });
|
|
30
|
+
html = html.replace('<pre class="shiki"', `<pre class="astro-code"`);
|
|
31
|
+
html = html.replace(
|
|
32
|
+
/style="(background-)?color: var\(--shiki-/g,
|
|
33
|
+
'style="$1color: var(--astro-code-'
|
|
34
|
+
);
|
|
35
|
+
if (node.lang === "diff") {
|
|
36
|
+
html = html.replace(
|
|
37
|
+
/<span class="line"><span style="(.*?)">([\+|\-])/g,
|
|
38
|
+
'<span class="line"><span style="$1"><span style="user-select: none;">$2</span>'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (wrap === false) {
|
|
42
|
+
html = html.replace(/style="(.*?)"/, 'style="$1; overflow-x: auto;"');
|
|
43
|
+
} else if (wrap === true) {
|
|
44
|
+
html = html.replace(
|
|
45
|
+
/style="(.*?)"/,
|
|
46
|
+
'style="$1; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
node.type = "html";
|
|
50
|
+
node.value = html;
|
|
51
|
+
node.children = [];
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
var remark_shiki_default = remarkShiki;
|
|
56
|
+
export {
|
|
57
|
+
remark_shiki_default as default
|
|
58
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrojs/mdx",
|
|
3
3
|
"description": "Use MDX within Astro",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.2-next.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"author": "withastro",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"rehype-raw": "^6.1.1",
|
|
34
34
|
"remark-frontmatter": "^4.0.1",
|
|
35
35
|
"remark-gfm": "^3.0.1",
|
|
36
|
-
"remark-shiki-twoslash": "^3.1.0",
|
|
37
36
|
"remark-smartypants": "^2.0.0",
|
|
38
37
|
"shiki": "^0.10.1",
|
|
39
38
|
"unist-util-visit": "^4.1.0",
|
|
@@ -43,13 +42,14 @@
|
|
|
43
42
|
"@types/chai": "^4.3.1",
|
|
44
43
|
"@types/mocha": "^9.1.1",
|
|
45
44
|
"@types/yargs-parser": "^21.0.0",
|
|
46
|
-
"astro": "1.0.
|
|
45
|
+
"astro": "1.1.0-next.0",
|
|
47
46
|
"astro-scripts": "0.0.7",
|
|
48
47
|
"chai": "^4.3.6",
|
|
49
48
|
"linkedom": "^0.14.12",
|
|
50
49
|
"mdast-util-to-string": "^3.1.0",
|
|
51
50
|
"mocha": "^9.2.2",
|
|
52
51
|
"reading-time": "^1.5.0",
|
|
52
|
+
"remark-shiki-twoslash": "^3.1.0",
|
|
53
53
|
"remark-toc": "^8.0.1"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
package/src/astro-data-utils.ts
CHANGED
|
@@ -20,13 +20,17 @@ export function rehypeApplyFrontmatterExport(pageFrontmatter: Record<string, any
|
|
|
20
20
|
jsToTreeNode(`export const ${EXPORT_NAME} = ${JSON.stringify(frontmatter)};`),
|
|
21
21
|
];
|
|
22
22
|
if (frontmatter.layout) {
|
|
23
|
+
// NOTE(bholmesdev) 08-22-2022
|
|
24
|
+
// Using an async layout import (i.e. `const Layout = (await import...)`)
|
|
25
|
+
// Preserves the dev server import cache when globbing a large set of MDX files
|
|
26
|
+
// Full explanation: 'https://github.com/withastro/astro/pull/4428'
|
|
23
27
|
exportNodes.unshift(
|
|
24
28
|
jsToTreeNode(
|
|
25
29
|
/** @see 'vite-plugin-markdown' for layout props reference */
|
|
26
30
|
`import { jsx as layoutJsx } from 'astro/jsx-runtime';
|
|
27
|
-
import Layout from ${JSON.stringify(frontmatter.layout)};
|
|
28
31
|
|
|
29
|
-
export default function ({ children }) {
|
|
32
|
+
export default async function ({ children }) {
|
|
33
|
+
const Layout = (await import(${JSON.stringify(frontmatter.layout)})).default;
|
|
30
34
|
const { layout, ...content } = frontmatter;
|
|
31
35
|
content.file = file;
|
|
32
36
|
content.url = url;
|
|
@@ -47,6 +51,8 @@ export function rehypeApplyFrontmatterExport(pageFrontmatter: Record<string, any
|
|
|
47
51
|
}
|
|
48
52
|
});
|
|
49
53
|
return layoutJsx(Layout, {
|
|
54
|
+
file,
|
|
55
|
+
url,
|
|
50
56
|
content,
|
|
51
57
|
frontmatter: content,
|
|
52
58
|
headings: getHeadings(),
|
package/src/index.ts
CHANGED
|
@@ -4,13 +4,13 @@ import type { AstroConfig, AstroIntegration } from 'astro';
|
|
|
4
4
|
import { parse as parseESM } from 'es-module-lexer';
|
|
5
5
|
import rehypeRaw from 'rehype-raw';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
|
-
import remarkShikiTwoslash from 'remark-shiki-twoslash';
|
|
8
7
|
import remarkSmartypants from 'remark-smartypants';
|
|
9
8
|
import { VFile } from 'vfile';
|
|
10
9
|
import type { Plugin as VitePlugin } from 'vite';
|
|
11
10
|
import { rehypeApplyFrontmatterExport, remarkInitializeAstroData } from './astro-data-utils.js';
|
|
12
11
|
import rehypeCollectHeadings from './rehype-collect-headings.js';
|
|
13
12
|
import remarkPrism from './remark-prism.js';
|
|
13
|
+
import remarkShiki from './remark-shiki.js';
|
|
14
14
|
import { getFileInfo, parseFrontmatter } from './utils.js';
|
|
15
15
|
|
|
16
16
|
type WithExtends<T> = T | { extends: T };
|
|
@@ -38,22 +38,17 @@ function handleExtends<T>(config: WithExtends<T[] | undefined>, defaults: T[] =
|
|
|
38
38
|
return [...defaults, ...(config?.extends ?? [])];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function getRemarkPlugins(
|
|
41
|
+
async function getRemarkPlugins(
|
|
42
42
|
mdxOptions: MdxOptions,
|
|
43
43
|
config: AstroConfig
|
|
44
|
-
): MdxRollupPluginOptions['remarkPlugins'] {
|
|
44
|
+
): Promise<MdxRollupPluginOptions['remarkPlugins']> {
|
|
45
45
|
let remarkPlugins = [
|
|
46
46
|
// Initialize vfile.data.astroExports before all plugins are run
|
|
47
47
|
remarkInitializeAstroData,
|
|
48
48
|
...handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS),
|
|
49
49
|
];
|
|
50
50
|
if (config.markdown.syntaxHighlight === 'shiki') {
|
|
51
|
-
|
|
52
|
-
// Workarounds tried:
|
|
53
|
-
// - "import * as remarkShikiTwoslash"
|
|
54
|
-
// - "import { default as remarkShikiTwoslash }"
|
|
55
|
-
const shikiTwoslash = (remarkShikiTwoslash as any).default ?? remarkShikiTwoslash;
|
|
56
|
-
remarkPlugins.push([shikiTwoslash, config.markdown.shikiConfig]);
|
|
51
|
+
remarkPlugins.push([await remarkShiki(config.markdown.shikiConfig)]);
|
|
57
52
|
}
|
|
58
53
|
if (config.markdown.syntaxHighlight === 'prism') {
|
|
59
54
|
remarkPlugins.push(remarkPrism);
|
|
@@ -65,11 +60,11 @@ function getRehypePlugins(
|
|
|
65
60
|
mdxOptions: MdxOptions,
|
|
66
61
|
config: AstroConfig
|
|
67
62
|
): MdxRollupPluginOptions['rehypePlugins'] {
|
|
68
|
-
let rehypePlugins =
|
|
63
|
+
let rehypePlugins = [
|
|
64
|
+
[rehypeRaw, { passThrough: nodeTypes }] as any,
|
|
65
|
+
...handleExtends(mdxOptions.rehypePlugins, DEFAULT_REHYPE_PLUGINS),
|
|
66
|
+
];
|
|
69
67
|
|
|
70
|
-
if (config.markdown.syntaxHighlight === 'shiki' || config.markdown.syntaxHighlight === 'prism') {
|
|
71
|
-
rehypePlugins.unshift([rehypeRaw, { passThrough: nodeTypes }]);
|
|
72
|
-
}
|
|
73
68
|
// getHeadings() is guaranteed by TS, so we can't allow user to override
|
|
74
69
|
rehypePlugins.unshift(rehypeCollectHeadings);
|
|
75
70
|
|
|
@@ -80,11 +75,11 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
|
|
|
80
75
|
return {
|
|
81
76
|
name: '@astrojs/mdx',
|
|
82
77
|
hooks: {
|
|
83
|
-
'astro:config:setup': ({ updateConfig, config, addPageExtension, command }: any) => {
|
|
78
|
+
'astro:config:setup': async ({ updateConfig, config, addPageExtension, command }: any) => {
|
|
84
79
|
addPageExtension('.mdx');
|
|
85
80
|
|
|
86
81
|
const mdxPluginOpts: MdxRollupPluginOptions = {
|
|
87
|
-
remarkPlugins: getRemarkPlugins(mdxOptions, config),
|
|
82
|
+
remarkPlugins: await getRemarkPlugins(mdxOptions, config),
|
|
88
83
|
rehypePlugins: getRehypePlugins(mdxOptions, config),
|
|
89
84
|
jsx: true,
|
|
90
85
|
jsxImportSource: 'astro',
|
|
@@ -124,6 +119,11 @@ export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
|
|
|
124
119
|
// These transforms must happen *after* JSX runtime transformations
|
|
125
120
|
transform(code, id) {
|
|
126
121
|
if (!id.endsWith('.mdx')) return;
|
|
122
|
+
|
|
123
|
+
// Ensures styles and scripts are injected into a `<head>`
|
|
124
|
+
// When a layout is not applied
|
|
125
|
+
code += `\nMDXContent[Symbol.for('astro.needsHeadRendering')] = !Boolean(frontmatter.layout);`;
|
|
126
|
+
|
|
127
127
|
const [, moduleExports] = parseESM(code);
|
|
128
128
|
|
|
129
129
|
const { fileUrl, fileId } = getFileInfo(id, config);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ShikiConfig } from 'astro';
|
|
2
|
+
import type * as shiki from 'shiki';
|
|
3
|
+
import { getHighlighter } from 'shiki';
|
|
4
|
+
import { visit } from 'unist-util-visit';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* getHighlighter() is the most expensive step of Shiki. Instead of calling it on every page,
|
|
8
|
+
* cache it here as much as possible. Make sure that your highlighters can be cached, state-free.
|
|
9
|
+
* We make this async, so that multiple calls to parse markdown still share the same highlighter.
|
|
10
|
+
*/
|
|
11
|
+
const highlighterCacheAsync = new Map<string, Promise<shiki.Highlighter>>();
|
|
12
|
+
|
|
13
|
+
const remarkShiki = async ({ langs = [], theme = 'github-dark', wrap = false }: ShikiConfig) => {
|
|
14
|
+
const cacheID: string = typeof theme === 'string' ? theme : theme.name;
|
|
15
|
+
let highlighterAsync = highlighterCacheAsync.get(cacheID);
|
|
16
|
+
if (!highlighterAsync) {
|
|
17
|
+
highlighterAsync = getHighlighter({ theme });
|
|
18
|
+
highlighterCacheAsync.set(cacheID, highlighterAsync);
|
|
19
|
+
}
|
|
20
|
+
const highlighter = await highlighterAsync;
|
|
21
|
+
|
|
22
|
+
// NOTE: There may be a performance issue here for large sites that use `lang`.
|
|
23
|
+
// Since this will be called on every page load. Unclear how to fix this.
|
|
24
|
+
for (const lang of langs) {
|
|
25
|
+
await highlighter.loadLanguage(lang);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return () => (tree: any) => {
|
|
29
|
+
visit(tree, 'code', (node) => {
|
|
30
|
+
let lang: string;
|
|
31
|
+
|
|
32
|
+
if (typeof node.lang === 'string') {
|
|
33
|
+
const langExists = highlighter.getLoadedLanguages().includes(node.lang);
|
|
34
|
+
if (langExists) {
|
|
35
|
+
lang = node.lang;
|
|
36
|
+
} else {
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.warn(`The language "${node.lang}" doesn't exist, falling back to plaintext.`);
|
|
39
|
+
lang = 'plaintext';
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
lang = 'plaintext';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let html = highlighter!.codeToHtml(node.value, { lang });
|
|
46
|
+
|
|
47
|
+
// Q: Couldn't these regexes match on a user's inputted code blocks?
|
|
48
|
+
// A: Nope! All rendered HTML is properly escaped.
|
|
49
|
+
// Ex. If a user typed `<span class="line"` into a code block,
|
|
50
|
+
// It would become this before hitting our regexes:
|
|
51
|
+
// <span class="line"
|
|
52
|
+
|
|
53
|
+
// Replace "shiki" class naming with "astro".
|
|
54
|
+
html = html.replace('<pre class="shiki"', `<pre class="astro-code"`);
|
|
55
|
+
// Replace "shiki" css variable naming with "astro".
|
|
56
|
+
html = html.replace(
|
|
57
|
+
/style="(background-)?color: var\(--shiki-/g,
|
|
58
|
+
'style="$1color: var(--astro-code-'
|
|
59
|
+
);
|
|
60
|
+
// Add "user-select: none;" for "+"/"-" diff symbols
|
|
61
|
+
if (node.lang === 'diff') {
|
|
62
|
+
html = html.replace(
|
|
63
|
+
/<span class="line"><span style="(.*?)">([\+|\-])/g,
|
|
64
|
+
'<span class="line"><span style="$1"><span style="user-select: none;">$2</span>'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
// Handle code wrapping
|
|
68
|
+
// if wrap=null, do nothing.
|
|
69
|
+
if (wrap === false) {
|
|
70
|
+
html = html.replace(/style="(.*?)"/, 'style="$1; overflow-x: auto;"');
|
|
71
|
+
} else if (wrap === true) {
|
|
72
|
+
html = html.replace(
|
|
73
|
+
/style="(.*?)"/,
|
|
74
|
+
'style="$1; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
node.type = 'html';
|
|
79
|
+
node.value = html;
|
|
80
|
+
node.children = [];
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default remarkShiki;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
const {
|
|
3
3
|
content = { title: "content didn't work" },
|
|
4
|
+
file = "file didn't work",
|
|
5
|
+
url = "url didn't work",
|
|
4
6
|
frontmatter = {
|
|
5
7
|
title: "frontmatter didn't work",
|
|
6
8
|
file: "file didn't work",
|
|
@@ -24,6 +26,8 @@ const {
|
|
|
24
26
|
<p data-frontmatter-title>{frontmatter.title}</p>
|
|
25
27
|
<p data-frontmatter-file>{frontmatter.file}</p>
|
|
26
28
|
<p data-frontmatter-url>{frontmatter.url}</p>
|
|
29
|
+
<p data-file>{frontmatter.file}</p>
|
|
30
|
+
<p data-url>{frontmatter.url}</p>
|
|
27
31
|
<p data-layout-rendered>Layout rendered!</p>
|
|
28
32
|
<ul data-headings>
|
|
29
33
|
{headings.map(heading => <li>{heading.slug}</li>)}
|
|
@@ -57,17 +57,21 @@ describe('MDX frontmatter', () => {
|
|
|
57
57
|
expect(headingSlugs).to.contain('section-2');
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
it('passes "file" and "url" to layout
|
|
60
|
+
it('passes "file" and "url" to layout', async () => {
|
|
61
61
|
const html = await fixture.readFile('/with-headings/index.html');
|
|
62
62
|
const { document } = parseHTML(html);
|
|
63
63
|
|
|
64
64
|
const frontmatterFile = document.querySelector('[data-frontmatter-file]')?.textContent;
|
|
65
65
|
const frontmatterUrl = document.querySelector('[data-frontmatter-url]')?.textContent;
|
|
66
|
+
const file = document.querySelector('[data-file]')?.textContent;
|
|
67
|
+
const url = document.querySelector('[data-url]')?.textContent;
|
|
66
68
|
|
|
67
69
|
expect(frontmatterFile?.endsWith('with-headings.mdx')).to.equal(
|
|
68
70
|
true,
|
|
69
71
|
'"file" prop does not end with correct path or is undefined'
|
|
70
72
|
);
|
|
71
73
|
expect(frontmatterUrl).to.equal('/with-headings');
|
|
74
|
+
expect(file).to.equal(frontmatterFile);
|
|
75
|
+
expect(url).to.equal(frontmatterUrl);
|
|
72
76
|
});
|
|
73
77
|
});
|
package/test/mdx-page.test.js
CHANGED
|
@@ -26,6 +26,15 @@ describe('MDX Page', () => {
|
|
|
26
26
|
|
|
27
27
|
expect(h1.textContent).to.equal('Hello page!');
|
|
28
28
|
});
|
|
29
|
+
|
|
30
|
+
it('injects style imports when layout is not applied', async () => {
|
|
31
|
+
const html = await fixture.readFile('/index.html');
|
|
32
|
+
const { document } = parseHTML(html);
|
|
33
|
+
|
|
34
|
+
const stylesheet = document.querySelector('link[rel="stylesheet"]');
|
|
35
|
+
|
|
36
|
+
expect(stylesheet).to.not.be.null;
|
|
37
|
+
});
|
|
29
38
|
});
|
|
30
39
|
|
|
31
40
|
describe('dev', () => {
|
|
@@ -3,6 +3,7 @@ import mdx from '@astrojs/mdx';
|
|
|
3
3
|
import { expect } from 'chai';
|
|
4
4
|
import { parseHTML } from 'linkedom';
|
|
5
5
|
import { loadFixture } from '../../../astro/test/test-utils.js';
|
|
6
|
+
import shikiTwoslash from 'remark-shiki-twoslash';
|
|
6
7
|
|
|
7
8
|
const FIXTURE_ROOT = new URL('./fixtures/mdx-syntax-hightlighting/', import.meta.url);
|
|
8
9
|
|
|
@@ -21,8 +22,9 @@ describe('MDX syntax highlighting', () => {
|
|
|
21
22
|
const html = await fixture.readFile('/index.html');
|
|
22
23
|
const { document } = parseHTML(html);
|
|
23
24
|
|
|
24
|
-
const shikiCodeBlock = document.querySelector('pre.
|
|
25
|
+
const shikiCodeBlock = document.querySelector('pre.astro-code');
|
|
25
26
|
expect(shikiCodeBlock).to.not.be.null;
|
|
27
|
+
expect(shikiCodeBlock.getAttribute('style')).to.contain('background-color:#0d1117');
|
|
26
28
|
});
|
|
27
29
|
|
|
28
30
|
it('respects markdown.shikiConfig.theme', async () => {
|
|
@@ -41,8 +43,9 @@ describe('MDX syntax highlighting', () => {
|
|
|
41
43
|
const html = await fixture.readFile('/index.html');
|
|
42
44
|
const { document } = parseHTML(html);
|
|
43
45
|
|
|
44
|
-
const shikiCodeBlock = document.querySelector('pre.
|
|
46
|
+
const shikiCodeBlock = document.querySelector('pre.astro-code');
|
|
45
47
|
expect(shikiCodeBlock).to.not.be.null;
|
|
48
|
+
expect(shikiCodeBlock.getAttribute('style')).to.contain('background-color:#282A36');
|
|
46
49
|
});
|
|
47
50
|
});
|
|
48
51
|
|
|
@@ -64,4 +67,25 @@ describe('MDX syntax highlighting', () => {
|
|
|
64
67
|
expect(prismCodeBlock).to.not.be.null;
|
|
65
68
|
});
|
|
66
69
|
});
|
|
70
|
+
|
|
71
|
+
it('supports custom highlighter - shiki-twoslash', async () => {
|
|
72
|
+
const fixture = await loadFixture({
|
|
73
|
+
root: FIXTURE_ROOT,
|
|
74
|
+
markdown: {
|
|
75
|
+
syntaxHighlight: false,
|
|
76
|
+
},
|
|
77
|
+
integrations: [
|
|
78
|
+
mdx({
|
|
79
|
+
remarkPlugins: [shikiTwoslash.default ?? shikiTwoslash],
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
await fixture.build();
|
|
84
|
+
|
|
85
|
+
const html = await fixture.readFile('/index.html');
|
|
86
|
+
const { document } = parseHTML(html);
|
|
87
|
+
|
|
88
|
+
const twoslashCodeBlock = document.querySelector('pre.shiki');
|
|
89
|
+
expect(twoslashCodeBlock).to.not.be.null;
|
|
90
|
+
});
|
|
67
91
|
});
|