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} */ ({
|
||||
// Enable Custom Markdown options, plugins, etc.
|
||||
markdownOptions: {
|
||||
remarkPlugins: ['remark-code-titles', 'remark-slug', ['remark-autolink-headings', { behavior: 'prepend' }]],
|
||||
remarkPlugins: ['remark-code-titles', 'remark-slug'],
|
||||
rehypePlugins: [
|
||||
['rehype-autolink-headings', { behavior: 'prepend' }],
|
||||
['rehype-toc', { headings: ['h2', 'h3'] }],
|
||||
['rehype-add-classes', { 'h1,h2,h3': 'title' }],
|
||||
],
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
"devDependencies": {
|
||||
"astro": "^0.19.4",
|
||||
"rehype-add-classes": "^1.0.0",
|
||||
"rehype-autolink-headings": "^6.1.0",
|
||||
"rehype-toc": "^3.0.2",
|
||||
"remark-autolink-headings": "^6.0.1",
|
||||
"remark-code-titles": "^0.1.2",
|
||||
"remark-slug": "^6.0.0"
|
||||
},
|
||||
|
|
|
@ -96,7 +96,6 @@
|
|||
"string-width": "^5.0.0",
|
||||
"supports-esm": "^1.0.0",
|
||||
"tiny-glob": "^0.2.8",
|
||||
"unified": "^9.2.1",
|
||||
"yargs-parser": "^20.2.7",
|
||||
"zod": "^3.8.1"
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@ export default {
|
|||
remarkPlugins: [
|
||||
'remark-code-titles',
|
||||
'remark-slug',
|
||||
['remark-autolink-headings', { behavior: 'prepend' }],
|
||||
['rehype-autolink-headings', { behavior: 'prepend' }],
|
||||
],
|
||||
rehypePlugins: [
|
||||
['rehype-toc', { headings: ["h2", "h3"] }],
|
||||
|
|
|
@ -19,19 +19,19 @@
|
|||
"dependencies": {
|
||||
"@silvenon/remark-smartypants": "^1.0.0",
|
||||
"github-slugger": "^1.3.0",
|
||||
"gray-matter": "^4.0.2",
|
||||
"mdast-util-mdx-expression": "^1.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"mdast-util-mdx-expression": "^1.1.0",
|
||||
"micromark-extension-mdx-expression": "^1.0.0",
|
||||
"rehype-raw": "^5.1.0",
|
||||
"rehype-stringify": "^8.0.0",
|
||||
"remark-footnotes": "^3.0.0",
|
||||
"remark-gfm": "^1.0.0",
|
||||
"remark-parse": "^9.0.0",
|
||||
"remark-rehype": "^8.1.0",
|
||||
"remark-slug": "^6.1.0",
|
||||
"unified": "^9.2.1",
|
||||
"rehype-raw": "^6.0.0",
|
||||
"rehype-stringify": "^9.0.1",
|
||||
"remark-footnotes": "^4.0.1",
|
||||
"remark-gfm": "^2.0.0",
|
||||
"remark-parse": "^10.0.0",
|
||||
"remark-rehype": "^9.0.0",
|
||||
"remark-slug": "^7.0.0",
|
||||
"unified": "^10.1.0",
|
||||
"unist-util-map": "^3.0.0",
|
||||
"unist-util-visit": "^3.1.0"
|
||||
"unist-util-visit": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/github-slugger": "^1.3.0"
|
||||
|
|
|
@ -1,43 +1,43 @@
|
|||
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() {
|
||||
const visitor = (node: any) => {
|
||||
const { data, meta } = node;
|
||||
let lang = node.lang || 'html'; // default to html matches GFM behavior.
|
||||
return function (tree: MdastRoot) {
|
||||
visit(tree, 'code', (node) => {
|
||||
const { data, meta } = node;
|
||||
let lang = node.lang || 'html'; // default to html to match GFM behavior.
|
||||
|
||||
let currentClassName = data?.hProperties?.class ?? '';
|
||||
node.data = node.data || {};
|
||||
node.data.hProperties = node.data.hProperties || {};
|
||||
node.data.hProperties = { ...node.data.hProperties, class: `language-${lang} ${currentClassName}`.trim(), lang, meta };
|
||||
|
||||
return node;
|
||||
let currentClassName = (data?.hProperties as Properties)?.class ?? '';
|
||||
node.data = node.data || {};
|
||||
node.data.hProperties = node.data.hProperties || {};
|
||||
node.data.hProperties = { ...(node.data.hProperties as Properties), class: `language-${lang} ${currentClassName}`.trim(), lang, meta };
|
||||
});
|
||||
};
|
||||
return () => (tree: any) => visit(tree, 'code', visitor);
|
||||
}
|
||||
|
||||
/** */
|
||||
export function rehypeCodeBlock() {
|
||||
const escapeCode = (code: any) => {
|
||||
code.children = code.children.map((child: any) => {
|
||||
if (child.type === 'text') {
|
||||
return { ...child, value: child.value.replace(/\{/g, 'ASTRO_ESCAPED_LEFT_CURLY_BRACKET\0') };
|
||||
return function (tree: HastRoot) {
|
||||
const escapeCode = (code: Element): void => {
|
||||
code.children = code.children.map((child) => {
|
||||
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 raw from 'rehype-raw';
|
||||
|
||||
import unified from 'unified';
|
||||
import { unified } from 'unified';
|
||||
import markdown from 'remark-parse';
|
||||
import markdownToHtml from 'remark-rehype';
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
|
@ -56,7 +56,7 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
|
|||
parser.use(scopedStyles(scopedClassName));
|
||||
}
|
||||
|
||||
parser.use(remarkCodeBlock());
|
||||
parser.use(remarkCodeBlock);
|
||||
parser.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression'] });
|
||||
parser.use(rehypeExpressions);
|
||||
|
||||
|
@ -69,10 +69,10 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp
|
|||
const vfile = await parser
|
||||
.use(raw)
|
||||
.use(rehypeCollectHeaders)
|
||||
.use(rehypeCodeBlock())
|
||||
.use(rehypeCodeBlock)
|
||||
.use(rehypeStringify, { entities: { useNamedReferences: true } })
|
||||
.process(content);
|
||||
result = vfile.contents.toString();
|
||||
result = vfile.toString();
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ async function importPlugin(p: string | UnifiedPluginImport): UnifiedPluginImpor
|
|||
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 new Promise((resolve, reject) => {
|
||||
if (Array.isArray(p)) {
|
||||
|
|
|
@ -1,32 +1,39 @@
|
|||
import { visit } from 'unist-util-visit';
|
||||
import type { Root, Properties } from 'hast';
|
||||
import slugger from 'github-slugger';
|
||||
|
||||
/** */
|
||||
export default function createCollectHeaders() {
|
||||
const headers: any[] = [];
|
||||
|
||||
const visitor = (node: any) => {
|
||||
if (node.type !== 'element') return;
|
||||
const { tagName, children } = node;
|
||||
if (tagName[0] !== 'h') return;
|
||||
let [_, depth] = tagName.match(/h([0-6])/) ?? [];
|
||||
if (!depth) return;
|
||||
depth = Number.parseInt(depth);
|
||||
function rehypeCollectHeaders() {
|
||||
return function (tree: Root) {
|
||||
visit(tree, (node) => {
|
||||
if (node.type !== 'element') return;
|
||||
const { tagName } = node;
|
||||
if (tagName[0] !== 'h') return;
|
||||
const [_, level] = tagName.match(/h([0-6])/) ?? [];
|
||||
if (!level) return;
|
||||
const depth = Number.parseInt(level);
|
||||
|
||||
let text = '';
|
||||
let text = '';
|
||||
|
||||
visit(node, 'text', (child) => {
|
||||
text += (child as any).value;
|
||||
});
|
||||
visit(node, 'text', (child) => {
|
||||
text += child.value;
|
||||
});
|
||||
|
||||
let slug = node.properties.id || slugger.slug(text);
|
||||
let slug = node?.data?.id || slugger.slug(text);
|
||||
|
||||
node.properties = node.properties || {};
|
||||
node.properties.id = slug;
|
||||
headers.push({ depth, slug, text });
|
||||
node.data = node.data || {};
|
||||
node.data.properties = node.data.properties || {};
|
||||
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';
|
||||
|
||||
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 {
|
||||
/** Enable or disable footnotes syntax extension */
|
||||
|
|
Loading…
Reference in a new issue