@dr-ishaan/remake-blocks 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +167 -16
- package/dist/accordion.js +28 -0
- package/dist/astro.d.ts +57 -0
- package/dist/astro.d.ts.map +1 -0
- package/dist/astro.js +87 -0
- package/dist/astro.js.map +1 -0
- package/dist/index.d.ts +8 -52
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -86
- package/dist/index.js.map +1 -1
- package/dist/remark-remake-blocks.d.ts +24 -0
- package/dist/remark-remake-blocks.d.ts.map +1 -0
- package/dist/remark-remake-blocks.js +724 -0
- package/dist/remark-remake-blocks.js.map +1 -0
- package/dist/styles.css +225 -4
- package/dist/types.d.ts +76 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -5
- package/dist/types.js.map +1 -1
- package/package.json +36 -16
- package/dist/remark-callout-blocks.d.ts +0 -16
- package/dist/remark-callout-blocks.d.ts.map +0 -1
- package/dist/remark-callout-blocks.js +0 -628
- package/dist/remark-callout-blocks.js.map +0 -1
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remake-blocks
|
|
3
|
+
*
|
|
4
|
+
* A remark plugin that transforms callout directives inside blockquotes
|
|
5
|
+
* into styled callout components, disclosure widgets, and optionally
|
|
6
|
+
* enhances regular blockquotes.
|
|
7
|
+
*
|
|
8
|
+
* 27 first-class callout types + disclosure widgets ([!])
|
|
9
|
+
* + accordion grouping + tree view nesting
|
|
10
|
+
* — each directive maps 1:1 to its own unique visual identity.
|
|
11
|
+
*
|
|
12
|
+
* @security By default, raw HTML in markdown source is HTML-escaped before
|
|
13
|
+
* being placed into callout bodies (safe-by-default). Set
|
|
14
|
+
* `allowDangerousHtml: true` in plugin options to disable escaping —
|
|
15
|
+
* only do this if you fully trust your markdown source.
|
|
16
|
+
*/
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// 27 Built-in callout configurations
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const BUILTIN_CALLOUTS = [
|
|
21
|
+
// ── GFM Primaries (5) ─────────────────────────────────────────────────
|
|
22
|
+
{
|
|
23
|
+
type: "note",
|
|
24
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
|
|
25
|
+
className: "callout-note", defaultTitle: "Note",
|
|
26
|
+
color: "#0969da", backgroundColor: "#ddf4ff", iconColor: "#0969da",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "tip",
|
|
30
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>`,
|
|
31
|
+
className: "callout-tip", defaultTitle: "Tip",
|
|
32
|
+
color: "#1a7f37", backgroundColor: "#dafbe1", iconColor: "#1a7f37",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: "important",
|
|
36
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
|
|
37
|
+
className: "callout-important", defaultTitle: "Important",
|
|
38
|
+
color: "#8250df", backgroundColor: "#fbefff", iconColor: "#8250df",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "warning",
|
|
42
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
|
|
43
|
+
className: "callout-warning", defaultTitle: "Warning",
|
|
44
|
+
color: "#9a6700", backgroundColor: "#fff8c5", iconColor: "#9a6700",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "caution",
|
|
48
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>`,
|
|
49
|
+
className: "callout-caution", defaultTitle: "Caution",
|
|
50
|
+
color: "#cf222e", backgroundColor: "#ffebe9", iconColor: "#cf222e",
|
|
51
|
+
},
|
|
52
|
+
// ── Obsidian Primaries (10) ────────────────────────────────────────────
|
|
53
|
+
{
|
|
54
|
+
type: "abstract",
|
|
55
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>`,
|
|
56
|
+
className: "callout-abstract", defaultTitle: "Abstract",
|
|
57
|
+
color: "#0891b2", backgroundColor: "#ecfeff", iconColor: "#0891b2",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: "info",
|
|
61
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>`,
|
|
62
|
+
className: "callout-info", defaultTitle: "Info",
|
|
63
|
+
color: "#57606a", backgroundColor: "#f6f8fa", iconColor: "#57606a",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: "success",
|
|
67
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="M22 4 12 14.01l-3-3"/></svg>`,
|
|
68
|
+
className: "callout-success", defaultTitle: "Success",
|
|
69
|
+
color: "#1a8840", backgroundColor: "#caf7ca", iconColor: "#1a8840",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "question",
|
|
73
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>`,
|
|
74
|
+
className: "callout-question", defaultTitle: "Question",
|
|
75
|
+
color: "#bf6c06", backgroundColor: "#fff1e5", iconColor: "#bf6c06",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: "failure",
|
|
79
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>`,
|
|
80
|
+
className: "callout-failure", defaultTitle: "Failure",
|
|
81
|
+
color: "#b33a3a", backgroundColor: "#ffe2e2", iconColor: "#b33a3a",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: "danger",
|
|
85
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`,
|
|
86
|
+
className: "callout-danger", defaultTitle: "Danger",
|
|
87
|
+
color: "#cf222e", backgroundColor: "#ffebe9", iconColor: "#cf222e",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: "quote",
|
|
91
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3z"/></svg>`,
|
|
92
|
+
className: "callout-quote", defaultTitle: "Quote",
|
|
93
|
+
color: "#656d76", backgroundColor: "#f6f8fa", iconColor: "#656d76",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: "bug",
|
|
97
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>`,
|
|
98
|
+
className: "callout-bug", defaultTitle: "Bug",
|
|
99
|
+
color: "#c93257", backgroundColor: "#ffedf2", iconColor: "#c93257",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: "example",
|
|
103
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>`,
|
|
104
|
+
className: "callout-example", defaultTitle: "Example",
|
|
105
|
+
color: "#8250df", backgroundColor: "#fbefff", iconColor: "#8250df",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: "todo",
|
|
109
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="m9 12 2 2 4-4"/></svg>`,
|
|
110
|
+
className: "callout-todo", defaultTitle: "Todo",
|
|
111
|
+
color: "#2274a5", backgroundColor: "#e5f2fc", iconColor: "#2274a5",
|
|
112
|
+
},
|
|
113
|
+
// ── Promoted Aliases — first-class types (12) ──────────────────────────
|
|
114
|
+
{
|
|
115
|
+
type: "summary",
|
|
116
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>`,
|
|
117
|
+
className: "callout-summary", defaultTitle: "Summary",
|
|
118
|
+
color: "#0e7490", backgroundColor: "#f0fdfa", iconColor: "#0e7490",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
type: "tldr",
|
|
122
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`,
|
|
123
|
+
className: "callout-tldr", defaultTitle: "TL;DR",
|
|
124
|
+
color: "#06b6d4", backgroundColor: "#ecfeff", iconColor: "#06b6d4",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: "hint",
|
|
128
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 18v3c0 .6.4 1 1 1h4v-3h3v-3h2l1.4-1.4a6.5 6.5 0 1 0-4-4Z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/></svg>`,
|
|
129
|
+
className: "callout-hint", defaultTitle: "Hint",
|
|
130
|
+
color: "#3d8b37", backgroundColor: "#e8f5e9", iconColor: "#3d8b37",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: "check",
|
|
134
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="m9 12 2 2 4-4"/></svg>`,
|
|
135
|
+
className: "callout-check", defaultTitle: "Check",
|
|
136
|
+
color: "#0d9488", backgroundColor: "#f0fdfa", iconColor: "#0d9488",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: "done",
|
|
140
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="M22 4 12 14.01l-3-3"/></svg>`,
|
|
141
|
+
className: "callout-done", defaultTitle: "Done",
|
|
142
|
+
color: "#15803d", backgroundColor: "#dcfce7", iconColor: "#15803d",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: "help",
|
|
146
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>`,
|
|
147
|
+
className: "callout-help", defaultTitle: "Help",
|
|
148
|
+
color: "#c26506", backgroundColor: "#fff7ed", iconColor: "#c26506",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: "faq",
|
|
152
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M8 10h.01"/><path d="M12 10h.01"/><path d="M16 10h.01"/></svg>`,
|
|
153
|
+
className: "callout-faq", defaultTitle: "FAQ",
|
|
154
|
+
color: "#b45309", backgroundColor: "#fffbeb", iconColor: "#b45309",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: "attention",
|
|
158
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`,
|
|
159
|
+
className: "callout-attention", defaultTitle: "Attention",
|
|
160
|
+
color: "#ca8a04", backgroundColor: "#fefce8", iconColor: "#ca8a04",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: "fail",
|
|
164
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>`,
|
|
165
|
+
className: "callout-fail", defaultTitle: "Fail",
|
|
166
|
+
color: "#be123c", backgroundColor: "#fff1f2", iconColor: "#be123c",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
type: "missing",
|
|
170
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>`,
|
|
171
|
+
className: "callout-missing", defaultTitle: "Missing",
|
|
172
|
+
color: "#9f3a3a", backgroundColor: "#fef2f2", iconColor: "#9f3a3a",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
type: "error",
|
|
176
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`,
|
|
177
|
+
className: "callout-error", defaultTitle: "Error",
|
|
178
|
+
color: "#dc2626", backgroundColor: "#fef2f2", iconColor: "#dc2626",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: "cite",
|
|
182
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M13 5v2"/><path d="M13 17v2"/><path d="M13 11v2"/></svg>`,
|
|
183
|
+
className: "callout-cite", defaultTitle: "Cite",
|
|
184
|
+
color: "#5b6abf", backgroundColor: "#eef2ff", iconColor: "#5b6abf",
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Default plugin options
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
const DEFAULT_OPTIONS = {
|
|
191
|
+
calloutContainerTag: "div",
|
|
192
|
+
calloutClass: "callout",
|
|
193
|
+
calloutTitleClass: "callout-title",
|
|
194
|
+
calloutBodyClass: "callout-body",
|
|
195
|
+
enhanceBlockquotes: true,
|
|
196
|
+
blockquoteClass: "blockquote-enhanced",
|
|
197
|
+
dataCalloutType: true,
|
|
198
|
+
customCallouts: [],
|
|
199
|
+
calloutPattern: undefined,
|
|
200
|
+
enableDisclosures: true,
|
|
201
|
+
disclosureClass: "disclosure",
|
|
202
|
+
disclosureTitleClass: "disclosure-title",
|
|
203
|
+
disclosureBodyClass: "disclosure-body",
|
|
204
|
+
enableAccordion: true,
|
|
205
|
+
accordionClass: "disclosure-accordion",
|
|
206
|
+
enableTreeView: true,
|
|
207
|
+
allowDangerousHtml: false,
|
|
208
|
+
};
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Helper: Validate + normalize a single custom callout config.
|
|
211
|
+
// Missing fields are filled with safe defaults so the plugin doesn't crash
|
|
212
|
+
// when a user supplies an incomplete config.
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
function normalizeCalloutConfig(config) {
|
|
215
|
+
if (!config || typeof config !== "object")
|
|
216
|
+
return null;
|
|
217
|
+
const rawType = config.type;
|
|
218
|
+
if (typeof rawType !== "string" || rawType.trim() === "")
|
|
219
|
+
return null;
|
|
220
|
+
return {
|
|
221
|
+
type: rawType,
|
|
222
|
+
icon: typeof config.icon === "string" ? config.icon : "",
|
|
223
|
+
className: typeof config.className === "string" ? config.className : `callout-${rawType.toLowerCase()}`,
|
|
224
|
+
defaultTitle: typeof config.defaultTitle === "string" ? config.defaultTitle : rawType,
|
|
225
|
+
color: typeof config.color === "string" ? config.color : "#57606a",
|
|
226
|
+
backgroundColor: typeof config.backgroundColor === "string" ? config.backgroundColor : "#f6f8fa",
|
|
227
|
+
iconColor: typeof config.iconColor === "string" ? config.iconColor : undefined,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Helper: Escape a string for safe interpolation into an HTML attribute value
|
|
232
|
+
// (class, style, etc.). Uses the same escapes as escapeHtml, but additionally
|
|
233
|
+
// rejects characters that could break out of the attribute context.
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
function escapeAttribute(str) {
|
|
236
|
+
return escapeHtml(str);
|
|
237
|
+
}
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Helper: Validate a CSS color value. Accepts hex, rgb(), rgba(), hsl(),
|
|
240
|
+
// hsla(), named colors, var(--token), currentColor, and transparent.
|
|
241
|
+
// Rejects anything that could break out of style="color:..." context.
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
const SAFE_COLOR_RE = /^(#[0-9a-fA-F]{3,8}|rgb\(\s*[\d.%\s,/-]+\s*\)|rgba\(\s*[\d.%\s,/-]+\s*\)|hsl\(\s*[\d.%\s,/-]+\s*\)|hsla\(\s*[\d.%\s,/-]+\s*\)|var\(\s*--[\w-]+\s*(?:,\s*[^()]*)?\)|currentColor|transparent|inherit|[a-zA-Z]+)$/;
|
|
244
|
+
function sanitizeColor(value, fallback) {
|
|
245
|
+
if (typeof value !== "string" || value.length === 0)
|
|
246
|
+
return fallback;
|
|
247
|
+
// Strip whitespace then validate.
|
|
248
|
+
const trimmed = value.trim();
|
|
249
|
+
if (SAFE_COLOR_RE.test(trimmed))
|
|
250
|
+
return trimmed;
|
|
251
|
+
// Reject anything that contains quotes, angle brackets, or semicolons —
|
|
252
|
+
// these could break out of the style attribute.
|
|
253
|
+
if (/["'<>;]/.test(trimmed))
|
|
254
|
+
return fallback;
|
|
255
|
+
// Otherwise, return as-is (allows reasonable CSS color values that the
|
|
256
|
+
// regex didn't anticipate, but blocks obvious injection attempts).
|
|
257
|
+
return trimmed;
|
|
258
|
+
}
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Helper: Build the callout configuration map
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
function buildCalloutConfigMap(options) {
|
|
263
|
+
const map = new Map();
|
|
264
|
+
for (const config of BUILTIN_CALLOUTS) {
|
|
265
|
+
map.set(config.type.toLowerCase(), config);
|
|
266
|
+
}
|
|
267
|
+
if (options.customCallouts) {
|
|
268
|
+
for (const rawConfig of options.customCallouts) {
|
|
269
|
+
// Normalize + validate. Skip invalid entries instead of crashing.
|
|
270
|
+
const config = normalizeCalloutConfig(rawConfig);
|
|
271
|
+
if (config) {
|
|
272
|
+
map.set(config.type.toLowerCase(), config);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return map;
|
|
277
|
+
}
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Helper: Build the dynamic regex matching all directives
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
function buildCalloutPattern(configMap, enableDisclosures) {
|
|
282
|
+
const allDirectives = Array.from(configMap.keys())
|
|
283
|
+
.map((t) => t.toUpperCase())
|
|
284
|
+
.sort((a, b) => b.length - a.length);
|
|
285
|
+
if (enableDisclosures) {
|
|
286
|
+
allDirectives.unshift("");
|
|
287
|
+
}
|
|
288
|
+
const typePattern = allDirectives.join("|");
|
|
289
|
+
return new RegExp(`^\\[!(${typePattern})\\]([+-]?)(?:[^\\S\\n]+(.+))?`, "i");
|
|
290
|
+
}
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Helper: Parse the first paragraph of a blockquote to detect a callout
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
function parseCalloutDirective(blockquote, calloutPattern, configMap, enableDisclosures) {
|
|
295
|
+
const firstChild = blockquote.children[0];
|
|
296
|
+
if (!firstChild || firstChild.type !== "paragraph")
|
|
297
|
+
return null;
|
|
298
|
+
const firstText = extractTextContent(firstChild);
|
|
299
|
+
if (!firstText)
|
|
300
|
+
return null;
|
|
301
|
+
const match = firstText.match(calloutPattern);
|
|
302
|
+
if (!match)
|
|
303
|
+
return null;
|
|
304
|
+
const rawType = match[1] || "";
|
|
305
|
+
const isDisclosure = enableDisclosures && rawType === "";
|
|
306
|
+
if (!isDisclosure && !configMap.has(rawType.toLowerCase()))
|
|
307
|
+
return null;
|
|
308
|
+
const type = isDisclosure ? "disclosure" : rawType.toLowerCase();
|
|
309
|
+
const foldMarker = match[2] || "";
|
|
310
|
+
const collapsible = foldMarker === "+" || foldMarker === "-";
|
|
311
|
+
const collapsibleOpen = foldMarker === "+";
|
|
312
|
+
const effectiveCollapsible = isDisclosure ? true : collapsible;
|
|
313
|
+
const effectiveCollapsibleOpen = isDisclosure ? (foldMarker === "+" || !foldMarker) : collapsibleOpen;
|
|
314
|
+
const remainder = firstText.slice(match[0].length);
|
|
315
|
+
let customTitle;
|
|
316
|
+
let remainingContent;
|
|
317
|
+
if (match[3]) {
|
|
318
|
+
customTitle = match[3].trim() || undefined;
|
|
319
|
+
if (remainder.includes("\n")) {
|
|
320
|
+
const newlineIdx = remainder.indexOf("\n");
|
|
321
|
+
remainingContent = remainder.slice(newlineIdx + 1).trim() || undefined;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
const hasNewlineAfterDirective = /^\s*\n/.test(remainder);
|
|
326
|
+
if (hasNewlineAfterDirective) {
|
|
327
|
+
const afterNewline = remainder.replace(/^\s*\n/, "");
|
|
328
|
+
remainingContent = afterNewline.trim() || undefined;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { type, customTitle, remainingContent, collapsible: effectiveCollapsible, collapsibleOpen: effectiveCollapsibleOpen, isDisclosure };
|
|
332
|
+
}
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Helpers: extractTextContent, html, escapeHtml
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
function extractTextContent(node) {
|
|
337
|
+
let text = "";
|
|
338
|
+
for (const child of node.children) {
|
|
339
|
+
if (child.type === "text") {
|
|
340
|
+
text += child.value;
|
|
341
|
+
}
|
|
342
|
+
else if ("children" in child && Array.isArray(child.children)) {
|
|
343
|
+
text += extractTextContent(child);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return text;
|
|
347
|
+
}
|
|
348
|
+
function html(value) {
|
|
349
|
+
return { type: "html", value };
|
|
350
|
+
}
|
|
351
|
+
function escapeHtml(str) {
|
|
352
|
+
return str
|
|
353
|
+
.replace(/&/g, "&")
|
|
354
|
+
.replace(/</g, "<")
|
|
355
|
+
.replace(/>/g, ">")
|
|
356
|
+
.replace(/"/g, """)
|
|
357
|
+
.replace(/'/g, "'");
|
|
358
|
+
}
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
// Helper: Build body HTML from blockquote children
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
function buildBodyHtml(blockquote, calloutPattern, options) {
|
|
363
|
+
let bodyHtml = "";
|
|
364
|
+
const bodyChildren = blockquote.children.slice(1);
|
|
365
|
+
for (const child of bodyChildren) {
|
|
366
|
+
bodyHtml += serializeNodeToHtml(child, "", options.allowDangerousHtml);
|
|
367
|
+
}
|
|
368
|
+
const firstPara = blockquote.children[0];
|
|
369
|
+
if (firstPara && firstPara.type === "paragraph") {
|
|
370
|
+
const firstParaHtml = serializeFirstParagraphAfterDirective(firstPara, calloutPattern, options.allowDangerousHtml);
|
|
371
|
+
if (firstParaHtml) {
|
|
372
|
+
bodyHtml = firstParaHtml + bodyHtml;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return bodyHtml;
|
|
376
|
+
}
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Helper: Build the callout HTML
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
function buildCalloutHtml(parsed, blockquote, calloutPattern, configMap, options, depth = 0) {
|
|
381
|
+
// ── Disclosure Widget ──────────────────────────────────────────────
|
|
382
|
+
if (parsed.isDisclosure) {
|
|
383
|
+
return buildDisclosureHtml(parsed, blockquote, calloutPattern, options, depth);
|
|
384
|
+
}
|
|
385
|
+
// ── Regular / Collapsible Callout ──────────────────────────────────
|
|
386
|
+
const config = configMap.get(parsed.type);
|
|
387
|
+
const title = parsed.customTitle || config.defaultTitle;
|
|
388
|
+
const dataAttr = options.dataCalloutType ? ` data-callout-type="${escapeAttribute(parsed.type)}"` : "";
|
|
389
|
+
const bodyHtml = buildBodyHtml(blockquote, calloutPattern, options);
|
|
390
|
+
// Sanitize colors before interpolating into style attribute
|
|
391
|
+
const safeIconColor = sanitizeColor(config.iconColor || config.color, "#57606a");
|
|
392
|
+
const ariaRole = getAriaRole(parsed.type);
|
|
393
|
+
// Escape className + calloutClass to prevent attribute breakout
|
|
394
|
+
const safeCalloutClass = escapeAttribute(options.calloutClass);
|
|
395
|
+
const safeConfigClassName = escapeAttribute(config.className);
|
|
396
|
+
const safeCalloutTitleClass = escapeAttribute(options.calloutTitleClass);
|
|
397
|
+
const safeCalloutBodyClass = escapeAttribute(options.calloutBodyClass);
|
|
398
|
+
if (parsed.collapsible) {
|
|
399
|
+
const openAttr = parsed.collapsibleOpen ? " open" : "";
|
|
400
|
+
return html([
|
|
401
|
+
`<details class="${safeCalloutClass} ${safeConfigClassName} collapsible"${dataAttr} role="${ariaRole}"${openAttr}>`,
|
|
402
|
+
` <summary class="${safeCalloutTitleClass}" style="color:${safeIconColor}">`,
|
|
403
|
+
` <span class="callout-icon" style="color:${safeIconColor}">${config.icon}</span>`,
|
|
404
|
+
` <span class="callout-title-text">${escapeHtml(title)}</span>`,
|
|
405
|
+
` </summary>`,
|
|
406
|
+
` <div class="${safeCalloutBodyClass}">`,
|
|
407
|
+
bodyHtml,
|
|
408
|
+
` </div>`,
|
|
409
|
+
`</details>`,
|
|
410
|
+
].join("\n"));
|
|
411
|
+
}
|
|
412
|
+
return html([
|
|
413
|
+
`<aside class="${safeCalloutClass} ${safeConfigClassName}"${dataAttr} role="${ariaRole}">`,
|
|
414
|
+
` <div class="${safeCalloutTitleClass}" style="color:${safeIconColor}">`,
|
|
415
|
+
` <span class="callout-icon" style="color:${safeIconColor}">${config.icon}</span>`,
|
|
416
|
+
` <span class="callout-title-text">${escapeHtml(title)}</span>`,
|
|
417
|
+
` </div>`,
|
|
418
|
+
` <div class="${safeCalloutBodyClass}">`,
|
|
419
|
+
bodyHtml,
|
|
420
|
+
` </div>`,
|
|
421
|
+
`</aside>`,
|
|
422
|
+
].join("\n"));
|
|
423
|
+
}
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// Helper: Build disclosure widget HTML (with tree view support)
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
function buildDisclosureHtml(parsed, blockquote, calloutPattern, options, depth = 0) {
|
|
428
|
+
const title = parsed.customTitle || "Details";
|
|
429
|
+
const openAttr = parsed.collapsibleOpen ? " open" : "";
|
|
430
|
+
const bodyHtml = buildBodyHtml(blockquote, calloutPattern, options);
|
|
431
|
+
// Tree view: add depth class for nested disclosures
|
|
432
|
+
const safeDisclosureClass = escapeAttribute(options.disclosureClass);
|
|
433
|
+
const safeDisclosureTitleClass = escapeAttribute(options.disclosureTitleClass);
|
|
434
|
+
const safeDisclosureBodyClass = escapeAttribute(options.disclosureBodyClass);
|
|
435
|
+
const safeDepth = String(depth).replace(/[^0-9]/g, "") || "0";
|
|
436
|
+
const treeAttr = options.enableTreeView && depth > 0
|
|
437
|
+
? ` class="${safeDisclosureClass} disclosure-tree" data-depth="${safeDepth}"`
|
|
438
|
+
: ` class="${safeDisclosureClass}"`;
|
|
439
|
+
const disclosureHtml = [
|
|
440
|
+
`<details${treeAttr}${openAttr}>`,
|
|
441
|
+
` <summary class="${safeDisclosureTitleClass}">${escapeHtml(title)}</summary>`,
|
|
442
|
+
` <div class="${safeDisclosureBodyClass}">`,
|
|
443
|
+
bodyHtml,
|
|
444
|
+
` </div>`,
|
|
445
|
+
`</details>`,
|
|
446
|
+
].join("\n");
|
|
447
|
+
return html(disclosureHtml);
|
|
448
|
+
}
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
// Helper: Map callout type to ARIA role
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
function getAriaRole(type) {
|
|
453
|
+
switch (type) {
|
|
454
|
+
case "warning":
|
|
455
|
+
case "attention":
|
|
456
|
+
case "caution":
|
|
457
|
+
case "danger":
|
|
458
|
+
case "error":
|
|
459
|
+
case "fail":
|
|
460
|
+
case "failure":
|
|
461
|
+
case "missing":
|
|
462
|
+
return "alert";
|
|
463
|
+
default:
|
|
464
|
+
return "note";
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// Helper: Serialize the first paragraph's content after the directive
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
function serializeFirstParagraphAfterDirective(paragraph, calloutPattern, allowDangerousHtml) {
|
|
471
|
+
const children = paragraph.children;
|
|
472
|
+
let foundDirective = false;
|
|
473
|
+
const remainingChildren = [];
|
|
474
|
+
for (let i = 0; i < children.length; i++) {
|
|
475
|
+
const child = children[i];
|
|
476
|
+
if (!foundDirective && child.type === "text") {
|
|
477
|
+
const textVal = child.value;
|
|
478
|
+
const match = textVal.match(calloutPattern);
|
|
479
|
+
if (match) {
|
|
480
|
+
foundDirective = true;
|
|
481
|
+
const afterDirective = textVal.slice(match[0].length);
|
|
482
|
+
if (afterDirective.trim()) {
|
|
483
|
+
const newlineIdx = afterDirective.indexOf("\n");
|
|
484
|
+
if (newlineIdx !== -1) {
|
|
485
|
+
const bodyText = afterDirective.slice(newlineIdx + 1).trim();
|
|
486
|
+
if (bodyText)
|
|
487
|
+
remainingChildren.push({ type: "text", value: bodyText });
|
|
488
|
+
}
|
|
489
|
+
// If there is no newline, the text after the directive on the
|
|
490
|
+
// same line is treated as the custom title (already extracted by
|
|
491
|
+
// parseCalloutDirective) and is intentionally NOT added to the
|
|
492
|
+
// body — this prevents the title from leaking into the body
|
|
493
|
+
// when it contains raw HTML.
|
|
494
|
+
}
|
|
495
|
+
for (let j = i + 1; j < children.length; j++) {
|
|
496
|
+
remainingChildren.push(children[j]);
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
remainingChildren.push(child);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else if (foundDirective) {
|
|
505
|
+
remainingChildren.push(child);
|
|
506
|
+
}
|
|
507
|
+
// If we haven't found the directive yet and the child is not a text node,
|
|
508
|
+
// skip it — but this typically doesn't happen because the directive is
|
|
509
|
+
// always in the first text node.
|
|
510
|
+
}
|
|
511
|
+
if (remainingChildren.length === 0)
|
|
512
|
+
return "";
|
|
513
|
+
const innerHtml = remainingChildren.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("");
|
|
514
|
+
return innerHtml.trim() ? `<p>${innerHtml}</p>\n` : "";
|
|
515
|
+
}
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
// Helpers: Serialize mdast nodes to HTML
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
function serializeNodeToHtml(node, indent = "", allowDangerousHtml = false) {
|
|
520
|
+
switch (node.type) {
|
|
521
|
+
case "paragraph": return `${indent}<p>${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</p>\n`;
|
|
522
|
+
case "heading": return `${indent}<h${node.depth}>${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</h${node.depth}>\n`;
|
|
523
|
+
case "list": {
|
|
524
|
+
const tag = node.ordered ? "ol" : "ul";
|
|
525
|
+
return `${indent}<${tag}>\n${node.children.map((c) => serializeNodeToHtml(c, indent + " ", allowDangerousHtml)).join("")}${indent}</${tag}>\n`;
|
|
526
|
+
}
|
|
527
|
+
case "listItem": return `${indent}<li>\n${node.children.map((c) => serializeNodeToHtml(c, indent + " ", allowDangerousHtml)).join("")}${indent}</li>\n`;
|
|
528
|
+
case "code": {
|
|
529
|
+
const lang = node.lang ? ` class="language-${escapeAttribute(node.lang)}"` : "";
|
|
530
|
+
return `${indent}<pre><code${lang}>${escapeHtml(node.value)}</code></pre>\n`;
|
|
531
|
+
}
|
|
532
|
+
case "blockquote": return `${indent}<blockquote>\n${node.children.map((c) => serializeNodeToHtml(c, indent + " ", allowDangerousHtml)).join("")}${indent}</blockquote>\n`;
|
|
533
|
+
case "thematicBreak": return `${indent}<hr />\n`;
|
|
534
|
+
case "html": return allowDangerousHtml ? `${indent}${node.value}\n` : `${indent}${escapeHtml(node.value)}\n`;
|
|
535
|
+
default: return "";
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function inlineNodeToHtml(node, allowDangerousHtml = false) {
|
|
539
|
+
switch (node.type) {
|
|
540
|
+
case "text": return escapeHtml(node.value);
|
|
541
|
+
case "strong": return `<strong>${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</strong>`;
|
|
542
|
+
case "emphasis": return `<em>${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</em>`;
|
|
543
|
+
case "inlineCode": return `<code>${escapeHtml(node.value)}</code>`;
|
|
544
|
+
case "link": return `<a href="${escapeHtml(node.url)}">${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</a>`;
|
|
545
|
+
case "image": return `<img src="${escapeHtml(node.url)}" alt="${escapeHtml(node.alt || "")}" />`;
|
|
546
|
+
case "delete": return `<del>${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</del>`;
|
|
547
|
+
// SAFE-BY-DEFAULT: raw inline HTML is escaped unless the user explicitly
|
|
548
|
+
// opted in with `allowDangerousHtml: true`. This prevents XSS from
|
|
549
|
+
// untrusted markdown containing payloads like <img src=x onerror=alert(1)>.
|
|
550
|
+
case "html": return allowDangerousHtml ? node.value : escapeHtml(node.value);
|
|
551
|
+
default:
|
|
552
|
+
if (node.children)
|
|
553
|
+
return node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("");
|
|
554
|
+
return "";
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
// Helper: Compute blockquote nesting depth for tree view
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
function getBlockquoteDepth(node, parent, tree) {
|
|
561
|
+
let depth = 0;
|
|
562
|
+
let current = parent;
|
|
563
|
+
while (current && current !== tree) {
|
|
564
|
+
if (current.type === "blockquote")
|
|
565
|
+
depth++;
|
|
566
|
+
// Walk up — this is a simplified approach; for deeply nested trees
|
|
567
|
+
// we rely on the visit order which processes outer blocks first
|
|
568
|
+
break; // We use a different approach: pass depth during visit
|
|
569
|
+
}
|
|
570
|
+
return depth;
|
|
571
|
+
}
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
// Plugin implementation
|
|
574
|
+
// ---------------------------------------------------------------------------
|
|
575
|
+
export const remarkRemakeBlocks = (userOptions) => {
|
|
576
|
+
const options = {
|
|
577
|
+
...DEFAULT_OPTIONS,
|
|
578
|
+
...userOptions,
|
|
579
|
+
};
|
|
580
|
+
const configMap = buildCalloutConfigMap(options);
|
|
581
|
+
const calloutPattern = options.calloutPattern || buildCalloutPattern(configMap, options.enableDisclosures);
|
|
582
|
+
return (tree) => {
|
|
583
|
+
// ── Pass 1: Transform blockquotes → callouts / disclosures ──────
|
|
584
|
+
// We must process DEEPEST blockquotes first (inside-out) so that
|
|
585
|
+
// nested [!] directives are converted before their parents read them.
|
|
586
|
+
// We collect all blockquote nodes first, then process depth-first.
|
|
587
|
+
const blockquotes = [];
|
|
588
|
+
function collectBlockquotes(node, depth) {
|
|
589
|
+
if (!node.children)
|
|
590
|
+
return;
|
|
591
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
592
|
+
const child = node.children[i];
|
|
593
|
+
if (child.type === "blockquote") {
|
|
594
|
+
blockquotes.push({ node: child, index: i, parent: node, depth });
|
|
595
|
+
// Recurse into the blockquote's children to find deeper ones
|
|
596
|
+
collectBlockquotes(child, depth + 1);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
collectBlockquotes(child, depth);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
collectBlockquotes(tree, 0);
|
|
604
|
+
// Sort by depth descending — deepest first
|
|
605
|
+
blockquotes.sort((a, b) => b.depth - a.depth);
|
|
606
|
+
// Process each blockquote (deepest first)
|
|
607
|
+
for (const { node: bq, index, parent, depth } of blockquotes) {
|
|
608
|
+
// Verify the node is still at the expected position
|
|
609
|
+
// (it may have been replaced by a different node if a sibling was processed)
|
|
610
|
+
if (!parent || !parent.children)
|
|
611
|
+
continue;
|
|
612
|
+
if (parent.children[index] !== bq) {
|
|
613
|
+
// The node may have shifted due to earlier replacements in the same parent
|
|
614
|
+
// Try to find it by reference
|
|
615
|
+
const foundIdx = parent.children.indexOf(bq);
|
|
616
|
+
if (foundIdx === -1)
|
|
617
|
+
continue; // Already replaced
|
|
618
|
+
}
|
|
619
|
+
const parsed = parseCalloutDirective(bq, calloutPattern, configMap, options.enableDisclosures);
|
|
620
|
+
if (parsed) {
|
|
621
|
+
// For disclosures: depth > 0 means it's nested inside another blockquote
|
|
622
|
+
// (which is likely another disclosure or callout) → tree view
|
|
623
|
+
const disclosureDepth = parsed.isDisclosure ? depth : 0;
|
|
624
|
+
const calloutNode = buildCalloutHtml(parsed, bq, calloutPattern, configMap, options, disclosureDepth);
|
|
625
|
+
// Replace using the actual current index
|
|
626
|
+
const actualIdx = parent.children.indexOf(bq);
|
|
627
|
+
if (actualIdx !== -1) {
|
|
628
|
+
parent.children[actualIdx] = calloutNode;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
else if (options.enhanceBlockquotes) {
|
|
632
|
+
const actualIdx = parent.children.indexOf(bq);
|
|
633
|
+
if (actualIdx !== -1) {
|
|
634
|
+
parent.children[actualIdx] = html([
|
|
635
|
+
`<div class="${escapeAttribute(options.blockquoteClass)}">`,
|
|
636
|
+
` <blockquote>`,
|
|
637
|
+
bq.children.map((c) => serializeNodeToHtml(c, " ", options.allowDangerousHtml)).join(""),
|
|
638
|
+
` </blockquote>`,
|
|
639
|
+
`</div>`,
|
|
640
|
+
].join("\n"));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// ── Pass 2: Group consecutive disclosures into accordions ───────
|
|
645
|
+
if (options.enableAccordion && options.enableDisclosures) {
|
|
646
|
+
groupAccordions(tree, options.accordionClass);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
};
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
// Pass 2: Group consecutive disclosure <details> into accordion wrapper
|
|
652
|
+
// ---------------------------------------------------------------------------
|
|
653
|
+
function groupAccordions(tree, accordionClass) {
|
|
654
|
+
function walk(node) {
|
|
655
|
+
if (!node.children)
|
|
656
|
+
return;
|
|
657
|
+
// Process children array
|
|
658
|
+
const newChildren = [];
|
|
659
|
+
let i = 0;
|
|
660
|
+
while (i < node.children.length) {
|
|
661
|
+
const child = node.children[i];
|
|
662
|
+
// Check if this is a disclosure <details> HTML node
|
|
663
|
+
if (child.type === "html" && isDisclosureHtml(child.value)) {
|
|
664
|
+
// Start collecting consecutive disclosures
|
|
665
|
+
const group = [child.value];
|
|
666
|
+
let j = i + 1;
|
|
667
|
+
// Collect consecutive disclosure nodes (skip whitespace-only text nodes)
|
|
668
|
+
while (j < node.children.length) {
|
|
669
|
+
const next = node.children[j];
|
|
670
|
+
if (next.type === "html" && isDisclosureHtml(next.value)) {
|
|
671
|
+
group.push(next.value);
|
|
672
|
+
j++;
|
|
673
|
+
}
|
|
674
|
+
else if (next.type === "text" && next.value.trim() === "") {
|
|
675
|
+
// Skip whitespace text nodes between disclosures
|
|
676
|
+
j++;
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
// If we found 2+ consecutive disclosures, wrap in accordion
|
|
683
|
+
if (group.length >= 2) {
|
|
684
|
+
const accordionHtml = [
|
|
685
|
+
`<div class="${escapeAttribute(accordionClass)}" data-accordion>`,
|
|
686
|
+
...group,
|
|
687
|
+
`</div>`,
|
|
688
|
+
].join("\n");
|
|
689
|
+
newChildren.push(html(accordionHtml));
|
|
690
|
+
i = j;
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
newChildren.push(child);
|
|
694
|
+
i++;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
newChildren.push(child);
|
|
699
|
+
i++;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
node.children = newChildren;
|
|
703
|
+
// Recurse into remaining children
|
|
704
|
+
for (const child of node.children) {
|
|
705
|
+
walk(child);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
walk(tree);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Check if an HTML string is a disclosure <details> element.
|
|
712
|
+
* Matches <details class="disclosure"...> but NOT callout <details class="callout ...">
|
|
713
|
+
*/
|
|
714
|
+
function isDisclosureHtml(htmlStr) {
|
|
715
|
+
const trimmed = htmlStr.trim();
|
|
716
|
+
return trimmed.startsWith('<details class="disclosure"') &&
|
|
717
|
+
!trimmed.includes('callout');
|
|
718
|
+
}
|
|
719
|
+
// ---------------------------------------------------------------------------
|
|
720
|
+
// Backward-compatible alias
|
|
721
|
+
// ---------------------------------------------------------------------------
|
|
722
|
+
export const remarkCalloutBlocks = remarkRemakeBlocks;
|
|
723
|
+
export { BUILTIN_CALLOUTS };
|
|
724
|
+
//# sourceMappingURL=remark-remake-blocks.js.map
|