@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.
- 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 +9 -52
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -86
- package/dist/index.js.map +1 -1
- package/dist/remark-remake-blocks.d.ts +28 -0
- package/dist/remark-remake-blocks.d.ts.map +1 -0
- package/dist/remark-remake-blocks.js +958 -0
- package/dist/remark-remake-blocks.js.map +1 -0
- package/dist/styles.css +225 -4
- package/dist/types.d.ts +193 -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,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, "&")
|
|
421
|
+
.replace(/</g, "<")
|
|
422
|
+
.replace(/>/g, ">")
|
|
423
|
+
.replace(/"/g, """)
|
|
424
|
+
.replace(/'/g, "'");
|
|
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
|