@dr-ishaan/remake-blocks 1.1.0 → 1.3.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.
@@ -185,6 +185,92 @@ const BUILTIN_CALLOUTS = [
185
185
  },
186
186
  ];
187
187
  // ---------------------------------------------------------------------------
188
+ // v1.3.0: Alternative icon sets (Lucide + Emoji)
189
+ // ---------------------------------------------------------------------------
190
+ // These are keyed by callout type. When iconSet === "lucide" or "emoji",
191
+ // the plugin uses these icons instead of the default Octicon SVGs.
192
+ // Custom callouts always use their configured `icon` regardless of iconSet.
193
+ const LUCIDE_ICONS = {
194
+ note: `<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>`,
195
+ tip: `<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="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>`,
196
+ important: `<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="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg>`,
197
+ warning: `<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.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>`,
198
+ caution: `<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="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`,
199
+ abstract: `<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"/><path d="M16 13H8"/><path d="M16 17H8"/><path d="M10 9H8"/></svg>`,
200
+ info: `<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>`,
201
+ success: `<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="m9 11 3 3L22 4"/></svg>`,
202
+ question: `<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>`,
203
+ failure: `<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="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`,
204
+ danger: `<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="m13 2-3 7h7l-3 7"/><path d="M12 22a10 10 0 1 1 0-20"/></svg>`,
205
+ quote: `<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.031V20c0 1 0 1 1 1z"/><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>`,
206
+ bug: `<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>`,
207
+ example: `<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.5z"/><polyline points="14 2 14 8 20 8"/><path d="M16 13H8"/><path d="M16 17H8"/><path d="M10 9H8"/></svg>`,
208
+ todo: `<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>`,
209
+ summary: `<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"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>`,
210
+ tldr: `<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="m13 2-3 7h7l-3 7"/><path d="M12 22a10 10 0 1 1 0-20"/></svg>`,
211
+ hint: `<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>`,
212
+ check: `<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>`,
213
+ done: `<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="m9 11 3 3L22 4"/></svg>`,
214
+ help: `<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>`,
215
+ faq: `<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="M10 7h.01"/><path d="M14 7h.01"/><path d="M8 11h.01"/><path d="M12 11h.01"/><path d="M16 11h.01"/></svg>`,
216
+ attention: `<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>`,
217
+ fail: `<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"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`,
218
+ missing: `<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"/><path d="m21 21-4.3-4.3"/><path d="M8 11h6"/></svg>`,
219
+ error: `<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"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>`,
220
+ cite: `<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>`,
221
+ };
222
+ const EMOJI_ICONS = {
223
+ note: "ℹ️",
224
+ tip: "💡",
225
+ important: "⚠️",
226
+ warning: "⚠️",
227
+ caution: "⛔",
228
+ abstract: "📋",
229
+ info: "ℹ️",
230
+ success: "✅",
231
+ question: "❓",
232
+ failure: "❌",
233
+ danger: "⚡",
234
+ quote: "💬",
235
+ bug: "🐛",
236
+ example: "📝",
237
+ todo: "☑️",
238
+ summary: "📋",
239
+ tldr: "⚡",
240
+ hint: "💡",
241
+ check: "✅",
242
+ done: "✅",
243
+ help: "❓",
244
+ faq: "❓",
245
+ attention: "⚠️",
246
+ fail: "❌",
247
+ missing: "🚫",
248
+ error: "❌",
249
+ cite: "📌",
250
+ };
251
+ /**
252
+ * Resolve the icon for a given callout type based on the iconSet option.
253
+ * Returns the icon string (SVG or emoji) or empty string if not found.
254
+ */
255
+ function resolveIcon(type, config, iconSet) {
256
+ // Custom callouts always use their configured icon, regardless of iconSet.
257
+ // We detect this by checking if the type is NOT in BUILTIN_CALLOUTS.
258
+ // (config may be a normalized custom callout — its icon is the user's choice.)
259
+ const isBuiltin = BUILTIN_CALLOUTS.some((b) => b.type === type);
260
+ if (!isBuiltin) {
261
+ return config.icon;
262
+ }
263
+ // For builtins, swap icon based on iconSet option.
264
+ if (iconSet === "lucide")
265
+ return LUCIDE_ICONS[type] ?? config.icon;
266
+ if (iconSet === "emoji")
267
+ return EMOJI_ICONS[type] ?? config.icon;
268
+ if (iconSet === "none")
269
+ return "";
270
+ // Default: octicon (the builtin's own icon)
271
+ return config.icon;
272
+ }
273
+ // ---------------------------------------------------------------------------
188
274
  // Default plugin options
189
275
  // ---------------------------------------------------------------------------
