Commit e17180a
Changed files (4)
src/plugins/rehype-components-async.ts
@@ -0,0 +1,114 @@
+import { isElement } from 'hast-util-is-element';
+import type { Element, ElementContent, Parent, Properties, Root, RootContent } from 'hast';
+
+interface ComponentContext {
+ tree: Root;
+ vfile: unknown;
+ processor: unknown;
+}
+
+type ComponentResult =
+ | ElementContent
+ | ElementContent[]
+ | null
+ | undefined
+ | void
+ | Promise<ElementContent | ElementContent[] | null | undefined | void>;
+
+type ComponentFn = (
+ props: Properties,
+ children: ElementContent[],
+ context: ComponentContext
+) => ComponentResult;
+
+interface Options {
+ components?: Record<string, ComponentFn>;
+}
+
+export function rehypeComponentsAsync(options: Options = {}) {
+ const { components = {} } = options;
+
+ return async function transformer(this: unknown, tree: Root, vfile: unknown) {
+ const context: ComponentContext = {
+ tree,
+ vfile,
+ processor: this,
+ };
+
+ await transformNode(tree, components, context);
+ };
+}
+
+async function transformNode(
+ node: Parent,
+ components: Record<string, ComponentFn>,
+ context: ComponentContext
+): Promise<void> {
+ if (!Array.isArray(node.children)) return;
+
+ for (let index = 0; index < node.children.length; index++) {
+ const child = node.children[index] as RootContent;
+ if (!child) continue;
+
+ if (isElement(child)) {
+ const element = child as Element;
+ const component = components[element.tagName];
+
+ if (component) {
+ const props = (element.properties ?? {}) as Properties;
+ const originalChildren = (element.children ?? []) as ElementContent[];
+
+ let returned = component(props, originalChildren, context);
+ if (isPromiseLike(returned)) {
+ returned = await returned;
+ }
+
+ const normalized = normalizeElementContent(returned);
+
+ if (!normalized.every(isElementContentValue)) {
+ throw new Error(
+ `rehype-components: Component function is expected to return ElementContent or an array of ElementContent, but got ${JSON.stringify(normalized)}.`
+ );
+ }
+
+ node.children.splice(index, 1, ...normalized);
+ index += normalized.length - 1;
+ continue;
+ }
+ }
+
+ if (hasChildren(child)) {
+ await transformNode(child, components, context);
+ }
+ }
+}
+
+function normalizeElementContent(
+ value: ElementContent | ElementContent[] | null | undefined | void
+): ElementContent[] {
+ if (value == null) return [];
+
+ return Array.isArray(value)
+ ? value.filter((item): item is ElementContent => item != null)
+ : [value];
+}
+
+function isPromiseLike<T>(value: unknown): value is Promise<T> {
+ return !!value && typeof (value as Promise<T>).then === 'function';
+}
+
+function hasChildren(value: RootContent): value is Parent & RootContent {
+ if (typeof value !== 'object' || value == null) return false;
+ return Array.isArray((value as Parent).children);
+}
+
+function isElementContentValue(value: unknown): value is ElementContent {
+ if (isElement(value)) return true;
+
+ if (typeof value === 'object' && value !== null && 'type' in value) {
+ const type = (value as { type: string }).type;
+ return type === 'text' || type === 'comment';
+ }
+
+ return false;
+}
astro.config.mjs
@@ -2,6 +2,7 @@
import { buildConfig, siteConfig } from './src/config.ts';
import { CDN } from './src/constants/cdn.ts';
import { pluginLanguageBadge } from './src/plugins/expressive-code-lang-badget.ts';
+import { rehypeComponentsAsync } from './src/plugins/rehype-components-async.ts';
import { rehypeComponentsList } from './src/plugins/rehype-components-list.ts';
import { rehypeWrapTables } from './src/plugins/rehype-wrap-tables.ts';
import { remarkArticleReferences } from './src/plugins/remark-article-references';
@@ -23,7 +24,6 @@ import icon from 'astro-icon';
import pagefind from 'astro-pagefind';
import { defineConfig } from 'astro/config';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
-import rehypeComponents from 'rehype-components';
import rehypeMathJaxCHtml from 'rehype-mathjax/chtml';
import remarkDirective from 'remark-directive';
import remarkDirectiveRehype from 'remark-directive-rehype';
@@ -111,7 +111,7 @@ export default defineConfig({
},
],
rehypeWrapTables,
- [rehypeComponents, { components: rehypeComponentsList }],
+ [rehypeComponentsAsync, { components: rehypeComponentsList }],
],
},
vite: {
package.json
@@ -47,6 +47,7 @@
"daisyui": "^5.2.3",
"dayjs": "^1.11.18",
"hast-util-from-html": "^2.0.3",
+ "hast-util-is-element": "^3.0.0",
"hastscript": "^9.0.1",
"markdown-it": "^14.1.0",
"mdast-util-to-string": "^4.0.0",
pnpm-lock.yaml
@@ -110,6 +110,9 @@ importers:
hast-util-from-html:
specifier: ^2.0.3
version: 2.0.3
+ hast-util-is-element:
+ specifier: ^3.0.0
+ version: 3.0.0
hastscript:
specifier: ^9.0.1
version: 9.0.1