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:
Robin Métral 2021-08-25 14:17:45 +02:00 committed by GitHub
parent 3bfd8c125e
commit 397d8f3d84
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 762 additions and 435 deletions

View 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`.

View file

@ -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' }],
], ],

View file

@ -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"
}, },

View file

@ -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"
}, },

View file

@ -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"] }],

View file

@ -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"

View file

@ -1,32 +1,34 @@
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) {
visit(tree, 'code', (node) => {
const { data, meta } = node; const { data, meta } = node;
let lang = node.lang || 'html'; // default to html matches GFM behavior. 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 => {
code.children = code.children.map((child) => {
if (child.type === 'text') { if (child.type === 'text') {
return { ...child, value: child.value.replace(/\{/g, 'ASTRO_ESCAPED_LEFT_CURLY_BRACKET\0') }; return { ...child, value: child.value.replace(/\{/g, 'ASTRO_ESCAPED_LEFT_CURLY_BRACKET\0') };
} }
return child; return child;
}); });
}; };
const visitor = (node: any) => { visit(tree, 'element', (node) => {
if (node.tagName === 'code') { if (node.tagName === 'code') {
escapeCode(node); escapeCode(node);
return; return;
@ -34,10 +36,8 @@ export function rehypeCodeBlock() {
if (node.tagName !== 'pre') return; if (node.tagName !== 'pre') return;
const code = node.children[0]; const code = node.children[0];
if (code.tagName !== 'code') return; if (code.type !== 'element' || code.tagName !== 'code') return;
node.properties = { ...code.properties }; node.properties = { ...code.properties };
});
return node;
}; };
return () => (tree: any) => visit(tree, 'element', visitor);
} }

View file

@ -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;
} }

View file

@ -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)) {

View file

@ -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() {
return function (tree: Root) {
visit(tree, (node) => {
if (node.type !== 'element') return; if (node.type !== 'element') return;
const { tagName, children } = node; const { tagName } = node;
if (tagName[0] !== 'h') return; if (tagName[0] !== 'h') return;
let [_, depth] = tagName.match(/h([0-6])/) ?? []; const [_, level] = tagName.match(/h([0-6])/) ?? [];
if (!depth) return; if (!level) return;
depth = Number.parseInt(depth); 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 || {};
node.data.properties = { ...(node.data.properties as Properties), slug };
headers.push({ depth, slug, text }); headers.push({ depth, slug, text });
});
return node; };
}
return {
headers,
rehypeCollectHeaders,
}; };
return { headers, rehypeCollectHeaders: () => (tree: any) => visit(tree, visitor) };
} }

View file

@ -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 */

1049
yarn.lock

File diff suppressed because it is too large Load diff