190
276
  const DEFAULT_OPTIONS = {
@@ -205,6 +291,16 @@ const DEFAULT_OPTIONS = {
205
291
  accordionClass: "disclosure-accordion",
206
292
  enableTreeView: true,
207
293
  allowDangerousHtml: false,
294
+ // v1.2.0 defaults
295
+ aliases: {},
296
+ showIndicator: true,
297
+ iconSet: "octicon",
298
+ appearance: "default",
299
+ props: {},
300
+ build: undefined,
301
+ tags: {},
302
+ // v1.3.0 defaults
303
+ enableDirectiveSyntax: false,
208
304
  };
209
305
  // ---------------------------------------------------------------------------
210
306
  // Helper: Validate + normalize a single custom callout config.
@@ -257,7 +353,7 @@ function sanitizeColor(value, fallback) {
257
353
  return trimmed;
258
354
  }
259
355
  // ---------------------------------------------------------------------------
260
- // Helper: Build the callout configuration map
356
+ // Helper: Build the callout configuration map (with alias resolution)
261
357
  // ---------------------------------------------------------------------------
262
358
  function buildCalloutConfigMap(options) {
263
359
  const map = new Map();
@@ -273,10 +369,28 @@ function buildCalloutConfigMap(options) {
273
369
  }
274
370
  }
275
371
  }
372
+ // Register aliases: each alias points to its canonical config.
373
+ // Aliases are case-insensitive (lowercased on registration AND lookup).
374
+ if (options.aliases) {
375
+ for (const [canonical, aliasList] of Object.entries(options.aliases)) {
376
+ const canonicalLower = canonical.toLowerCase();
377
+ const canonicalConfig = map.get(canonicalLower);
378
+ if (!canonicalConfig || !Array.isArray(aliasList))
379
+ continue;
380
+ for (const alias of aliasList) {
381
+ if (typeof alias !== "string" || alias.trim() === "")
382
+ continue;
383
+ map.set(alias.toLowerCase(), canonicalConfig);
384
+ }
385
+ }
386
+ }
276
387
  return map;
277
388
  }
278
389
  // ---------------------------------------------------------------------------
279
- // Helper: Build the dynamic regex matching all directives
390
+ // Helper: Build the dynamic regex matching all directives.
391
+ // Captures: (1) type (or empty for disclosure), (2) fold marker + or -,
392
+ // (3) optional inline title text (everything until end-of-line or `{`),
393
+ // (4) optional `{key=value key=value}` overrides block.
280
394
  // ---------------------------------------------------------------------------
281
395
  function buildCalloutPattern(configMap, enableDisclosures) {
282
396
  const allDirectives = Array.from(configMap.keys())
@@ -286,7 +400,76 @@ function buildCalloutPattern(configMap, enableDisclosures) {
286
400
  allDirectives.unshift("");
287
401
  }
288
402
  const typePattern = allDirectives.join("|");
289
- return new RegExp(`^\\[!(${typePattern})\\]([+-]?)(?:[^\\S\\n]+(.+))?`, "i");
403
+ // Group 3 (title) is non-greedy and stops at `{` (start of overrides) or end of line.
404
+ // Group 4 (overrides) captures `{...}` if present.
405
+ return new RegExp(`^\\[!(${typePattern})\\]([+-]?)(?:[^\\S\\n]+([^\\n{]*))?(\\s*\\{[^\\n]*\\})?`, "i");
406
+ }
407
+ // ---------------------------------------------------------------------------
408
+ // Helper: Parse `{key=value key=value}` overrides block into an object.
409
+ // Supported keys (v1.2.0):
410
+ // - icon: true|false — override showIndicator per-callout
411
+ // - appearance: default|minimal|simple|hidden — override appearance per-callout
412
+ // - inline: inline|inline-end — v1.3.0+: float left/right (responsive)
413
+ // Unknown keys are silently ignored.
414
+ // ---------------------------------------------------------------------------
415
+ function parseOverrides(overridesBlock) {
416
+ if (!overridesBlock)
417
+ return undefined;
418
+ const inner = overridesBlock.trim().replace(/^\{|\}$/g, "").trim();
419
+ if (!inner)
420
+ return undefined;
421
+ const result = {};
422
+ // First pass: handle bare keywords (no `=` sign).
423
+ // Supported bare keywords: `inline`, `inline-end`
424
+ // These set the inline override without needing `inline=true`.
425
+ // Note: `inline-end` must be matched BEFORE `inline` to avoid the prefix
426
+ // matching, so we use a single regex with `inline-end` first in the
427
+ // alternation and consume the entire match.
428
+ const bareKeywordRe = /\b(inline-end|inline)\b(?!\s*=)/gi;
429
+ let bareMatch;
430
+ while ((bareMatch = bareKeywordRe.exec(inner)) !== null) {
431
+ const kw = bareMatch[1].toLowerCase();
432
+ if (kw === "inline-end")
433
+ result.inline = "inline-end";
434
+ else if (kw === "inline")
435
+ result.inline = "inline";
436
+ }
437
+ // Second pass: handle key=value pairs.
438
+ // Values can be unquoted barewords or "quoted strings".
439
+ const pairRe = /(\w[\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s}]+))/g;
440
+ let m;
441
+ while ((m = pairRe.exec(inner)) !== null) {
442
+ const key = m[1].toLowerCase();
443
+ const value = (m[2] ?? m[3] ?? m[4] ?? "").trim();
444
+ if (key === "icon") {
445
+ if (value === "false" || value === "0" || value === "no")
446
+ result.icon = false;
447
+ else if (value === "true" || value === "1" || value === "yes")
448
+ result.icon = true;
449
+ }
450
+ else if (key === "appearance") {
451
+ if (value === "default" || value === "minimal" || value === "simple" || value === "hidden") {
452
+ result.appearance = value;
453
+ }
454
+ }
455
+ else if (key === "inline") {
456
+ // v1.3.0+: inline floating callouts
457
+ if (value === "true" || value === "left" || value === "inline") {
458
+ result.inline = "inline";
459
+ }
460
+ else if (value === "right" || value === "inline-end" || value === "end") {
461
+ result.inline = "inline-end";
462
+ }
463
+ else if (value === "false" || value === "0" || value === "no") {
464
+ // explicitly disable inline (clear any bare-keyword setting)
465
+ result.inline = undefined;
466
+ }
467
+ }
468
+ // Unknown keys silently ignored
469
+ }
470
+ if (result.icon === undefined && result.appearance === undefined && result.inline === undefined)
471
+ return undefined;
472
+ return result;
290
473
  }
