1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
import type { AdmonitionType } from "@/types";
import { type Properties, h as _h } from "hastscript";
import type { Node, Paragraph as P, Parent, PhrasingContent, Root } from "mdast";
import type { Directives, LeafDirective, TextDirective } from "mdast-util-directive";
import { directiveToMarkdown } from "mdast-util-directive";
import { toMarkdown } from "mdast-util-to-markdown";
import { toString as mdastToString } from "mdast-util-to-string";
import type { Plugin } from "unified";
import { visit } from "unist-util-visit";
// Supported admonition types
const Admonitions = new Set<AdmonitionType>(["tip", "note", "important", "caution", "warning"]);
/** Checks if a string is a supported admonition type. */
function isAdmonition(s: string): s is AdmonitionType {
return Admonitions.has(s as AdmonitionType);
}
/** Checks if a node is a directive. */
function isNodeDirective(node: Node): node is Directives {
return (
node.type === "containerDirective" ||
node.type === "leafDirective" ||
node.type === "textDirective"
);
}
/**
* From Astro Starlight:
* Transforms directives not supported back to original form as it can break user content and result in 'broken' output.
*/
function transformUnhandledDirective(
node: LeafDirective | TextDirective,
index: number,
parent: Parent,
) {
const textNode = {
type: "text",
value: toMarkdown(node, { extensions: [directiveToMarkdown()] }),
} as const;
if (node.type === "textDirective") {
parent.children[index] = textNode;
} else {
parent.children[index] = {
children: [textNode],
type: "paragraph",
};
}
}
/** From Astro Starlight: Function that generates an mdast HTML tree ready for conversion to HTML by rehype. */
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
function h(el: string, attrs: Properties = {}, children: any[] = []): P {
const { properties, tagName } = _h(el, attrs);
return {
children,
data: { hName: tagName, hProperties: properties },
type: "paragraph",
};
}
export const remarkAdmonitions: Plugin<[], Root> = () => (tree) => {
visit(tree, (node, index, parent) => {
if (!parent || index === undefined || !isNodeDirective(node)) return;
if (node.type === "textDirective" || node.type === "leafDirective") {
transformUnhandledDirective(node, index, parent);
return;
}
const admonitionType = node.name;
if (!isAdmonition(admonitionType)) return;
let title: string = admonitionType;
let titleNode: PhrasingContent[] = [{ type: "text", value: title }];
// Check if there's a custom title
const firstChild = node.children[0];
if (
firstChild?.type === "paragraph" &&
firstChild.data &&
"directiveLabel" in firstChild.data &&
firstChild.children.length > 0
) {
titleNode = firstChild.children;
title = mdastToString(firstChild.children);
// The first paragraph contains a custom title, we can safely remove it.
node.children.splice(0, 1);
}
// Do not change prefix to AD, ADM, or similar, adblocks will block the content inside.
const admonition = h(
"aside",
{ "aria-label": title, class: "admonition", "data-admonition-type": admonitionType },
[
h("p", { class: "admonition-title", "aria-hidden": "true" }, [...titleNode]),
h("div", { class: "admonition-content" }, node.children),
],
);
parent.children[index] = admonition;
});
};
|