blog/plugin/remark-admonitions.ts

158 lines
4.5 KiB
TypeScript
Raw Normal View History

2023-09-01 15:09:01 +00:00
// https://github.com/myl7/remark-github-beta-blockquote-admonitions
// License: Apache-2.0
import { visit } from "unist-util-visit";
import type { Data } from "unist";
import type { BuildVisitor } from "unist-util-visit";
import type { Blockquote, Paragraph, Text } from "mdast";
import type { RemarkPlugin } from "@astrojs/markdown-remark";
2024-06-29 20:15:48 +00:00
import classNames from "classnames";
2023-09-01 15:09:01 +00:00
2024-04-05 15:34:32 +00:00
const remarkAdmonitions: RemarkPlugin =
(providedConfig?: Partial<Config>) => (tree) => {
2023-09-01 15:09:01 +00:00
visit(tree, handleNode({ ...defaultConfig, ...providedConfig }));
};
export default remarkAdmonitions;
2024-06-29 20:15:48 +00:00
const handleNode =
(config: Config): BuildVisitor =>
(node) => {
// Filter required elems
if (node.type !== "blockquote") return;
const blockquote = node as Blockquote;
if (blockquote.children[0]?.type !== "paragraph") return;
const paragraph = blockquote.children[0];
if (paragraph.children[0]?.type !== "text") return;
const text = paragraph.children[0];
// A link break after the title is explicitly required by GitHub
const titleEnd = text.value.indexOf("\n");
if (titleEnd < 0) return;
const textBody = text.value.substring(titleEnd + 1);
let title = text.value.substring(0, titleEnd);
// Handle whitespaces after the title.
// Whitespace characters are defined by GFM
const m = /[ \t\v\f\r]+$/.exec(title);
if (m && !config.titleKeepTrailingWhitespaces) {
title = title.substring(0, title.length - m[0].length);
}
if (!nameFilter(config.titleFilter)(title)) return;
const { displayTitle, checkedTitle } = config.titleTextMap(title);
// Update the text body
text.value = textBody;
// Insert the title element and add classes for the title
const paragraphTitleText: Text = { type: "text", value: displayTitle };
const paragraphTitle: Paragraph = {
type: "paragraph",
children: [paragraphTitleText],
data: config.dataMaps.title({
hProperties: {
className: classNameMap(config.classNameMaps.title)(checkedTitle),
},
}),
};
blockquote.children.unshift(paragraphTitle);
// Add classes for the block
blockquote.data = config.dataMaps.block({
...blockquote.data,
2024-04-05 15:34:32 +00:00
hProperties: {
2024-06-29 20:15:48 +00:00
className: classNameMap(config.classNameMaps.block)(checkedTitle),
2024-04-05 15:34:32 +00:00
},
2024-06-29 20:15:48 +00:00
// The blockquote should be rendered as a div, which is explicitly required by GitHub
hName: "div",
});
2023-09-01 15:09:01 +00:00
};
2024-06-29 20:15:48 +00:00
const TITLE_PATTERN =
/\[\!admonition: (attention|caution|danger|error|hint|important|note|tip|warning)\]/i;
2023-09-01 15:09:01 +00:00
export const mkdocsConfig: Partial<Config> = {
classNameMaps: {
block: (title) => [
"admonition",
2024-04-05 15:34:32 +00:00
...(title.startsWith("admonition: ")
? title.substring("admonition: ".length)
2024-06-29 20:15:48 +00:00
: title
).split(" "),
2023-09-01 15:09:01 +00:00
],
2024-06-29 20:15:48 +00:00
title: classNames("admonition-title"),
2023-09-01 15:09:01 +00:00
},
2024-06-29 20:15:48 +00:00
titleFilter: (title) => Boolean(title.match(TITLE_PATTERN)),
titleTextMap: (title: string) => {
console.log("title", title);
const match = title.match(TITLE_PATTERN);
console.log("matches", match);
const displayTitle = match?.[1] ?? "";
const checkedTitle = displayTitle;
2023-09-01 15:09:01 +00:00
return { displayTitle, checkedTitle };
},
};
2024-06-29 20:15:48 +00:00
2023-09-01 15:09:01 +00:00
export interface Config {
classNameMaps: {
block: ClassNameMap;
title: ClassNameMap;
};
titleFilter: NameFilter;
2024-06-29 20:15:48 +00:00
titleTextMap: (title: string) => {
displayTitle: string;
checkedTitle: string;
};
2023-09-01 15:09:01 +00:00
dataMaps: {
block: (data: Data) => Data;
title: (data: Data) => Data;
};
titleKeepTrailingWhitespaces: boolean;
legacyTitle: boolean;
}
export const defaultConfig: Config = {
classNameMaps: {
block: "admonition",
title: "admonition-title",
},
2024-06-29 20:15:48 +00:00
2023-09-01 15:09:01 +00:00
titleFilter: ["[!NOTE]", "[!IMPORTANT]", "[!WARNING]"],
2024-06-29 20:15:48 +00:00
2023-09-01 15:09:01 +00:00
titleTextMap: (title) => ({
displayTitle: title.substring(2, title.length - 1),
checkedTitle: title.substring(2, title.length - 1),
}),
dataMaps: {
block: (data) => data,
title: (data) => data,
},
titleKeepTrailingWhitespaces: false,
legacyTitle: false,
};
type ClassNames = string | string[];
type ClassNameMap = ClassNames | ((title: string) => ClassNames);
export function classNameMap(gen: ClassNameMap) {
return (title: string) => {
2024-04-06 02:52:20 +00:00
const classNames = typeof gen === "function" ? gen(title) : gen;
return typeof classNames === "object" ? classNames.join(" ") : classNames;
2023-09-01 15:09:01 +00:00
};
}
type NameFilter = ((title: string) => boolean) | string[];
export function nameFilter(filter: NameFilter) {
return (title: string) => {
2024-04-06 02:52:20 +00:00
return typeof filter === "function"
? filter(title)
: filter.includes(title);
2023-09-01 15:09:01 +00:00
};
}