291
474
  // ---------------------------------------------------------------------------
292
475
  // Helper: Parse the first paragraph of a blockquote to detect a callout
@@ -328,7 +511,9 @@ function parseCalloutDirective(blockquote, calloutPattern, configMap, enableDisc
328
511
  remainingContent = afterNewline.trim() || undefined;
329
512
  }
330
513
  }
331
- return { type, customTitle, remainingContent, collapsible: effectiveCollapsible, collapsibleOpen: effectiveCollapsibleOpen, isDisclosure };
514
+ // v1.2.0: parse per-callout overrides from `{key=value}` block (match[4])
515
+ const overrides = parseOverrides(match[4]);
516
+ return { type, customTitle, remainingContent, collapsible: effectiveCollapsible, collapsibleOpen: effectiveCollapsibleOpen, isDisclosure, overrides };
332
517
  }
333
518
  // ---------------------------------------------------------------------------
334
519
  // Helpers: extractTextContent, html, escapeHtml
@@ -377,6 +562,83 @@ function buildBodyHtml(blockquote, calloutPattern, options) {
377
562
  // ---------------------------------------------------------------------------
378
563
  // Helper: Build the callout HTML
379
564
  // ---------------------------------------------------------------------------
565
+ // ---------------------------------------------------------------------------
566
+ // v1.2.0 helpers: stable id generation, props formatting, override resolution
567
+ // ---------------------------------------------------------------------------
568
+ // Per-tree counter for callout ids. Reset at the start of each plugin run
569
+ // (each `transform(tree)` invocation) so the same markdown always produces
570
+ // the same output HTML (idempotency / determinism for snapshot tests).
571
+ let __calloutIdCounter = 0;
572
+ function resetCalloutIdCounter() {
573
+ __calloutIdCounter = 0;
574
+ }
575
+ function generateCalloutId(type) {
576
+ __calloutIdCounter += 1;
577
+ // Sanitize type for use in an HTML id (letters/digits/hyphen only).
578
+ const safeType = String(type).replace(/[^a-zA-Z0-9-]/g, "-") || "callout";
579
+ return `callout-${safeType}-${__calloutIdCounter.toString(36)}`;
580
+ }
581
+ function formatPropsAsAttrs(props) {
582
+ if (!props)
583
+ return "";
584
+ let out = "";
585
+ for (const [key, value] of Object.entries(props)) {
586
+ if (value === undefined || value === null)
587
+ continue;
588
+ // Sanitize the key: allow letters, digits, hyphens, colons (for namespaces like xml:lang)
589
+ const safeKey = key.replace(/[^a-zA-Z0-9:_-]/g, "");
590
+ if (!safeKey)
591
+ continue;
592
+ // Don't allow `style` or event handlers to be set via props (defense in depth)
593
+ if (safeKey.toLowerCase() === "style")
594
+ continue;
595
+ if (safeKey.toLowerCase().startsWith("on"))
596
+ continue;
597
+ out += ` ${safeKey}="${escapeAttribute(String(value))}"`;
598
+ }
599
+ return out;
600
+ }
601
+ function resolveProps(propsConfig, type, parsed) {
602
+ if (!propsConfig)
603
+ return {};
604
+ const entry = propsConfig[type] ?? propsConfig[type.toLowerCase()];
605
+ if (!entry)
606
+ return {};
607
+ if (typeof entry === "function") {
608
+ try {
609
+ return entry(parsed) || {};
610
+ }
611
+ catch {
612
+ return {};
613
+ }
614
+ }
615
+ if (typeof entry === "object")
616
+ return entry;
617
+ return {};
618
+ }
619
+ // ---------------------------------------------------------------------------
620
+ // Helper: Resolve effective icon visibility + appearance, merging global
621
+ // options with per-callout overrides.
622
+ // ---------------------------------------------------------------------------
623
+ function resolveVisuals(parsed, options) {
624
+ let showIcon = options.showIndicator !== false; // default true
625
+ let appearance = options.appearance ?? "default";
626
+ if (parsed.overrides) {
627
+ if (parsed.overrides.icon === false)
628
+ showIcon = false;
629
+ else if (parsed.overrides.icon === true)
630
+ showIcon = true;
631
+ if (parsed.overrides.appearance)
632
+ appearance = parsed.overrides.appearance;
633
+ }
634
+ // "hidden" appearance implies no icon
635
+ if (appearance === "hidden")
636
+ showIcon = false;
637
+ return { showIcon, appearance };
638
+ }
639
+ // ---------------------------------------------------------------------------
640
+ // Helper: Build the callout HTML (v1.2.0 — with all community-inspired features)
641
+ // ---------------------------------------------------------------------------
380
642
  function buildCalloutHtml(parsed, blockquote, calloutPattern, configMap, options, depth = 0) {
381
643
  // ── Disclosure Widget ──────────────────────────────────────────────
382
644
  if (parsed.isDisclosure) {
@@ -387,39 +649,108 @@ function buildCalloutHtml(parsed, blockquote, calloutPattern, configMap, options
387
649
  const title = parsed.customTitle || config.defaultTitle;
388
650
  const dataAttr = options.dataCalloutType ? ` data-callout-type="${escapeAttribute(parsed.type)}"` : "";
389
651
  const bodyHtml = buildBodyHtml(blockquote, calloutPattern, options);
652
+ // v1.2.0: resolve per-callout visuals (icon visibility + appearance)
653
+ const { showIcon, appearance } = resolveVisuals(parsed, options);
654
+ // v1.2.0: build custom render escape hatch
655
+ if (typeof options.build === "function") {
656
+ try {
657
+ const customHtml = options.build(parsed, config, bodyHtml, options);
658
+ if (typeof customHtml === "string" && customHtml.length > 0) {
659
+ return html(customHtml);
660
+ }
661
+ }
662
+ catch {
663
+ // Fall through to default renderer on error
664
+ }
665
+ }
390
666
  // Sanitize colors before interpolating into style attribute
391
667
  const safeIconColor = sanitizeColor(config.iconColor || config.color, "#57606a");
392
- const ariaRole = getAriaRole(parsed.type);
668
+ // v1.2.0: WCAG fix — use role="note" for ALL callouts (was: role="alert" for warnings).
669
+ // role="alert" is for dynamically-inserted content and causes aggressive immediate
670
+ // announcement that disrupts screen reader users on static content.
671
+ // Use role="note" + aria-labelledby for proper title→container association.
672
+ const ariaRole = "note";
393
673
  // Escape className + calloutClass to prevent attribute breakout
394
674
  const safeCalloutClass = escapeAttribute(options.calloutClass);
395
675
  const safeConfigClassName = escapeAttribute(config.className);
396
676
  const safeCalloutTitleClass = escapeAttribute(options.calloutTitleClass);
397
677
  const safeCalloutBodyClass = escapeAttribute(options.calloutBodyClass);
678
+ // v1.2.0: stable title id for aria-labelledby
679
+ const titleId = generateCalloutId(parsed.type);
680
+ // v1.2.0: resolve per-type props (dir, style, data-*, etc.)
681
+ const extraProps = resolveProps(options.props, parsed.type, parsed);
682
+ const extraAttrs = formatPropsAsAttrs(extraProps);
683
+ // v1.2.0: appearance class
684
+ const appearanceClass = appearance !== "default" ? ` callout-${appearance}` : "";
685
+ // v1.3.0: inline floating class (responsive float left/right)
686
+ const inlineClass = parsed.overrides?.inline ? ` callout-${parsed.overrides.inline}` : "";
687
+ // v1.2.0: icon HTML — only render if showIcon AND appearance allows it
688
+ // v1.3.0: resolve icon based on iconSet option (octicon|lucide|emoji|none)
689
+ const renderIcon = showIcon && appearance !== "simple" && appearance !== "hidden";
690
+ const resolvedIcon = renderIcon ? resolveIcon(parsed.type, config, options.iconSet) : "";
691
+ const effectiveRenderIcon = renderIcon && resolvedIcon.length > 0;
692
+ const iconHtml = effectiveRenderIcon
693
+ ? `<span class="callout-icon" style="color:${safeIconColor}" aria-hidden="true">${resolvedIcon}</span>`
694
+ : "";
695
+ // v1.2.0: title-text always rendered (unless appearance="hidden")
696
+ const renderTitle = appearance !== "hidden";
697
+ const titleTextHtml = renderTitle
698
+ ? `<span class="callout-title-text">${escapeHtml(title)}</span>`
699
+ : "";
700
+ // v1.2.0: tags option (override element names)
701
+ const tags = options.tags ?? {};
702
+ const containerTag = tags.container || (parsed.collapsible ? "details" : "aside");
703
+ const titleTag = tags.title || (parsed.collapsible ? "summary" : "div");
704
+ const iconTag = tags.icon || "span";
705
+ const titleTextTag = tags.titleText || "span";
706
+ const bodyTag = tags.body || "div";
707
+ // Build icon span with custom tag if provided
708
+ const iconSpan = effectiveRenderIcon
709
+ ? `<${iconTag} class="callout-icon" style="color:${safeIconColor}" aria-hidden="true">${resolvedIcon}</${iconTag}>`
710
+ : "";
711
+ // Build the title block (or skip for "hidden" appearance)
712
+ const titleBlock = renderTitle
713
+ ? [
714
+ ` <${titleTag} class="${safeCalloutTitleClass}" style="color:${safeIconColor}" id="${titleId}">`,
715
+ iconSpan && ` ${iconSpan}`,
716
+ ` <${titleTextTag} class="callout-title-text">${escapeHtml(title)}</${titleTextTag}>`,
717
+ ` </${titleTag}>`,
718
+ ].filter(Boolean).join("\n")
719
+ : "";
720
+ // v1.2.0: aria-labelledby on container (only if title rendered)
721
+ const labelledby = renderTitle ? ` aria-labelledby="${titleId}"` : "";
722
+ // v1.2.0: dir="auto" on container + title for RTL/Unicode (community best practice)
723
+ const dirAuto = ` dir="auto"`;
724
+ const titleDirAuto = renderTitle ? ` dir="auto"` : "";
725
+ // Rebuild title block with dir="auto" (already in <title> opening tag below)
726
+ // For simplicity, we'll inline the title block here with all attributes:
727
+ const titleBlockWithDir = renderTitle
728
+ ? [
729
+ ` <${titleTag} class="${safeCalloutTitleClass}" style="color:${safeIconColor}" id="${titleId}"${titleDirAuto}>`,
730
+ iconSpan && ` ${iconSpan}`,
731
+ ` <${titleTextTag} class="callout-title-text">${escapeHtml(title)}</${titleTextTag}>`,
732
+ ` </${titleTag}>`,
733
+ ].filter(Boolean).join("\n")
734
+ : "";
398
735
  if (parsed.collapsible) {
399
736
  const openAttr = parsed.collapsibleOpen ? " open" : "";
400
737
  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}">`,
738
+ `<${containerTag} class="${safeCalloutClass} ${safeConfigClassName} collapsible${appearanceClass}${inlineClass}"${dataAttr} role="${ariaRole}"${labelledby}${dirAuto}${openAttr}${extraAttrs}>`,
739
+ titleBlockWithDir,
740
+ ` <${bodyTag} class="${safeCalloutBodyClass}">`,
407
741
  bodyHtml,
408
- ` </div>`,
409
- `</details>`,
410
- ].join("\n"));
742
+ ` </${bodyTag}>`,
743
+ `</${containerTag}>`,
744
+ ].filter(Boolean).join("\n"));
411
745
  }
