blog/plugin/remark-admonitions.ts

146 lines
4.6 KiB
TypeScript

// 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";
const remarkAdmonitions: RemarkPlugin = function (providedConfig?: Partial<Config>) {
return (tree) => {
visit(tree, handleNode({ ...defaultConfig, ...providedConfig }));
};
};
export default remarkAdmonitions;
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,
hProperties: { className: classNameMap(config.classNameMaps.block)(checkedTitle) },
// The blockquote should be rendered as a div, which is explicitly required by GitHub
hName: "div",
});
};
export const mkdocsConfig: Partial<Config> = {
classNameMaps: {
block: (title) => [
"admonition",
...(title.startsWith("admonition: ") ? title.substring("admonition: ".length) : title).split(
" ",
),
],
title: "admonition-title",
},
titleFilter: (title) =>
(title.startsWith("[!admonition: ") && title.endsWith("]")) ||
(Boolean(title.match(/^\[!(attention|caution|danger|error|hint|important|note|tip|warning)/)) &&
title.endsWith("]")),
titleTextMap: (title) => {
title = title.substring(2, title.length - 1);
// ' "' will not occur in classes
const i = title.indexOf(' "');
const displayTitle =
i >= 0
? title.substring(i + 2, title.length - 1) // Display title is wrapped with ""
: "";
const checkedTitle = title.substring(0, i);
return { displayTitle, checkedTitle };
},
};
export interface Config {
classNameMaps: {
block: ClassNameMap;
title: ClassNameMap;
};
titleFilter: NameFilter;
titleTextMap: (title: string) => { displayTitle: string; checkedTitle: string };
dataMaps: {
block: (data: Data) => Data;
title: (data: Data) => Data;
};
titleKeepTrailingWhitespaces: boolean;
legacyTitle: boolean;
}
export const defaultConfig: Config = {
classNameMaps: {
block: "admonition",
title: "admonition-title",
},
titleFilter: ["[!NOTE]", "[!IMPORTANT]", "[!WARNING]"],
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) => {
const classNames = typeof gen == "function" ? gen(title) : gen;
return typeof classNames == "object" ? classNames.join(" ") : classNames;
};
}
type NameFilter = ((title: string) => boolean) | string[];
export function nameFilter(filter: NameFilter) {
return (title: string) => {
return typeof filter == "function" ? filter(title) : filter.includes(title);
};
}