// 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) { 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 = { 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); }; }