412
746
  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}">`,
747
+ `<${containerTag} class="${safeCalloutClass} ${safeConfigClassName}${appearanceClass}${inlineClass}"${dataAttr} role="${ariaRole}"${labelledby}${dirAuto}${extraAttrs}>`,
748
+ titleBlockWithDir,
749
+ ` <${bodyTag} class="${safeCalloutBodyClass}">`,
419
750
  bodyHtml,
420
- ` </div>`,
421
- `</aside>`,
422
- ].join("\n"));
751
+ ` </${bodyTag}>`,
752
+ `</${containerTag}>`,
753
+ ].filter(Boolean).join("\n"));
423
754
  }
424
755
  // ---------------------------------------------------------------------------
425
756
  // Helper: Build disclosure widget HTML (with tree view support)
@@ -428,6 +759,18 @@ function buildDisclosureHtml(parsed, blockquote, calloutPattern, options, depth
428
759
  const title = parsed.customTitle || "Details";
429
760
  const openAttr = parsed.collapsibleOpen ? " open" : "";
430
761
  const bodyHtml = buildBodyHtml(blockquote, calloutPattern, options);
762
+ // v1.2.0: build custom render escape hatch
763
+ if (typeof options.build === "function") {
764
+ try {
765
+ const customHtml = options.build(parsed, undefined, bodyHtml, options);
766
+ if (typeof customHtml === "string" && customHtml.length > 0) {
767
+ return html(customHtml);
768
+ }
769
+ }
770
+ catch {
771
+ // Fall through to default renderer on error
772
+ }
773
+ }
431
774
  // Tree view: add depth class for nested disclosures
432
775
  const safeDisclosureClass = escapeAttribute(options.disclosureClass);
433
776
  const safeDisclosureTitleClass = escapeAttribute(options.disclosureTitleClass);
@@ -436,13 +779,20 @@ function buildDisclosureHtml(parsed, blockquote, calloutPattern, options, depth
436
779
  const treeAttr = options.enableTreeView && depth > 0
437
780
  ? ` class="${safeDisclosureClass} disclosure-tree" data-depth="${safeDepth}"`
438
781
  : ` class="${safeDisclosureClass}"`;
782
+ // v1.2.0: stable title id + aria-labelledby + dir="auto"
783
+ const titleId = generateCalloutId("disclosure");
784
+ // v1.2.0: tags option (override element names for disclosures too)
785
+ const tags = options.tags ?? {};
786
+ const containerTag = tags.container || "details";
787
+ const titleTag = tags.title || "summary";
788
+ const bodyTag = tags.body || "div";
439
789
  const disclosureHtml = [
440
- `<details${treeAttr}${openAttr}>`,
441
- ` <summary class="${safeDisclosureTitleClass}">${escapeHtml(title)}</summary>`,
442
- ` <div class="${safeDisclosureBodyClass}">`,
790
+ `<${containerTag}${treeAttr} aria-labelledby="${titleId}" dir="auto"${openAttr}>`,
791
+ ` <${titleTag} class="${safeDisclosureTitleClass}" id="${titleId}" dir="auto">${escapeHtml(title)}</${titleTag}>`,
792
+ ` <${bodyTag} class="${safeDisclosureBodyClass}">`,
443
793
  bodyHtml,
444
- ` </div>`,
445
- `</details>`,
794
+ ` </${bodyTag}>`,
795
+ `</${containerTag}>`,
446
796
  ].join("\n");
447
797
  return html(disclosureHtml);
448
798
  }
@@ -570,6 +920,192 @@ function getBlockquoteDepth(node, parent, tree) {
570
920
  return depth;
571
921
  }
572
922
  // ---------------------------------------------------------------------------
923
+ // v1.3.0: Directive syntax transformer (:::type[Title] ... :::)
924
+ // ---------------------------------------------------------------------------
925
+ // Detects :::type paragraphs and converts them to callouts. This is a
926
+ // lightweight alternative to installing remark-directive — we detect the
927
+ // pattern in raw paragraph text and synthesize a ParsedCallout + HTML node.
928
+ //
929
+ // Supported syntax:
930
+ // :::note — basic callout (default title)
931
+ // :::note[Custom Title] — with custom title
932
+ // :::note{fold=-} — with overrides (icon, appearance, fold, inline)
933
+ // :::note[Title]{fold=-} — title + overrides
934
+ // ::::note ... :::: — 4-colon variant (for nesting; same behavior)
935
+ //
936
+ // The closing ::: must be on its own line. Body content between the opening
937
+ // and closing lines is rendered as the callout body.
938
+ function transformDirectiveSyntax(tree, configMap, options) {
939
+ if (!tree.children)
940
+ return;
941
+ const newChildren = [];
942
+ let i = 0;
943
+ while (i < tree.children.length) {
944
+ const child = tree.children[i];
945
+ // Check if this is a paragraph containing a :::type ... ::: directive
946
+ if (child.type === "paragraph") {
947
+ const text = extractTextContent(child);
948
+ // Use string methods (more reliable than regex with ^ anchor in some Node versions)
949
+ // Pattern: :::type[Title]{overrides}\n<body>\n:::
950
+ // Note: We use startsWith() + manual parsing instead of /^.../ regex
951
+ // because the ^ anchor has issues in some Node.js environments.
952
+ if (text.startsWith(":::")) {
953
+ // Count the colons
954
+ let colonCount = 0;
955
+ while (colonCount < text.length && text[colonCount] === ":")
956
+ colonCount++;
957
+ if (colonCount >= 3) {
958
+ const afterColons = text.slice(colonCount);
959
+ // Match type name (letters, digits, hyphens)
960
+ const typeMatch = afterColons.match(/^([a-zA-Z][\w-]*)/);
961
+ if (typeMatch) {
962
+ const rawType = typeMatch[1];
963
+ const type = rawType.toLowerCase();
964
+ const afterType = afterColons.slice(typeMatch[0].length);
965
+ // Parse optional [Title] and {overrides}
966
+ let title;
967
+ let overridesBlock;
968
+ let remaining = afterType;
969
+ // Optional [Title]
970
+ if (remaining.startsWith("[")) {
971
+ const closeIdx = remaining.indexOf("]");
972
+ if (closeIdx !== -1) {
973
+ title = remaining.slice(1, closeIdx);
974
+ remaining = remaining.slice(closeIdx + 1);
975
+ }
976
+ }
977
+ // Optional {overrides}
978
+ if (remaining.startsWith("{")) {
979
+ const closeIdx = remaining.indexOf("}");
980
+ if (closeIdx !== -1) {
981
+ overridesBlock = `{${remaining.slice(1, closeIdx)}}`;
982
+ remaining = remaining.slice(closeIdx + 1);
983
+ }
984
+ }
985
+ // Must be followed by \n
986
+ if (remaining.startsWith("\n")) {
987
+ remaining = remaining.slice(1);
988
+ // Find the closing ::: (must be on its own line at the end)
989
+ // Look for \n::: at the end
990
+ const closingMatch = remaining.match(/\n(:{3,})$/);
991
+ if (closingMatch) {
992
+ const bodyText = remaining.slice(0, closingMatch.index);
993
+ // Only convert if the type is a known callout type
994
+ if (configMap.has(type)) {
995
+ const config = configMap.get(type);
996
+ const customTitle = title?.trim() || undefined;
997
+ const overrides = parseOverrides(overridesBlock);
998
+ // Check for fold override
999
+ let foldMarker = "";
1000
+ if (overridesBlock) {
1001
+ const foldMatch = overridesBlock.match(/fold\s*=\s*([+-])/);
1002
+ if (foldMatch)
1003
+ foldMarker = foldMatch[1];
1004
+ }
1005
+ const collapsible = foldMarker === "+" || foldMarker === "-";
1006
+ const collapsibleOpen = foldMarker === "+";
1007
+ const parsed = {
1008
+ type: type,
1009
+ customTitle,
1010
+ collapsible,
1011
+ collapsibleOpen,
1012
+ isDisclosure: false,
1013
+ overrides,
1014
+ };
1015
+ // Build body HTML from the bodyText
1016
+ const bodyLines = bodyText.split("\n");
1017
+ const bodyHtml = bodyLines
1018
+ .map((line) => {
1019
+ if (line.trim() === "")
1020
+ return "";
1021
+ const escaped = options.allowDangerousHtml ? line : escapeHtml(line);
1022
+ return `<p>${escaped}</p>\n`;
1023
+ })
1024
+ .join("");
1025
+ const calloutHtml = buildCalloutFromParts(parsed, config, bodyHtml, options);
1026
+ newChildren.push(html(calloutHtml));
1027
+ i++;
1028
+ continue;
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ }
1034
+ }
1035
+ }
1036
+ newChildren.push(child);
1037
+ i++;
1038
+ }
1039
+ tree.children = newChildren;
1040
+ }
1041
+ // ---------------------------------------------------------------------------
1042
+ // Helper: Build callout HTML from parsed parts (used by directive syntax)
1043
+ // ---------------------------------------------------------------------------
1044
+ function buildCalloutFromParts(parsed, config, bodyHtml, options) {
1045
+ // Reuse the existing buildCalloutHtml by synthesizing a fake blockquote
1046
+ // structure. We construct a minimal Blockquote-compatible object that
1047
+ // buildBodyHtml can process.
1048
+ const fakeBlockquote = {
1049
+ type: "blockquote",
1050
+ children: [
1051
+ {
1052
+ type: "paragraph",
1053
+ children: [{ type: "text", value: "" }],
1054
+ },
1055
+ ],
1056
+ };
1057
+ // We need to call buildCalloutHtml which expects a real blockquote.
1058
+ // Instead, inline the essential rendering logic here.
1059
+ const title = parsed.customTitle || config.defaultTitle;
1060
+ const dataAttr = options.dataCalloutType ? ` data-callout-type="${escapeAttribute(parsed.type)}"` : "";
1061
+ const safeIconColor = sanitizeColor(config.iconColor || config.color, "#57606a");
1062
+ const ariaRole = "note";
1063
+ const safeCalloutClass = escapeAttribute(options.calloutClass);
1064
+ const safeConfigClassName = escapeAttribute(config.className);
1065
+ const safeCalloutTitleClass = escapeAttribute(options.calloutTitleClass);
1066
+ const safeCalloutBodyClass = escapeAttribute(options.calloutBodyClass);
1067
+ const titleId = generateCalloutId(parsed.type);
1068
+ const { showIcon, appearance } = resolveVisuals(parsed, options);
1069
+ const resolvedIcon = showIcon && appearance !== "simple" && appearance !== "hidden"
1070
+ ? resolveIcon(parsed.type, config, options.iconSet)
1071
+ : "";
1072
+ const effectiveRenderIcon = showIcon && appearance !== "simple" && appearance !== "hidden" && resolvedIcon.length > 0;
1073
+ const iconSpan = effectiveRenderIcon
1074
+ ? `<span class="callout-icon" style="color:${safeIconColor}" aria-hidden="true">${resolvedIcon}</span>`
1075
+ : "";
1076
+ const renderTitle = appearance !== "hidden";
1077
+ const labelledby = renderTitle ? ` aria-labelledby="${titleId}"` : "";
1078
+ const appearanceClass = appearance !== "default" ? ` callout-${appearance}` : "";
1079
+ const inlineClass = parsed.overrides?.inline ? ` callout-${parsed.overrides.inline}` : "";
1080
+ const titleBlock = renderTitle
1081
+ ? [
1082
+ ` <div class="${safeCalloutTitleClass}" style="color:${safeIconColor}" id="${titleId}" dir="auto">`,
1083
+ iconSpan && ` ${iconSpan}`,
1084
+ ` <span class="callout-title-text">${escapeHtml(title)}</span>`,
1085
+ ` </div>`,
1086
+ ].filter(Boolean).join("\n")
1087
+ : "";
1088
+ if (parsed.collapsible) {
1089
+ const openAttr = parsed.collapsibleOpen ? " open" : "";
1090
+ return [
1091
+ `<details class="${safeCalloutClass} ${safeConfigClassName} collapsible${appearanceClass}${inlineClass}"${dataAttr} role="${ariaRole}"${labelledby} dir="auto"${openAttr}>`,
1092
+ titleBlock.replace(/<div /, "<summary ").replace(/<\/div>$/, "</summary>"),
1093
+ ` <div class="${safeCalloutBodyClass}">`,
1094
+ bodyHtml,
1095
+ ` </div>`,
1096
+ `</details>`,
1097
+ ].filter(Boolean).join("\n");
1098
+ }
1099
+ return [
1100
+ `<aside class="${safeCalloutClass} ${safeConfigClassName}${appearanceClass}${inlineClass}"${dataAttr} role="${ariaRole}"${labelledby} dir="auto">`,
1101
+ titleBlock,
1102
+ ` <div class="${safeCalloutBodyClass}">`,
1103
+ bodyHtml,
1104
+ ` </div>`,
1105
+ `</aside>`,
1106
+ ].filter(Boolean).join("\n");
1107
+ }
1108
+ // ---------------------------------------------------------------------------
573
1109
  // Plugin implementation
574
1110
  // ---------------------------------------------------------------------------
575
1111
  export const remarkRemakeBlocks = (userOptions) => {
@@ -580,6 +1116,15 @@ export const remarkRemakeBlocks = (userOptions) => {
580
1116
  const configMap = buildCalloutConfigMap(options);
581
1117
  const calloutPattern = options.calloutPattern || buildCalloutPattern(configMap, options.enableDisclosures);
582
1118
  return (tree) => {
1119
+ // v1.2.0: reset per-tree counter for idempotent output (same markdown →
1120
+ // same HTML, including same ids). This is important for snapshot tests
1121
+ // and for SSR/deterministic rendering.
1122
+ resetCalloutIdCounter();
1123
+ // v1.3.0: Pass 0 — Transform :::type[Title]{overrides} ... ::: directive syntax
1124
+ // into callouts (Starlight/Docusaurus/MkDocs content portability).
1125
+ if (options.enableDirectiveSyntax) {
1126
+ transformDirectiveSyntax(tree, configMap, options);
1127
+ }
583
1128
  // ── Pass 1: Transform blockquotes → callouts / disclosures ──────
584
1129
  // We must process DEEPEST blockquotes first (inside-out) so that
585
1130
  // nested [!] directives are converted before their parents read them.
@@ -720,5 +1265,8 @@ function isDisclosureHtml(htmlStr) {
720
1265
  // Backward-compatible alias
721
1266
  // ---------------------------------------------------------------------------
722
1267
  export const remarkCalloutBlocks = remarkRemakeBlocks;
1268
+ // Export helpers for users who supply a custom `build` function.
1269
+ // These let `build` authors reuse the plugin's escaping logic.
1270
+ export { escapeHtml, escapeAttribute, sanitizeColor };
723
1271
  export { BUILTIN_CALLOUTS };
724
1272
  //# sourceMappingURL=remark-remake-blocks.js.map