132 lines
4.1 KiB
TypeScript
132 lines
4.1 KiB
TypeScript
import { visit } from "unist-util-visit";
|
|
import remarkGfm from "remark-gfm";
|
|
import type { Root, Element, Text as HastText, Properties } from "hast";
|
|
import type { Plugin } from "unified";
|
|
import type { Root as MdastRoot, Code, Text } from "mdast";
|
|
import type { BuildVisitor } from "unist-util-visit";
|
|
|
|
// MDX AST Node Type Definition
|
|
interface MdxJsxFlowElement {
|
|
type: "mdxJsxFlowElement";
|
|
name: string;
|
|
children: Text[];
|
|
}
|
|
|
|
// Extended Properties Type
|
|
interface ExtendedProperties extends Properties {
|
|
className?: string;
|
|
id?: string;
|
|
}
|
|
|
|
// Extended Element Type
|
|
interface ExtendedElement extends Omit<Element, "properties"> {
|
|
properties: ExtendedProperties;
|
|
}
|
|
|
|
// Generate a valid ID, preserving Chinese characters
|
|
const generateValidId = (text: string): string => {
|
|
return encodeURIComponent(
|
|
text
|
|
.trim() // Trim leading/trailing whitespace
|
|
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
|
.replace(/\-\-+/g, "-") // Replace multiple hyphens with a single one
|
|
.replace(/^-+/, "") // Remove leading hyphens
|
|
.replace(/-+$/, "") // Remove trailing hyphens
|
|
);
|
|
};
|
|
|
|
// Get a unique ID
|
|
const getUniqueId = (baseId: string, usedIds: Set<string>): string => {
|
|
let uniqueId = baseId;
|
|
let counter = 1;
|
|
while (usedIds.has(uniqueId)) {
|
|
uniqueId = `${baseId}-${counter}`;
|
|
counter++;
|
|
}
|
|
return uniqueId;
|
|
};
|
|
|
|
export const mdxOptions = {
|
|
mdxOptions: {
|
|
remarkPlugins: [
|
|
remarkGfm,
|
|
// Mermaid code block processing plugin
|
|
(() => {
|
|
return (tree: MdastRoot) => {
|
|
visit(tree, "code", (node: Code) => {
|
|
if (node.lang === "mermaid") {
|
|
const mermaidNode = node as unknown as MdxJsxFlowElement;
|
|
mermaidNode.type = "mdxJsxFlowElement";
|
|
mermaidNode.name = "mermaid";
|
|
mermaidNode.children = [
|
|
{ type: "text", value: node.value } as Text,
|
|
];
|
|
}
|
|
});
|
|
return tree;
|
|
};
|
|
}) as Plugin<[], MdastRoot>,
|
|
],
|
|
rehypePlugins: [
|
|
// Plugin to handle images and tables
|
|
(() => {
|
|
return (tree: Root) => {
|
|
visit(tree, "element", ((
|
|
node: Element,
|
|
index: number | null,
|
|
parent: Element | Root | null
|
|
) => {
|
|
if (node.tagName === "img") {
|
|
if (parent && "tagName" in parent) {
|
|
(parent as ExtendedElement).tagName = "div";
|
|
(parent as ExtendedElement).properties = {
|
|
...((parent as ExtendedElement).properties || {}),
|
|
className: "image-container",
|
|
};
|
|
}
|
|
}
|
|
if (node.tagName === "table") {
|
|
(node as ExtendedElement).properties = {
|
|
...((node as ExtendedElement).properties || {}),
|
|
className: "min-w-full divide-y divide-gray-300",
|
|
};
|
|
}
|
|
}) as BuildVisitor<Root, "element">);
|
|
return tree;
|
|
};
|
|
}) as Plugin<[], Root>,
|
|
|
|
// Plugin to handle heading IDs
|
|
(() => {
|
|
return (tree: Root) => {
|
|
const usedIds = new Set<string>(); // Keep track of used IDs to avoid duplicates
|
|
visit(tree, "element", ((
|
|
node: Element,
|
|
index: number | null,
|
|
parent: Element | Root | null
|
|
) => {
|
|
if (["h1", "h2", "h3"].includes(node.tagName)) {
|
|
let titleText = "";
|
|
visit(node, "text", ((textNode: HastText) => {
|
|
titleText += textNode.value;
|
|
}) as BuildVisitor<Element, "text">);
|
|
|
|
if (titleText) {
|
|
let id = generateValidId(titleText);
|
|
let uniqueId = getUniqueId(id, usedIds); // Handle duplicate IDs by adding a numeric suffix
|
|
usedIds.add(uniqueId);
|
|
|
|
(node as ExtendedElement).properties = {
|
|
...((node as ExtendedElement).properties || {}),
|
|
id: uniqueId,
|
|
};
|
|
}
|
|
}
|
|
}) as BuildVisitor<Root, "element">);
|
|
return tree;
|
|
};
|
|
}) as Plugin<[], Root>,
|
|
],
|
|
},
|
|
};
|