Upgrade unified deps and improve unified plugins types (#1200)
* Upgrade @astrojs/markdown-support deps and update types * Add changeset * Update changeset * Switch astro-markdown-plugins example to use rehype-autolink-headings Usage of remark-autolink-headings is discouraged in favor of the rehype counterpart: https://github.com/remarkjs/remark-autolink-headings\#remark-autolink-headings * Add stricter types for unified plugins This includes a few suggestions from a code review: - use vfile.toString instead of vfile.value.toString - refactor plugins to follow unified best practices instead of returning functions that return a plugin - use any instead of any[] for plugin options types * Narrow down types to more specific hast or mdast typings
This commit is contained in:
parent
3bfd8c125e
commit
397d8f3d84
12 changed files with 762 additions and 435 deletions
5
.changeset/red-news-tap.md
Normal file
5
.changeset/red-news-tap.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@astrojs/markdown-support': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Upgrade `@astrojs/markdown-support` dependencies. The `remark-rehype@9` upgrade enables accessible footnotes with `remark-footnotes`.
|
|
@ -10,8 +10,9 @@
|
||||||
export default /** @type {import('astro').AstroUserConfig} */ ({
|
export default /** @type {import('astro').AstroUserConfig} */ ({
|
||||||
// Enable Custom Markdown options, plugins, etc.
|
// Enable Custom Markdown options, plugins, etc.
|
||||||
markdownOptions: {
|
markdownOptions: {
|
||||||
remarkPlugins: ['remark-code-titles', 'remark-slug', ['remark-autolink-headings', { behavior: 'prepend' }]],
|
remarkPlugins: ['remark-code-titles', 'remark-slug'],
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
|
['rehype-autolink-headings', { behavior: 'prepend' }],
|
||||||
['rehype-toc', { headings: ['h2', 'h3'] }],
|
['rehype-toc', { headings: ['h2', 'h3'] }],
|
||||||
['rehype-add-classes', { 'h1,h2,h3': 'title' }],
|
['rehype-add-classes', { 'h1,h2,h3': 'title' }],
|
||||||
],
|
],
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"astro": "^0.19.4",
|
"astro": "^0.19.4",
|
||||||
"rehype-add-classes": "^1.0.0",
|
"rehype-add-classes": "^1.0.0",
|
||||||
|
"rehype-autolink-headings": "^6.1.0",
|
||||||
"rehype-toc": "^3.0.2",
|
"rehype-toc": "^3.0.2",
|
||||||
"remark-autolink-headings": "^6.0.1",
|
|
||||||
"remark-code-titles": "^0.1.2",
|
"remark-code-titles": "^0.1.2",
|
||||||
"remark-slug": "^6.0.0"
|
"remark-slug": "^6.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -96,7 +96,6 @@
|
||||||
"string-width": "^5.0.0",
|
"string-width": "^5.0.0",
|
||||||
"supports-esm": "^1.0.0",
|
"supports-esm": "^1.0.0",
|
||||||
"tiny-glob": "^0.2.8",
|
"tiny-glob": "^0.2.8",
|
||||||
"unified": "^9.2.1",
|
|
||||||
"yargs-parser": "^20.2.7",
|
"yargs-parser": "^20.2.7",
|
||||||
"zod": "^3.8.1"
|
"zod": "^3.8.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default {
|
||||||
remarkPlugins: [
|
remarkPlugins: [
|
||||||
'remark-code-titles',
|
'remark-code-titles',
|
||||||
'remark-slug',
|
'remark-slug',
|
||||||
['remark-autolink-headings', { behavior: 'prepend' }],
|
['rehype-autolink-headings', { behavior: 'prepend' }],
|
||||||
],
|
],
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
['rehype-toc', { headings: ["h2", "h3"] }],
|
['rehype-toc', { headings: ["h2", "h3"] }],
|
||||||
|
|
|
@ -19,19 +19,19 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@silvenon/remark-smartypants": "^1.0.0",
|
"@silvenon/remark-smartypants": "^1.0.0",
|
||||||
"github-slugger": "^1.3.0",
|
"github-slugger": "^1.3.0",
|
||||||
"gray-matter": "^4.0.2",
|
"gray-matter": "^4.0.3",
|
||||||
"mdast-util-mdx-expression": "^1.0.0",
|
"mdast-util-mdx-expression": "^1.1.0",
|
||||||
"micromark-extension-mdx-expression": "^1.0.0",
|
"micromark-extension-mdx-expression": "^1.0.0",
|
||||||
"rehype-raw": "^5.1.0",
|
"rehype-raw": "^6.0.0",
|
||||||
"rehype-stringify": "^8.0.0",
|
"rehype-stringify": "^9.0.1",
|
||||||
"remark-footnotes": "^3.0.0",
|
"remark-footnotes": "^4.0.1",
|
||||||
"remark-gfm": "^1.0.0",
|
"remark-gfm": "^2.0.0",
|
||||||
"remark-parse": "^9.0.0",
|
"remark-parse": "^10.0.0",
|
||||||
"remark-rehype": "^8.1.0",
|
"remark-rehype": "^9.0.0",
|
||||||
"remark-slug": "^6.1.0",
|
"remark-slug": "^7.0.0",
|
||||||
"unified": "^9.2.1",
|
"unified": "^10.1.0",
|
||||||
"unist-util-map": "^3.0.0",
|
"unist-util-map": "^3.0.0",
|
||||||
"unist-util-visit": "^3.1.0"
|
"unist-util-visit": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/github-slugger": "^1.3.0"
|
"@types/github-slugger": "^1.3.0"
|
||||||
|
|
|
@ -1,43 +1,43 @@
|
||||||
import { visit } from 'unist-util-visit';
|
import { visit } from 'unist-util-visit';
|
||||||
|
import type { Element, Root as HastRoot, Properties } from 'hast';
|
||||||
|
import type { Root as MdastRoot } from 'mdast';
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
export function remarkCodeBlock() {
|
export function remarkCodeBlock() {
|
||||||
const visitor = (node: any) => {
|
return function (tree: MdastRoot) {
|
||||||
const { data, meta } = node;
|
visit(tree, 'code', (node) => {
|
||||||
let lang = node.lang || 'html'; // default to html matches GFM behavior.
|
const { data, meta } = node;
|
||||||
|
let lang = node.lang || 'html'; // default to html to match GFM behavior.
|
||||||
|
|
||||||
let currentClassName = data?.hProperties?.class ?? '';
|
let currentClassName = (data?.hProperties as Properties)?.class ?? '';
|
||||||
node.data = node.data || {};
|
node.data = node.data || {};
|
||||||
node.data.hProperties = node.data.hProperties || {};
|
node.data.hProperties = node.data.hProperties || {};
|
||||||
node.data.hProperties = { ...node.data.hProperties, class: `language-${lang} ${currentClassName}`.trim(), lang, meta };
|
node.data.hProperties = { ...(node.data.hProperties as Properties), class: `language-${lang} ${currentClassName}`.trim(), lang, meta };
|
||||||
|
});
|
||||||
return node;
|
|
||||||
};
|
};
|
||||||
return () => (tree: any) => visit(tree, 'code', visitor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
export function rehypeCodeBlock() {
|
export function rehypeCodeBlock() {
|
||||||
const escapeCode = (code: any) => {
|
return function (tree: HastRoot) {
|
||||||
code.children = code.children.map((child: any) => {
|
const escapeCode = (code: Element): void => {
|
||||||
if (child.type === 'text') {
|
code.children = code.children.map((child) => {
|
||||||
return { ...child, value: child.value.replace(/\{/g, 'ASTRO_ESCAPED_LEFT_CURLY_BRACKET\0') };
|
if (child.type === 'text') {
|
||||||
|
return { ...child, value: child.value.replace(/\{/g, 'ASTRO_ESCAPED_LEFT_CURLY_BRACKET\0') };
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
visit(tree, 'element', (node) => {
|
||||||
|
if (node.tagName === 'code') {
|
||||||
|
escapeCode(node);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return child;
|
|
||||||
|
if (node.tagName !== 'pre') return;
|
||||||
|
const code = node.children[0];
|
||||||
|
if (code.type !== 'element' || code.tagName !== 'code') return;
|
||||||
|
node.properties = { ...code.properties };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const visitor = (node: any) => {
|
|
||||||
if (node.tagName === 'code') {
|
|
||||||
escapeCode(node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.tagName !== 'pre') return;
|
|
||||||
const code = node.children[0];
|
|
||||||
if (code.tagName !== 'code') return;
|
|
||||||
node.properties = { ...code.properties };
|
|
||||||
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
return () => (tree: any) => visit(tree, 'element', visitor);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { remarkCodeBlock, rehypeCodeBlock } from './codeblock.js';
|
||||||
import { loadPlugins } from './load-plugins.js';
|
import { loadPlugins } from './load-plugins.js';
|
||||||
import raw from 'rehype-raw';
|
import raw from 'rehype-raw';
|
||||||
|
|
||||||
import unified from 'unified';
|
import { unified } from 'unified';
|
||||||
import markdown from 'remark-parse';
|
import markdown from 'remark-parse';
|
||||||
import markdownToHtml from 'remark-rehype';
|
import markdownToHtml from 'remark-rehype';
|
||||||
import rehypeStringify from 'rehype-stringify';
|
import rehypeStringify from 'rehype-stringify';
|
||||||
|
@ -56,7 +56,7 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
|
||||||
parser.use(scopedStyles(scopedClassName));
|
parser.use(scopedStyles(scopedClassName));
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.use(remarkCodeBlock());
|
parser.use(remarkCodeBlock);
|
||||||
parser.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression'] });
|
parser.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression'] });
|
||||||
parser.use(rehypeExpressions);
|
parser.use(rehypeExpressions);
|
||||||
|
|
||||||
|
@ -69,10 +69,10 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
|
||||||
const vfile = await parser
|
const vfile = await parser
|
||||||
.use(raw)
|
.use(raw)
|
||||||
.use(rehypeCollectHeaders)
|
.use(rehypeCollectHeaders)
|
||||||
.use(rehypeCodeBlock())
|
.use(rehypeCodeBlock)
|
||||||
.use(rehypeStringify, { entities: { useNamedReferences: true } })
|
.use(rehypeStringify, { entities: { useNamedReferences: true } })
|
||||||
.process(content);
|
.process(content);
|
||||||
result = vfile.contents.toString();
|
result = vfile.toString();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ async function importPlugin(p: string | UnifiedPluginImport): UnifiedPluginImpor
|
||||||
return await p;
|
return await p;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadPlugins(items: Plugin[]): Promise<[unified.Plugin] | [unified.Plugin, unified.Settings]>[] {
|
export function loadPlugins(items: Plugin[]): Promise<[unified.Plugin] | [unified.Plugin, any]>[] {
|
||||||
return items.map((p) => {
|
return items.map((p) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (Array.isArray(p)) {
|
if (Array.isArray(p)) {
|
||||||
|
|
|
@ -1,32 +1,39 @@
|
||||||
import { visit } from 'unist-util-visit';
|
import { visit } from 'unist-util-visit';
|
||||||
|
import type { Root, Properties } from 'hast';
|
||||||
import slugger from 'github-slugger';
|
import slugger from 'github-slugger';
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
export default function createCollectHeaders() {
|
export default function createCollectHeaders() {
|
||||||
const headers: any[] = [];
|
const headers: any[] = [];
|
||||||
|
|
||||||
const visitor = (node: any) => {
|
function rehypeCollectHeaders() {
|
||||||
if (node.type !== 'element') return;
|
return function (tree: Root) {
|
||||||
const { tagName, children } = node;
|
visit(tree, (node) => {
|
||||||
if (tagName[0] !== 'h') return;
|
if (node.type !== 'element') return;
|
||||||
let [_, depth] = tagName.match(/h([0-6])/) ?? [];
|
const { tagName } = node;
|
||||||
if (!depth) return;
|
if (tagName[0] !== 'h') return;
|
||||||
depth = Number.parseInt(depth);
|
const [_, level] = tagName.match(/h([0-6])/) ?? [];
|
||||||
|
if (!level) return;
|
||||||
|
const depth = Number.parseInt(level);
|
||||||
|
|
||||||
let text = '';
|
let text = '';
|
||||||
|
|
||||||
visit(node, 'text', (child) => {
|
visit(node, 'text', (child) => {
|
||||||
text += (child as any).value;
|
text += child.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
let slug = node.properties.id || slugger.slug(text);
|
let slug = node?.data?.id || slugger.slug(text);
|
||||||
|
|
||||||
node.properties = node.properties || {};
|
node.data = node.data || {};
|
||||||
node.properties.id = slug;
|
node.data.properties = node.data.properties || {};
|
||||||
headers.push({ depth, slug, text });
|
node.data.properties = { ...(node.data.properties as Properties), slug };
|
||||||
|
headers.push({ depth, slug, text });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return node;
|
return {
|
||||||
|
headers,
|
||||||
|
rehypeCollectHeaders,
|
||||||
};
|
};
|
||||||
|
|
||||||
return { headers, rehypeCollectHeaders: () => (tree: any) => visit(tree, visitor) };
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import unified from 'unified';
|
import unified from 'unified';
|
||||||
|
|
||||||
export type UnifiedPluginImport = Promise<{ default: unified.Plugin }>;
|
export type UnifiedPluginImport = Promise<{ default: unified.Plugin }>;
|
||||||
export type Plugin = string | [string, unified.Settings] | UnifiedPluginImport | [UnifiedPluginImport, unified.Settings];
|
export type Plugin = string | [string, any] | UnifiedPluginImport | [UnifiedPluginImport, any];
|
||||||
|
|
||||||
export interface AstroMarkdownOptions {
|
export interface AstroMarkdownOptions {
|
||||||
/** Enable or disable footnotes syntax extension */
|
/** Enable or disable footnotes syntax extension */
|
||||||
|
|
Loading…
Reference in a new issue