@dr-ishaan/remake-blocks 1.0.0 → 1.2.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.
@@ -0,0 +1,958 @@
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
+ // v1.2.0 defaults
209
+ aliases: {},
210
+ showIndicator: true,
211
+ iconSet: "octicon",
212
+ appearance: "default",
213
+ props: {},
214
+ build: undefined,
215
+ tags: {},
216
+ };
217
+ // ---------------------------------------------------------------------------
218
+ // Helper: Validate + normalize a single custom callout config.
219
+ // Missing fields are filled with safe defaults so the plugin doesn't crash
220
+ // when a user supplies an incomplete config.
221
+ // ---------------------------------------------------------------------------
222
+ function normalizeCalloutConfig(config) {
223
+ if (!config || typeof config !== "object")
224
+ return null;
225
+ const rawType = config.type;
226
+ if (typeof rawType !== "string" || rawType.trim() === "")
227
+ return null;
228
+ return {
229
+ type: rawType,
230
+ icon: typeof config.icon === "string" ? config.icon : "",
231
+ className: typeof config.className === "string" ? config.className : `callout-${rawType.toLowerCase()}`,
232
+ defaultTitle: typeof config.defaultTitle === "string" ? config.defaultTitle : rawType,
233
+ color: typeof config.color === "string" ? config.color : "#57606a",
234
+ backgroundColor: typeof config.backgroundColor === "string" ? config.backgroundColor : "#f6f8fa",
235
+ iconColor: typeof config.iconColor === "string" ? config.iconColor : undefined,
236
+ };
237
+ }
238
+ // ---------------------------------------------------------------------------
239
+ // Helper: Escape a string for safe interpolation into an HTML attribute value
240
+ // (class, style, etc.). Uses the same escapes as escapeHtml, but additionally
241
+ // rejects characters that could break out of the attribute context.
242
+ // ---------------------------------------------------------------------------
243
+ function escapeAttribute(str) {
244
+ return escapeHtml(str);
245
+ }
246
+ // ---------------------------------------------------------------------------
247
+ // Helper: Validate a CSS color value. Accepts hex, rgb(), rgba(), hsl(),
248
+ // hsla(), named colors, var(--token), currentColor, and transparent.
249
+ // Rejects anything that could break out of style="color:..." context.
250
+ // ---------------------------------------------------------------------------
251
+ 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]+)$/;
252
+ function sanitizeColor(value, fallback) {
253
+ if (typeof value !== "string" || value.length === 0)
254
+ return fallback;
255
+ // Strip whitespace then validate.
256
+ const trimmed = value.trim();
257
+ if (SAFE_COLOR_RE.test(trimmed))
258
+ return trimmed;
259
+ // Reject anything that contains quotes, angle brackets, or semicolons —
260
+ // these could break out of the style attribute.
261
+ if (/["'<>;]/.test(trimmed))
262
+ return fallback;
263
+ // Otherwise, return as-is (allows reasonable CSS color values that the
264
+ // regex didn't anticipate, but blocks obvious injection attempts).
265
+ return trimmed;
266
+ }
267
+ // ---------------------------------------------------------------------------
268
+ // Helper: Build the callout configuration map (with alias resolution)
269
+ // ---------------------------------------------------------------------------
270
+ function buildCalloutConfigMap(options) {
271
+ const map = new Map();
272
+ for (const config of BUILTIN_CALLOUTS) {
273
+ map.set(config.type.toLowerCase(), config);
274
+ }
275
+ if (options.customCallouts) {
276
+ for (const rawConfig of options.customCallouts) {
277
+ // Normalize + validate. Skip invalid entries instead of crashing.
278
+ const config = normalizeCalloutConfig(rawConfig);
279
+ if (config) {
280
+ map.set(config.type.toLowerCase(), config);
281
+ }
282
+ }
283
+ }
284
+ // Register aliases: each alias points to its canonical config.
285
+ // Aliases are case-insensitive (lowercased on registration AND lookup).
286
+ if (options.aliases) {
287
+ for (const [canonical, aliasList] of Object.entries(options.aliases)) {
288
+ const canonicalLower = canonical.toLowerCase();
289
+ const canonicalConfig = map.get(canonicalLower);
290
+ if (!canonicalConfig || !Array.isArray(aliasList))
291
+ continue;
292
+ for (const alias of aliasList) {
293
+ if (typeof alias !== "string" || alias.trim() === "")
294
+ continue;
295
+ map.set(alias.toLowerCase(), canonicalConfig);
296
+ }
297
+ }
298
+ }
299
+ return map;
300
+ }
301
+ // ---------------------------------------------------------------------------
302
+ // Helper: Build the dynamic regex matching all directives.
303
+ // Captures: (1) type (or empty for disclosure), (2) fold marker + or -,
304
+ // (3) optional inline title text (everything until end-of-line or `{`),
305
+ // (4) optional `{key=value key=value}` overrides block.
306
+ // ---------------------------------------------------------------------------
307
+ function buildCalloutPattern(configMap, enableDisclosures) {
308
+ const allDirectives = Array.from(configMap.keys())
309
+ .map((t) => t.toUpperCase())
310
+ .sort((a, b) => b.length - a.length);
311
+ if (enableDisclosures) {
312
+ allDirectives.unshift("");
313
+ }
314
+ const typePattern = allDirectives.join("|");
315
+ // Group 3 (title) is non-greedy and stops at `{` (start of overrides) or end of line.
316
+ // Group 4 (overrides) captures `{...}` if present.
317
+ return new RegExp(`^\\[!(${typePattern})\\]([+-]?)(?:[^\\S\\n]+([^\\n{]*))?(\\s*\\{[^\\n]*\\})?`, "i");
318
+ }
319
+ // ---------------------------------------------------------------------------
320
+ // Helper: Parse `{key=value key=value}` overrides block into an object.
321
+ // Supported keys (v1.2.0):
322
+ // - icon: true|false — override showIndicator per-callout
323
+ // - appearance: default|minimal|simple|hidden — override appearance per-callout
324
+ // Unknown keys are silently ignored.
325
+ // ---------------------------------------------------------------------------
326
+ function parseOverrides(overridesBlock) {
327
+ if (!overridesBlock)
328
+ return undefined;
329
+ const inner = overridesBlock.trim().replace(/^\{|\}$/g, "").trim();
330
+ if (!inner)
331
+ return undefined;
332
+ const result = {};
333
+ // Match key=value pairs. Values can be unquoted barewords or "quoted strings".
334
+ const pairRe = /(\w[\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s}]+))/g;
335
+ let m;
336
+ while ((m = pairRe.exec(inner)) !== null) {
337
+ const key = m[1].toLowerCase();
338
+ const value = (m[2] ?? m[3] ?? m[4] ?? "").trim();
339
+ if (key === "icon") {
340
+ if (value === "false" || value === "0" || value === "no")
341
+ result.icon = false;
342
+ else if (value === "true" || value === "1" || value === "yes")
343
+ result.icon = true;
344
+ }
345
+ else if (key === "appearance") {
346
+ if (value === "default" || value === "minimal" || value === "simple" || value === "hidden") {
347
+ result.appearance = value;
348
+ }
349
+ }
350
+ // Unknown keys silently ignored
351
+ }
352
+ if (result.icon === undefined && result.appearance === undefined)
353
+ return undefined;
354
+ return result;
355
+ }
356
+ // ---------------------------------------------------------------------------
357
+ // Helper: Parse the first paragraph of a blockquote to detect a callout
358
+ // ---------------------------------------------------------------------------
359
+ function parseCalloutDirective(blockquote, calloutPattern, configMap, enableDisclosures) {
360
+ const firstChild = blockquote.children[0];
361
+ if (!firstChild || firstChild.type !== "paragraph")
362
+ return null;
363
+ const firstText = extractTextContent(firstChild);
364
+ if (!firstText)
365
+ return null;
366
+ const match = firstText.match(calloutPattern);
367
+ if (!match)
368
+ return null;
369
+ const rawType = match[1] || "";
370
+ const isDisclosure = enableDisclosures && rawType === "";
371
+ if (!isDisclosure && !configMap.has(rawType.toLowerCase()))
372
+ return null;
373
+ const type = isDisclosure ? "disclosure" : rawType.toLowerCase();
374
+ const foldMarker = match[2] || "";
375
+ const collapsible = foldMarker === "+" || foldMarker === "-";
376
+ const collapsibleOpen = foldMarker === "+";
377
+ const effectiveCollapsible = isDisclosure ? true : collapsible;
378
+ const effectiveCollapsibleOpen = isDisclosure ? (foldMarker === "+" || !foldMarker) : collapsibleOpen;
379
+ const remainder = firstText.slice(match[0].length);
380
+ let customTitle;
381
+ let remainingContent;
382
+ if (match[3]) {
383
+ customTitle = match[3].trim() || undefined;
384
+ if (remainder.includes("\n")) {
385
+ const newlineIdx = remainder.indexOf("\n");
386
+ remainingContent = remainder.slice(newlineIdx + 1).trim() || undefined;
387
+ }
388
+ }
389
+ else {
390
+ const hasNewlineAfterDirective = /^\s*\n/.test(remainder);
391
+ if (hasNewlineAfterDirective) {
392
+ const afterNewline = remainder.replace(/^\s*\n/, "");
393
+ remainingContent = afterNewline.trim() || undefined;
394
+ }
395
+ }
396
+ // v1.2.0: parse per-callout overrides from `{key=value}` block (match[4])
397
+ const overrides = parseOverrides(match[4]);
398
+ return { type, customTitle, remainingContent, collapsible: effectiveCollapsible, collapsibleOpen: effectiveCollapsibleOpen, isDisclosure, overrides };
399
+ }
400
+ // ---------------------------------------------------------------------------
401
+ // Helpers: extractTextContent, html, escapeHtml
402
+ // ---------------------------------------------------------------------------
403
+ function extractTextContent(node) {
404
+ let text = "";
405
+ for (const child of node.children) {
406
+ if (child.type === "text") {
407
+ text += child.value;
408
+ }
409
+ else if ("children" in child && Array.isArray(child.children)) {
410
+ text += extractTextContent(child);
411
+ }
412
+ }
413
+ return text;
414
+ }
415
+ function html(value) {
416
+ return { type: "html", value };
417
+ }
418
+ function escapeHtml(str) {
419
+ return str
420
+ .replace(/&/g, "&amp;")
421
+ .replace(/</g, "&lt;")
422
+ .replace(/>/g, "&gt;")
423
+ .replace(/"/g, "&quot;")
424
+ .replace(/'/g, "&#039;");
425
+ }
426
+ // ---------------------------------------------------------------------------
427
+ // Helper: Build body HTML from blockquote children
428
+ // ---------------------------------------------------------------------------
429
+ function buildBodyHtml(blockquote, calloutPattern, options) {
430
+ let bodyHtml = "";
431
+ const bodyChildren = blockquote.children.slice(1);
432
+ for (const child of bodyChildren) {
433
+ bodyHtml += serializeNodeToHtml(child, "", options.allowDangerousHtml);
434
+ }
435
+ const firstPara = blockquote.children[0];
436
+ if (firstPara && firstPara.type === "paragraph") {
437
+ const firstParaHtml = serializeFirstParagraphAfterDirective(firstPara, calloutPattern, options.allowDangerousHtml);
438
+ if (firstParaHtml) {
439
+ bodyHtml = firstParaHtml + bodyHtml;
440
+ }
441
+ }
442
+ return bodyHtml;
443
+ }
444
+ // ---------------------------------------------------------------------------
445
+ // Helper: Build the callout HTML
446
+ // ---------------------------------------------------------------------------
447
+ // ---------------------------------------------------------------------------
448
+ // v1.2.0 helpers: stable id generation, props formatting, override resolution
449
+ // ---------------------------------------------------------------------------
450
+ // Per-tree counter for callout ids. Reset at the start of each plugin run
451
+ // (each `transform(tree)` invocation) so the same markdown always produces
452
+ // the same output HTML (idempotency / determinism for snapshot tests).
453
+ let __calloutIdCounter = 0;
454
+ function resetCalloutIdCounter() {
455
+ __calloutIdCounter = 0;
456
+ }
457
+ function generateCalloutId(type) {
458
+ __calloutIdCounter += 1;
459
+ // Sanitize type for use in an HTML id (letters/digits/hyphen only).
460
+ const safeType = String(type).replace(/[^a-zA-Z0-9-]/g, "-") || "callout";
461
+ return `callout-${safeType}-${__calloutIdCounter.toString(36)}`;
462
+ }
463
+ function formatPropsAsAttrs(props) {
464
+ if (!props)
465
+ return "";
466
+ let out = "";
467
+ for (const [key, value] of Object.entries(props)) {
468
+ if (value === undefined || value === null)
469
+ continue;
470
+ // Sanitize the key: allow letters, digits, hyphens, colons (for namespaces like xml:lang)
471
+ const safeKey = key.replace(/[^a-zA-Z0-9:_-]/g, "");
472
+ if (!safeKey)
473
+ continue;
474
+ // Don't allow `style` or event handlers to be set via props (defense in depth)
475
+ if (safeKey.toLowerCase() === "style")
476
+ continue;
477
+ if (safeKey.toLowerCase().startsWith("on"))
478
+ continue;
479
+ out += ` ${safeKey}="${escapeAttribute(String(value))}"`;
480
+ }
481
+ return out;
482
+ }
483
+ function resolveProps(propsConfig, type, parsed) {
484
+ if (!propsConfig)
485
+ return {};
486
+ const entry = propsConfig[type] ?? propsConfig[type.toLowerCase()];
487
+ if (!entry)
488
+ return {};
489
+ if (typeof entry === "function") {
490
+ try {
491
+ return entry(parsed) || {};
492
+ }
493
+ catch {
494
+ return {};
495
+ }
496
+ }
497
+ if (typeof entry === "object")
498
+ return entry;
499
+ return {};
500
+ }
501
+ // ---------------------------------------------------------------------------
502
+ // Helper: Resolve effective icon visibility + appearance, merging global
503
+ // options with per-callout overrides.
504
+ // ---------------------------------------------------------------------------
505
+ function resolveVisuals(parsed, options) {
506
+ let showIcon = options.showIndicator !== false; // default true
507
+ let appearance = options.appearance ?? "default";
508
+ if (parsed.overrides) {
509
+ if (parsed.overrides.icon === false)
510
+ showIcon = false;
511
+ else if (parsed.overrides.icon === true)
512
+ showIcon = true;
513
+ if (parsed.overrides.appearance)
514
+ appearance = parsed.overrides.appearance;
515
+ }
516
+ // "hidden" appearance implies no icon
517
+ if (appearance === "hidden")
518
+ showIcon = false;
519
+ return { showIcon, appearance };
520
+ }
521
+ // ---------------------------------------------------------------------------
522
+ // Helper: Build the callout HTML (v1.2.0 — with all community-inspired features)
523
+ // ---------------------------------------------------------------------------
524
+ function buildCalloutHtml(parsed, blockquote, calloutPattern, configMap, options, depth = 0) {
525
+ // ── Disclosure Widget ──────────────────────────────────────────────
526
+ if (parsed.isDisclosure) {
527
+ return buildDisclosureHtml(parsed, blockquote, calloutPattern, options, depth);
528
+ }
529
+ // ── Regular / Collapsible Callout ──────────────────────────────────
530
+ const config = configMap.get(parsed.type);
531
+ const title = parsed.customTitle || config.defaultTitle;
532
+ const dataAttr = options.dataCalloutType ? ` data-callout-type="${escapeAttribute(parsed.type)}"` : "";
533
+ const bodyHtml = buildBodyHtml(blockquote, calloutPattern, options);
534
+ // v1.2.0: resolve per-callout visuals (icon visibility + appearance)
535
+ const { showIcon, appearance } = resolveVisuals(parsed, options);
536
+ // v1.2.0: build custom render escape hatch
537
+ if (typeof options.build === "function") {
538
+ try {
539
+ const customHtml = options.build(parsed, config, bodyHtml, options);
540
+ if (typeof customHtml === "string" && customHtml.length > 0) {
541
+ return html(customHtml);
542
+ }
543
+ }
544
+ catch {
545
+ // Fall through to default renderer on error
546
+ }
547
+ }
548
+ // Sanitize colors before interpolating into style attribute
549
+ const safeIconColor = sanitizeColor(config.iconColor || config.color, "#57606a");
550
+ // v1.2.0: WCAG fix — use role="note" for ALL callouts (was: role="alert" for warnings).
551
+ // role="alert" is for dynamically-inserted content and causes aggressive immediate
552
+ // announcement that disrupts screen reader users on static content.
553
+ // Use role="note" + aria-labelledby for proper title→container association.
554
+ const ariaRole = "note";
555
+ // Escape className + calloutClass to prevent attribute breakout
556
+ const safeCalloutClass = escapeAttribute(options.calloutClass);
557
+ const safeConfigClassName = escapeAttribute(config.className);
558
+ const safeCalloutTitleClass = escapeAttribute(options.calloutTitleClass);
559
+ const safeCalloutBodyClass = escapeAttribute(options.calloutBodyClass);
560
+ // v1.2.0: stable title id for aria-labelledby
561
+ const titleId = generateCalloutId(parsed.type);
562
+ // v1.2.0: resolve per-type props (dir, style, data-*, etc.)
563
+ const extraProps = resolveProps(options.props, parsed.type, parsed);
564
+ const extraAttrs = formatPropsAsAttrs(extraProps);
565
+ // v1.2.0: appearance class
566
+ const appearanceClass = appearance !== "default" ? ` callout-${appearance}` : "";
567
+ // v1.2.0: icon HTML — only render if showIcon AND appearance allows it
568
+ const renderIcon = showIcon && appearance !== "simple" && appearance !== "hidden";
569
+ const iconHtml = renderIcon
570
+ ? `<span class="callout-icon" style="color:${safeIconColor}" aria-hidden="true">${config.icon}</span>`
571
+ : "";
572
+ // v1.2.0: title-text always rendered (unless appearance="hidden")
573
+ const renderTitle = appearance !== "hidden";
574
+ const titleTextHtml = renderTitle
575
+ ? `<span class="callout-title-text">${escapeHtml(title)}</span>`
576
+ : "";
577
+ // v1.2.0: tags option (override element names)
578
+ const tags = options.tags ?? {};
579
+ const containerTag = tags.container || (parsed.collapsible ? "details" : "aside");
580
+ const titleTag = tags.title || (parsed.collapsible ? "summary" : "div");
581
+ const iconTag = tags.icon || "span";
582
+ const titleTextTag = tags.titleText || "span";
583
+ const bodyTag = tags.body || "div";
584
+ // Build icon span with custom tag if provided
585
+ const iconSpan = renderIcon
586
+ ? `<${iconTag} class="callout-icon" style="color:${safeIconColor}" aria-hidden="true">${config.icon}</${iconTag}>`
587
+ : "";
588
+ // Build the title block (or skip for "hidden" appearance)
589
+ const titleBlock = renderTitle
590
+ ? [
591
+ ` <${titleTag} class="${safeCalloutTitleClass}" style="color:${safeIconColor}" id="${titleId}">`,
592
+ iconSpan && ` ${iconSpan}`,
593
+ ` <${titleTextTag} class="callout-title-text">${escapeHtml(title)}</${titleTextTag}>`,
594
+ ` </${titleTag}>`,
595
+ ].filter(Boolean).join("\n")
596
+ : "";
597
+ // v1.2.0: aria-labelledby on container (only if title rendered)
598
+ const labelledby = renderTitle ? ` aria-labelledby="${titleId}"` : "";
599
+ // v1.2.0: dir="auto" on container + title for RTL/Unicode (community best practice)
600
+ const dirAuto = ` dir="auto"`;
601
+ const titleDirAuto = renderTitle ? ` dir="auto"` : "";
602
+ // Rebuild title block with dir="auto" (already in <title> opening tag below)
603
+ // For simplicity, we'll inline the title block here with all attributes:
604
+ const titleBlockWithDir = renderTitle
605
+ ? [
606
+ ` <${titleTag} class="${safeCalloutTitleClass}" style="color:${safeIconColor}" id="${titleId}"${titleDirAuto}>`,
607
+ iconSpan && ` ${iconSpan}`,
608
+ ` <${titleTextTag} class="callout-title-text">${escapeHtml(title)}</${titleTextTag}>`,
609
+ ` </${titleTag}>`,
610
+ ].filter(Boolean).join("\n")
611
+ : "";
612
+ if (parsed.collapsible) {
613
+ const openAttr = parsed.collapsibleOpen ? " open" : "";
614
+ return html([
615
+ `<${containerTag} class="${safeCalloutClass} ${safeConfigClassName} collapsible${appearanceClass}"${dataAttr} role="${ariaRole}"${labelledby}${dirAuto}${openAttr}${extraAttrs}>`,
616
+ titleBlockWithDir,
617
+ ` <${bodyTag} class="${safeCalloutBodyClass}">`,
618
+ bodyHtml,
619
+ ` </${bodyTag}>`,
620
+ `</${containerTag}>`,
621
+ ].filter(Boolean).join("\n"));
622
+ }
623
+ return html([
624
+ `<${containerTag} class="${safeCalloutClass} ${safeConfigClassName}${appearanceClass}"${dataAttr} role="${ariaRole}"${labelledby}${dirAuto}${extraAttrs}>`,
625
+ titleBlockWithDir,
626
+ ` <${bodyTag} class="${safeCalloutBodyClass}">`,
627
+ bodyHtml,
628
+ ` </${bodyTag}>`,
629
+ `</${containerTag}>`,
630
+ ].filter(Boolean).join("\n"));
631
+ }
632
+ // ---------------------------------------------------------------------------
633
+ // Helper: Build disclosure widget HTML (with tree view support)
634
+ // ---------------------------------------------------------------------------
635
+ function buildDisclosureHtml(parsed, blockquote, calloutPattern, options, depth = 0) {
636
+ const title = parsed.customTitle || "Details";
637
+ const openAttr = parsed.collapsibleOpen ? " open" : "";
638
+ const bodyHtml = buildBodyHtml(blockquote, calloutPattern, options);
639
+ // v1.2.0: build custom render escape hatch
640
+ if (typeof options.build === "function") {
641
+ try {
642
+ const customHtml = options.build(parsed, undefined, bodyHtml, options);
643
+ if (typeof customHtml === "string" && customHtml.length > 0) {
644
+ return html(customHtml);
645
+ }
646
+ }
647
+ catch {
648
+ // Fall through to default renderer on error
649
+ }
650
+ }
651
+ // Tree view: add depth class for nested disclosures
652
+ const safeDisclosureClass = escapeAttribute(options.disclosureClass);
653
+ const safeDisclosureTitleClass = escapeAttribute(options.disclosureTitleClass);
654
+ const safeDisclosureBodyClass = escapeAttribute(options.disclosureBodyClass);
655
+ const safeDepth = String(depth).replace(/[^0-9]/g, "") || "0";
656
+ const treeAttr = options.enableTreeView && depth > 0
657
+ ? ` class="${safeDisclosureClass} disclosure-tree" data-depth="${safeDepth}"`
658
+ : ` class="${safeDisclosureClass}"`;
659
+ // v1.2.0: stable title id + aria-labelledby + dir="auto"
660
+ const titleId = generateCalloutId("disclosure");
661
+ // v1.2.0: tags option (override element names for disclosures too)
662
+ const tags = options.tags ?? {};
663
+ const containerTag = tags.container || "details";
664
+ const titleTag = tags.title || "summary";
665
+ const bodyTag = tags.body || "div";
666
+ const disclosureHtml = [
667
+ `<${containerTag}${treeAttr} aria-labelledby="${titleId}" dir="auto"${openAttr}>`,
668
+ ` <${titleTag} class="${safeDisclosureTitleClass}" id="${titleId}" dir="auto">${escapeHtml(title)}</${titleTag}>`,
669
+ ` <${bodyTag} class="${safeDisclosureBodyClass}">`,
670
+ bodyHtml,
671
+ ` </${bodyTag}>`,
672
+ `</${containerTag}>`,
673
+ ].join("\n");
674
+ return html(disclosureHtml);
675
+ }
676
+ // ---------------------------------------------------------------------------
677
+ // Helper: Map callout type to ARIA role
678
+ // ---------------------------------------------------------------------------
679
+ function getAriaRole(type) {
680
+ switch (type) {
681
+ case "warning":
682
+ case "attention":
683
+ case "caution":
684
+ case "danger":
685
+ case "error":
686
+ case "fail":
687
+ case "failure":
688
+ case "missing":
689
+ return "alert";
690
+ default:
691
+ return "note";
692
+ }
693
+ }
694
+ // ---------------------------------------------------------------------------
695
+ // Helper: Serialize the first paragraph's content after the directive
696
+ // ---------------------------------------------------------------------------
697
+ function serializeFirstParagraphAfterDirective(paragraph, calloutPattern, allowDangerousHtml) {
698
+ const children = paragraph.children;
699
+ let foundDirective = false;
700
+ const remainingChildren = [];
701
+ for (let i = 0; i < children.length; i++) {
702
+ const child = children[i];
703
+ if (!foundDirective && child.type === "text") {
704
+ const textVal = child.value;
705
+ const match = textVal.match(calloutPattern);
706
+ if (match) {
707
+ foundDirective = true;
708
+ const afterDirective = textVal.slice(match[0].length);
709
+ if (afterDirective.trim()) {
710
+ const newlineIdx = afterDirective.indexOf("\n");
711
+ if (newlineIdx !== -1) {
712
+ const bodyText = afterDirective.slice(newlineIdx + 1).trim();
713
+ if (bodyText)
714
+ remainingChildren.push({ type: "text", value: bodyText });
715
+ }
716
+ // If there is no newline, the text after the directive on the
717
+ // same line is treated as the custom title (already extracted by
718
+ // parseCalloutDirective) and is intentionally NOT added to the
719
+ // body — this prevents the title from leaking into the body
720
+ // when it contains raw HTML.
721
+ }
722
+ for (let j = i + 1; j < children.length; j++) {
723
+ remainingChildren.push(children[j]);
724
+ }
725
+ break;
726
+ }
727
+ else {
728
+ remainingChildren.push(child);
729
+ }
730
+ }
731
+ else if (foundDirective) {
732
+ remainingChildren.push(child);
733
+ }
734
+ // If we haven't found the directive yet and the child is not a text node,
735
+ // skip it — but this typically doesn't happen because the directive is
736
+ // always in the first text node.
737
+ }
738
+ if (remainingChildren.length === 0)
739
+ return "";
740
+ const innerHtml = remainingChildren.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("");
741
+ return innerHtml.trim() ? `<p>${innerHtml}</p>\n` : "";
742
+ }
743
+ // ---------------------------------------------------------------------------
744
+ // Helpers: Serialize mdast nodes to HTML
745
+ // ---------------------------------------------------------------------------
746
+ function serializeNodeToHtml(node, indent = "", allowDangerousHtml = false) {
747
+ switch (node.type) {
748
+ case "paragraph": return `${indent}<p>${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</p>\n`;
749
+ case "heading": return `${indent}<h${node.depth}>${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</h${node.depth}>\n`;
750
+ case "list": {
751
+ const tag = node.ordered ? "ol" : "ul";
752
+ return `${indent}<${tag}>\n${node.children.map((c) => serializeNodeToHtml(c, indent + " ", allowDangerousHtml)).join("")}${indent}</${tag}>\n`;
753
+ }
754
+ case "listItem": return `${indent}<li>\n${node.children.map((c) => serializeNodeToHtml(c, indent + " ", allowDangerousHtml)).join("")}${indent}</li>\n`;
755
+ case "code": {
756
+ const lang = node.lang ? ` class="language-${escapeAttribute(node.lang)}"` : "";
757
+ return `${indent}<pre><code${lang}>${escapeHtml(node.value)}</code></pre>\n`;
758
+ }
759
+ case "blockquote": return `${indent}<blockquote>\n${node.children.map((c) => serializeNodeToHtml(c, indent + " ", allowDangerousHtml)).join("")}${indent}</blockquote>\n`;
760
+ case "thematicBreak": return `${indent}<hr />\n`;
761
+ case "html": return allowDangerousHtml ? `${indent}${node.value}\n` : `${indent}${escapeHtml(node.value)}\n`;
762
+ default: return "";
763
+ }
764
+ }
765
+ function inlineNodeToHtml(node, allowDangerousHtml = false) {
766
+ switch (node.type) {
767
+ case "text": return escapeHtml(node.value);
768
+ case "strong": return `<strong>${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</strong>`;
769
+ case "emphasis": return `<em>${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</em>`;
770
+ case "inlineCode": return `<code>${escapeHtml(node.value)}</code>`;
771
+ case "link": return `<a href="${escapeHtml(node.url)}">${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</a>`;
772
+ case "image": return `<img src="${escapeHtml(node.url)}" alt="${escapeHtml(node.alt || "")}" />`;
773
+ case "delete": return `<del>${node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("")}</del>`;
774
+ // SAFE-BY-DEFAULT: raw inline HTML is escaped unless the user explicitly
775
+ // opted in with `allowDangerousHtml: true`. This prevents XSS from
776
+ // untrusted markdown containing payloads like <img src=x onerror=alert(1)>.
777
+ case "html": return allowDangerousHtml ? node.value : escapeHtml(node.value);
778
+ default:
779
+ if (node.children)
780
+ return node.children.map((c) => inlineNodeToHtml(c, allowDangerousHtml)).join("");
781
+ return "";
782
+ }
783
+ }
784
+ // ---------------------------------------------------------------------------
785
+ // Helper: Compute blockquote nesting depth for tree view
786
+ // ---------------------------------------------------------------------------
787
+ function getBlockquoteDepth(node, parent, tree) {
788
+ let depth = 0;
789
+ let current = parent;
790
+ while (current && current !== tree) {
791
+ if (current.type === "blockquote")
792
+ depth++;
793
+ // Walk up — this is a simplified approach; for deeply nested trees
794
+ // we rely on the visit order which processes outer blocks first
795
+ break; // We use a different approach: pass depth during visit
796
+ }
797
+ return depth;
798
+ }
799
+ // ---------------------------------------------------------------------------
800
+ // Plugin implementation
801
+ // ---------------------------------------------------------------------------
802
+ export const remarkRemakeBlocks = (userOptions) => {
803
+ const options = {
804
+ ...DEFAULT_OPTIONS,
805
+ ...userOptions,
806
+ };
807
+ const configMap = buildCalloutConfigMap(options);
808
+ const calloutPattern = options.calloutPattern || buildCalloutPattern(configMap, options.enableDisclosures);
809
+ return (tree) => {
810
+ // v1.2.0: reset per-tree counter for idempotent output (same markdown →
811
+ // same HTML, including same ids). This is important for snapshot tests
812
+ // and for SSR/deterministic rendering.
813
+ resetCalloutIdCounter();
814
+ // ── Pass 1: Transform blockquotes → callouts / disclosures ──────
815
+ // We must process DEEPEST blockquotes first (inside-out) so that
816
+ // nested [!] directives are converted before their parents read them.
817
+ // We collect all blockquote nodes first, then process depth-first.
818
+ const blockquotes = [];
819
+ function collectBlockquotes(node, depth) {
820
+ if (!node.children)
821
+ return;
822
+ for (let i = 0; i < node.children.length; i++) {
823
+ const child = node.children[i];
824
+ if (child.type === "blockquote") {
825
+ blockquotes.push({ node: child, index: i, parent: node, depth });
826
+ // Recurse into the blockquote's children to find deeper ones
827
+ collectBlockquotes(child, depth + 1);
828
+ }
829
+ else {
830
+ collectBlockquotes(child, depth);
831
+ }
832
+ }
833
+ }
834
+ collectBlockquotes(tree, 0);
835
+ // Sort by depth descending — deepest first
836
+ blockquotes.sort((a, b) => b.depth - a.depth);
837
+ // Process each blockquote (deepest first)
838
+ for (const { node: bq, index, parent, depth } of blockquotes) {
839
+ // Verify the node is still at the expected position
840
+ // (it may have been replaced by a different node if a sibling was processed)
841
+ if (!parent || !parent.children)
842
+ continue;
843
+ if (parent.children[index] !== bq) {
844
+ // The node may have shifted due to earlier replacements in the same parent
845
+ // Try to find it by reference
846
+ const foundIdx = parent.children.indexOf(bq);
847
+ if (foundIdx === -1)
848
+ continue; // Already replaced
849
+ }
850
+ const parsed = parseCalloutDirective(bq, calloutPattern, configMap, options.enableDisclosures);
851
+ if (parsed) {
852
+ // For disclosures: depth > 0 means it's nested inside another blockquote
853
+ // (which is likely another disclosure or callout) → tree view
854
+ const disclosureDepth = parsed.isDisclosure ? depth : 0;
855
+ const calloutNode = buildCalloutHtml(parsed, bq, calloutPattern, configMap, options, disclosureDepth);
856
+ // Replace using the actual current index
857
+ const actualIdx = parent.children.indexOf(bq);
858
+ if (actualIdx !== -1) {
859
+ parent.children[actualIdx] = calloutNode;
860
+ }
861
+ }
862
+ else if (options.enhanceBlockquotes) {
863
+ const actualIdx = parent.children.indexOf(bq);
864
+ if (actualIdx !== -1) {
865
+ parent.children[actualIdx] = html([
866
+ `<div class="${escapeAttribute(options.blockquoteClass)}">`,
867
+ ` <blockquote>`,
868
+ bq.children.map((c) => serializeNodeToHtml(c, " ", options.allowDangerousHtml)).join(""),
869
+ ` </blockquote>`,
870
+ `</div>`,
871
+ ].join("\n"));
872
+ }
873
+ }
874
+ }
875
+ // ── Pass 2: Group consecutive disclosures into accordions ───────
876
+ if (options.enableAccordion && options.enableDisclosures) {
877
+ groupAccordions(tree, options.accordionClass);
878
+ }
879
+ };
880
+ };
881
+ // ---------------------------------------------------------------------------
882
+ // Pass 2: Group consecutive disclosure <details> into accordion wrapper
883
+ // ---------------------------------------------------------------------------
884
+ function groupAccordions(tree, accordionClass) {
885
+ function walk(node) {
886
+ if (!node.children)
887
+ return;
888
+ // Process children array
889
+ const newChildren = [];
890
+ let i = 0;
891
+ while (i < node.children.length) {
892
+ const child = node.children[i];
893
+ // Check if this is a disclosure <details> HTML node
894
+ if (child.type === "html" && isDisclosureHtml(child.value)) {
895
+ // Start collecting consecutive disclosures
896
+ const group = [child.value];
897
+ let j = i + 1;
898
+ // Collect consecutive disclosure nodes (skip whitespace-only text nodes)
899
+ while (j < node.children.length) {
900
+ const next = node.children[j];
901
+ if (next.type === "html" && isDisclosureHtml(next.value)) {
902
+ group.push(next.value);
903
+ j++;
904
+ }
905
+ else if (next.type === "text" && next.value.trim() === "") {
906
+ // Skip whitespace text nodes between disclosures
907
+ j++;
908
+ }
909
+ else {
910
+ break;
911
+ }
912
+ }
913
+ // If we found 2+ consecutive disclosures, wrap in accordion
914
+ if (group.length >= 2) {
915
+ const accordionHtml = [
916
+ `<div class="${escapeAttribute(accordionClass)}" data-accordion>`,
917
+ ...group,
918
+ `</div>`,
919
+ ].join("\n");
920
+ newChildren.push(html(accordionHtml));
921
+ i = j;
922
+ }
923
+ else {
924
+ newChildren.push(child);
925
+ i++;
926
+ }
927
+ }
928
+ else {
929
+ newChildren.push(child);
930
+ i++;
931
+ }
932
+ }
933
+ node.children = newChildren;
934
+ // Recurse into remaining children
935
+ for (const child of node.children) {
936
+ walk(child);
937
+ }
938
+ }
939
+ walk(tree);
940
+ }
941
+ /**
942
+ * Check if an HTML string is a disclosure <details> element.
943
+ * Matches <details class="disclosure"...> but NOT callout <details class="callout ...">
944
+ */
945
+ function isDisclosureHtml(htmlStr) {
946
+ const trimmed = htmlStr.trim();
947
+ return trimmed.startsWith('<details class="disclosure"') &&
948
+ !trimmed.includes('callout');
949
+ }
950
+ // ---------------------------------------------------------------------------
951
+ // Backward-compatible alias
952
+ // ---------------------------------------------------------------------------
953
+ export const remarkCalloutBlocks = remarkRemakeBlocks;
954
+ // Export helpers for users who supply a custom `build` function.
955
+ // These let `build` authors reuse the plugin's escaping logic.
956
+ export { escapeHtml, escapeAttribute, sanitizeColor };
957
+ export { BUILTIN_CALLOUTS };
958
+ //# sourceMappingURL=remark-remake-blocks.js.map