@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.
@@ -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, "&amp;")
354
+ .replace(/</g, "&lt;")
355
+ .replace(/>/g, "&gt;")
356
+ .replace(/"/g, "&quot;")
357
+ .replace(/'/g, "&#039;");
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