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} */ ({
// 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' }],
],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

1049
yarn.lock

File diff suppressed because it is too large Load diff