@leadertechie/md2html 0.1.0-alpha.15 → 0.1.0-alpha.17

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/dist/index.js CHANGED
@@ -181,6 +181,77 @@ class CatchAllHandler {
181
181
  };
182
182
  }
183
183
  }
184
+ class FrontmatterHandler {
185
+ constructor() {
186
+ this.type = "frontmatter";
187
+ }
188
+ handle(token, ctx) {
189
+ const raw = token.raw || "";
190
+ const lines = raw.split("\n");
191
+ const parsed = {};
192
+ let currentKey = null;
193
+ let currentArray = [];
194
+ for (const line of lines) {
195
+ const listMatch = line.match(/^\s+-\s+(.+)$/);
196
+ if (listMatch && currentKey) {
197
+ currentArray.push(listMatch[1].trim());
198
+ continue;
199
+ }
200
+ if (currentKey && currentArray.length > 0) {
201
+ parsed[currentKey] = [...currentArray];
202
+ currentArray = [];
203
+ currentKey = null;
204
+ }
205
+ const keyMatch = line.match(/^(\w[\w_-]*)\s*:\s*(.*)$/);
206
+ if (keyMatch) {
207
+ currentKey = keyMatch[1];
208
+ const val = keyMatch[2].trim();
209
+ if (val === "") {
210
+ continue;
211
+ } else if (val.startsWith("[") && val.endsWith("]")) {
212
+ parsed[currentKey] = val.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, ""));
213
+ currentKey = null;
214
+ } else {
215
+ parsed[currentKey] = val.replace(/^["']|["']$/g, "");
216
+ currentKey = null;
217
+ }
218
+ }
219
+ }
220
+ if (currentKey && currentArray.length > 0) {
221
+ parsed[currentKey] = [...currentArray];
222
+ }
223
+ if (ctx.metadata) {
224
+ Object.assign(ctx.metadata, parsed);
225
+ }
226
+ return null;
227
+ }
228
+ }
229
+ class ContainerBlockHandler {
230
+ constructor() {
231
+ this.type = "containerBlock";
232
+ }
233
+ handle(token, ctx) {
234
+ const specifier = token.specifier;
235
+ const childTokens = token.tokens;
236
+ if (!specifier) return null;
237
+ const tagMatch = specifier.match(/^(\w+)/);
238
+ const idMatch = specifier.match(/#([\w-]+)/);
239
+ const classMatches = [...specifier.matchAll(/\.([\w-]+)/g)];
240
+ const tag = tagMatch?.[1] || "div";
241
+ const id = idMatch?.[1] || "";
242
+ const classes = classMatches.map((m) => m[1]);
243
+ const children = ctx.parseTokens(childTokens, 0);
244
+ return {
245
+ type: "container",
246
+ children,
247
+ attributes: {
248
+ tag,
249
+ id: id || void 0
250
+ },
251
+ className: classes.length > 0 ? classes.join(" ") : void 0
252
+ };
253
+ }
254
+ }
184
255
  class TokenHandlerRegistry {
185
256
  constructor() {
186
257
  this.handlers = /* @__PURE__ */ new Map();
@@ -192,6 +263,8 @@ class TokenHandlerRegistry {
192
263
  this.register(new HrHandler());
193
264
  this.register(new BlockquoteHandler());
194
265
  this.register(new HtmlHandler());
266
+ this.register(new FrontmatterHandler());
267
+ this.register(new ContainerBlockHandler());
195
268
  this.catchAll = new CatchAllHandler();
196
269
  }
197
270
  /** Register a handler. Overrides any existing handler for the same token type. */
@@ -240,6 +313,7 @@ class MarkdownParser {
240
313
  ...defaultAllowedHTMLTags,
241
314
  ...options?.allowedHTMLTags ?? []
242
315
  ]);
316
+ this.allowedAttributes = options?.allowedAttributes ?? {};
243
317
  this.handlerRegistry = new TokenHandlerRegistry();
244
318
  this.onUnhandledToken = options?.onUnhandledToken;
245
319
  }
@@ -266,6 +340,65 @@ class MarkdownParser {
266
340
  return this.onSlot(name.trim());
267
341
  });
268
342
  }
343
+ /**
344
+ * Check if an attribute name is allowed for a given tag.
345
+ * Supports "data-*" wildcard prefix matching.
346
+ */
347
+ isAttributeAllowed(tagName, attrName) {
348
+ const globalAllowed = this.allowedAttributes["*"];
349
+ if (globalAllowed && this.matchesAttributeList(attrName, globalAllowed)) {
350
+ return true;
351
+ }
352
+ const tagAllowed = this.allowedAttributes[tagName];
353
+ if (tagAllowed && this.matchesAttributeList(attrName, tagAllowed)) {
354
+ return true;
355
+ }
356
+ return false;
357
+ }
358
+ /**
359
+ * Check if an attribute name matches a list of allowed patterns.
360
+ * Supports "data-*" wildcard prefix matching.
361
+ */
362
+ matchesAttributeList(attrName, allowed) {
363
+ const lower = attrName.toLowerCase();
364
+ for (const pattern of allowed) {
365
+ if (pattern.endsWith("-*")) {
366
+ const prefix = pattern.slice(0, -1).toLowerCase();
367
+ if (lower.startsWith(prefix)) return true;
368
+ } else if (pattern.toLowerCase() === lower) {
369
+ return true;
370
+ }
371
+ }
372
+ return false;
373
+ }
374
+ /**
375
+ * Filter attributes on an HTML tag, keeping only allowed ones.
376
+ */
377
+ filterTagAttributes(tag) {
378
+ const tagMatch = tag.match(/^<\/?(\w+)/);
379
+ if (!tagMatch) return tag;
380
+ const tagName = tagMatch[1].toLowerCase();
381
+ const hasRules = Object.keys(this.allowedAttributes).length > 0;
382
+ if (!hasRules) return tag;
383
+ const attrRegex = /(\S+)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/g;
384
+ const selfClosing = tag.endsWith("/>");
385
+ const tagOpen = tagMatch[0];
386
+ const remaining = tag.slice(tagOpen.length);
387
+ let filtered = tagOpen;
388
+ let match;
389
+ while ((match = attrRegex.exec(remaining)) !== null) {
390
+ const attrName = match[1];
391
+ const attrValue = match[2] ?? match[3] ?? match[4] ?? "";
392
+ if (this.isAttributeAllowed(tagName, attrName)) {
393
+ filtered += ` ${attrName}="${attrValue.replace(/"/g, '"')}"`;
394
+ }
395
+ }
396
+ if (selfClosing) {
397
+ filtered += " /";
398
+ }
399
+ filtered += ">";
400
+ return filtered;
401
+ }
269
402
  processRawHTML(html) {
270
403
  if (!this.allowedHTMLTags.has("script")) {
271
404
  html = html.replace(/<script[\s\S]*?<\/script>/gi, "");
@@ -275,6 +408,10 @@ class MarkdownParser {
275
408
  html = html.replace(/<style[\s\S]*?<\/style>/gi, "");
276
409
  html = html.replace(/<\/?style[^>]*>/gi, "");
277
410
  }
411
+ const hasAttrRules = Object.keys(this.allowedAttributes).length > 0;
412
+ if (hasAttrRules) {
413
+ html = html.replace(/<[^>]+>/g, (match) => this.filterTagAttributes(match));
414
+ }
278
415
  return html;
279
416
  }
280
417
  /**
@@ -283,6 +420,7 @@ class MarkdownParser {
283
420
  */
284
421
  createContext() {
285
422
  const self = this;
423
+ const metadata = {};
286
424
  return {
287
425
  get preserveRawHTML() {
288
426
  return self.preserveRawHTML;
@@ -300,10 +438,16 @@ class MarkdownParser {
300
438
  parseTokens: (tokens, depth) => self.parseTokens(tokens, depth),
301
439
  reportUnhandled: (type, token) => {
302
440
  self.onUnhandledToken?.(type, token);
303
- }
441
+ },
442
+ metadata
304
443
  };
305
444
  }
306
- parseTokens(tokens, depth = 0) {
445
+ /**
446
+ * Process an array of marked tokens into ContentNodes.
447
+ * When depth === 0 (root), creates a shared context that accumulates metadata.
448
+ * For recursive calls (depth > 0), creates a fresh context for each level.
449
+ */
450
+ parseTokens(tokens, depth = 0, sharedCtx) {
307
451
  if (depth > this.maxRecursionDepth) {
308
452
  const msg = `[md2html] Max recursion depth (${this.maxRecursionDepth}) exceeded, truncating`;
309
453
  if (this.errorRecovery === "warn") {
@@ -312,7 +456,7 @@ class MarkdownParser {
312
456
  return [];
313
457
  }
314
458
  const nodes = [];
315
- const ctx = this.createContext();
459
+ const ctx = sharedCtx || this.createContext();
316
460
  for (const token of tokens) {
317
461
  const typedToken = token;
318
462
  const handler = this.handlerRegistry.get(typedToken.type);
@@ -323,6 +467,82 @@ class MarkdownParser {
323
467
  }
324
468
  return nodes;
325
469
  }
470
+ /**
471
+ * Pre-process markdown: convert `:::tag#id.class` container syntax
472
+ * into HTML comment markers that marked will preserve as html tokens,
473
+ * but won't affect markdown parsing of the inner content.
474
+ *
475
+ * Example:
476
+ * :::section#header
477
+ * # Heading inside container
478
+ * Some text
479
+ * :::
480
+ *
481
+ * Becomes:
482
+ * <!-- md-container:section#header -->
483
+ * # Heading inside container
484
+ * Some text
485
+ * <!-- /md-container -->
486
+ */
487
+ preprocessContainerBlocks(markdown) {
488
+ return markdown.replace(/^:::(?:(\w+(?:[.#][\w-]+)*)\s*)?$/gm, (match, specifier) => {
489
+ if (!specifier) {
490
+ return "<!-- /md-container -->";
491
+ }
492
+ const normalized = specifier.match(/^\w/) ? specifier : `div${specifier}`;
493
+ return `<!-- md-container:${normalized} -->`;
494
+ });
495
+ }
496
+ /**
497
+ * Post-process marked tokens to collapse container block markers
498
+ * into structured containerBlock tokens with proper nesting.
499
+ *
500
+ * This handles nesting depth up to maxRecursionDepth.
501
+ */
502
+ postprocessTokens(tokens) {
503
+ const result = [];
504
+ const stack = [];
505
+ for (const token of tokens) {
506
+ const t = token;
507
+ if (t.type === "html") {
508
+ const raw = t.raw.trim();
509
+ const openMatch = raw.match(/^<!--\s*md-container:\s*(\S+)\s*-->$/);
510
+ const closeMatch = raw.match(/^<!--\s*\/md-container\s*-->$/);
511
+ if (openMatch) {
512
+ const newContainer = {
513
+ specifier: openMatch[1],
514
+ tokens: []
515
+ };
516
+ stack.push(newContainer);
517
+ continue;
518
+ }
519
+ if (closeMatch) {
520
+ if (stack.length === 0) {
521
+ continue;
522
+ }
523
+ const container = stack.pop();
524
+ const processedInner = this.postprocessTokens(container.tokens);
525
+ const containerToken = {
526
+ type: "containerBlock",
527
+ specifier: container.specifier,
528
+ tokens: processedInner
529
+ };
530
+ if (stack.length > 0) {
531
+ stack[stack.length - 1].tokens.push(containerToken);
532
+ } else {
533
+ result.push(containerToken);
534
+ }
535
+ continue;
536
+ }
537
+ }
538
+ if (stack.length > 0) {
539
+ stack[stack.length - 1].tokens.push(token);
540
+ } else {
541
+ result.push(token);
542
+ }
543
+ }
544
+ return result;
545
+ }
326
546
  parse(markdown, options) {
327
547
  const parseOptions = {
328
548
  gfm: options?.gfm ?? true,
@@ -330,10 +550,14 @@ class MarkdownParser {
330
550
  pedantic: options?.pedantic ?? false
331
551
  };
332
552
  try {
333
- const tokens = marked.lexer(markdown, parseOptions);
334
- const content = this.parseTokens(tokens);
553
+ const processed = this.preprocessContainerBlocks(markdown);
554
+ const rawTokens = marked.lexer(processed, parseOptions);
555
+ const tokens = this.postprocessTokens(rawTokens);
556
+ const ctx = this.createContext();
557
+ const content = this.parseTokens(tokens, 0, ctx);
335
558
  return {
336
559
  title: "",
560
+ metadata: { ...ctx.metadata },
337
561
  content
338
562
  };
339
563
  } catch (err) {
@@ -352,6 +576,198 @@ class MarkdownParser {
352
576
  return this.parse(markdown, options).content;
353
577
  }
354
578
  }
579
+ class HeadingRendererStrategy {
580
+ constructor() {
581
+ this.type = "heading";
582
+ }
583
+ render(node, _renderChild, ctx) {
584
+ const level = node.attributes?.level || "2";
585
+ const headingId = ctx.addHeadingIds ? ` id="${ctx.generateHeadingId(node.content)}"` : "";
586
+ const scopeAttr = ctx.getScopeAttr(node);
587
+ if (!ctx.hasClassConfig()) {
588
+ return `<h${level}${headingId}${scopeAttr}>${node.content || ""}</h${level}>`;
589
+ }
590
+ const prefix = ctx.classPrefix;
591
+ const levelClass = level === "1" ? "h1" : level === "2" ? "h2" : level === "3" ? "h3" : level === "4" ? "h4" : level === "5" ? "h5" : "h6";
592
+ const headingClass = prefix ? `${prefix}${levelClass}` : levelClass;
593
+ return `<h${level}${headingId}${scopeAttr} class="${headingClass}">${node.content || ""}</h${level}>`;
594
+ }
595
+ }
596
+ class ParagraphRendererStrategy {
597
+ constructor() {
598
+ this.type = "paragraph";
599
+ }
600
+ render(node, renderChild, ctx) {
601
+ const scopeAttr = ctx.getScopeAttr(node);
602
+ if (node.children) {
603
+ const childrenHtml = node.children.map(renderChild).join("");
604
+ return ctx.hasClassConfig() && ctx.classPrefix ? `<p class="${ctx.classPrefix}paragraph"${scopeAttr}>${childrenHtml}</p>` : `<p${scopeAttr}>${childrenHtml}</p>`;
605
+ }
606
+ return ctx.hasClassConfig() && ctx.classPrefix ? `<p class="${ctx.classPrefix}paragraph"${scopeAttr}>${node.content || ""}</p>` : `<p${scopeAttr}>${node.content || ""}</p>`;
607
+ }
608
+ }
609
+ class ListRendererStrategy {
610
+ constructor() {
611
+ this.type = "list";
612
+ }
613
+ render(node, renderChild, ctx) {
614
+ const tag = node.ordered ? "ol" : "ul";
615
+ const items = node.children?.map(renderChild).join("") || "";
616
+ const scopeAttr = ctx.getScopeAttr(node);
617
+ return ctx.hasClassConfig() && ctx.classPrefix ? `<${tag} class="${ctx.classPrefix}list"${scopeAttr}>${items}</${tag}>` : `<${tag}${scopeAttr}>${items}</${tag}>`;
618
+ }
619
+ }
620
+ class ListItemRendererStrategy {
621
+ constructor() {
622
+ this.type = "list-item";
623
+ }
624
+ render(node, _renderChild, ctx) {
625
+ const scopeAttr = ctx.getScopeAttr(node);
626
+ return ctx.hasClassConfig() && ctx.classPrefix ? `<li class="${ctx.classPrefix}list-item"${scopeAttr}>${node.content || ""}</li>` : `<li${scopeAttr}>${node.content || ""}</li>`;
627
+ }
628
+ }
629
+ class ImageRendererStrategy {
630
+ constructor() {
631
+ this.type = "image";
632
+ }
633
+ render(node, _renderChild, ctx) {
634
+ const src = node.src || node.attributes?.src || "";
635
+ const alt = node.alt || node.attributes?.alt || "";
636
+ const scopeAttr = ctx.getScopeAttr(node);
637
+ let classStr = "";
638
+ if (ctx.hasClassConfig()) {
639
+ const prefix = ctx.classPrefix;
640
+ classStr = prefix ? `${prefix}image` : "image";
641
+ if (node.className) classStr += ` ${node.className}`;
642
+ return `<img src="${src}" alt="${alt}" class="${classStr}"${scopeAttr}>`;
643
+ }
644
+ if (node.className) {
645
+ return `<img src="${src}" alt="${alt}" class="${node.className}"${scopeAttr}>`;
646
+ }
647
+ return `<img src="${src}" alt="${alt}"${scopeAttr}>`;
648
+ }
649
+ }
650
+ class CodeRendererStrategy {
651
+ constructor() {
652
+ this.type = "code";
653
+ }
654
+ render(node, _renderChild, ctx) {
655
+ const scopeAttr = ctx.getScopeAttr(node);
656
+ const lang = node.attributes?.lang || "";
657
+ if (ctx.hasClassConfig()) {
658
+ const prefix = ctx.classPrefix;
659
+ const codeClass = prefix ? `${prefix}code` : "code";
660
+ return `<pre${scopeAttr}><code class="${codeClass} language-${lang}">${node.content || ""}</code></pre>`;
661
+ }
662
+ return `<pre${scopeAttr}><code class="language-${lang}">${node.content || ""}</code></pre>`;
663
+ }
664
+ }
665
+ class ContainerRendererStrategy {
666
+ constructor() {
667
+ this.type = "container";
668
+ }
669
+ render(node, renderChild, ctx) {
670
+ if (node.rawHTML) {
671
+ return node.rawHTML;
672
+ }
673
+ const tag = node.attributes?.tag || "div";
674
+ const children = node.children?.map(renderChild).join("") || "";
675
+ const id = node.attributes?.id;
676
+ const idAttr = id ? ` id="${id}"` : "";
677
+ const scopeAttr = ctx.getScopeAttr(node);
678
+ if (tag === "hr") return "<hr>";
679
+ if (ctx.hasClassConfig()) {
680
+ const containerClass = ctx.getContainerClass(tag);
681
+ const prefix = ctx.classPrefix;
682
+ if (prefix) {
683
+ const classes2 = [prefix + (containerClass || "container")];
684
+ if (node.className) classes2.push(node.className);
685
+ return `<${tag} class="${classes2.join(" ")}"${idAttr}${scopeAttr}>${children}</${tag}>`;
686
+ }
687
+ const classes = [containerClass || "container"];
688
+ if (node.className) classes.push(node.className);
689
+ return `<${tag} class="${classes.join(" ")}"${idAttr}${scopeAttr}>${children}</${tag}>`;
690
+ }
691
+ if (node.className) {
692
+ return `<${tag} class="${node.className}"${idAttr}${scopeAttr}>${children}</${tag}>`;
693
+ }
694
+ return `<${tag}${idAttr}${scopeAttr}>${children}</${tag}>`;
695
+ }
696
+ }
697
+ class StrongRendererStrategy {
698
+ constructor() {
699
+ this.type = "strong";
700
+ }
701
+ render(node, _renderChild, ctx) {
702
+ return `<strong${ctx.getScopeAttr(node)}>${node.content || ""}</strong>`;
703
+ }
704
+ }
705
+ class EmphasisRendererStrategy {
706
+ constructor() {
707
+ this.type = "emphasis";
708
+ }
709
+ render(node, _renderChild, ctx) {
710
+ return `<em${ctx.getScopeAttr(node)}>${node.content || ""}</em>`;
711
+ }
712
+ }
713
+ class LinkRendererStrategy {
714
+ constructor() {
715
+ this.type = "link";
716
+ }
717
+ render(node, _renderChild, ctx) {
718
+ const href = node.attributes?.href || "";
719
+ return `<a href="${href}"${ctx.getScopeAttr(node)}>${node.content || ""}</a>`;
720
+ }
721
+ }
722
+ class TextRendererStrategy {
723
+ constructor() {
724
+ this.type = "text";
725
+ }
726
+ render(node, _renderChild, _ctx) {
727
+ return node.content || "";
728
+ }
729
+ }
730
+ class RendererStrategyRegistry {
731
+ constructor() {
732
+ this.strategies = /* @__PURE__ */ new Map();
733
+ this.register(new HeadingRendererStrategy());
734
+ this.register(new ParagraphRendererStrategy());
735
+ this.register(new ListRendererStrategy());
736
+ this.register(new ListItemRendererStrategy());
737
+ this.register(new ImageRendererStrategy());
738
+ this.register(new CodeRendererStrategy());
739
+ this.register(new ContainerRendererStrategy());
740
+ this.register(new StrongRendererStrategy());
741
+ this.register(new EmphasisRendererStrategy());
742
+ this.register(new LinkRendererStrategy());
743
+ this.register(new TextRendererStrategy());
744
+ this.fallback = new TextRendererStrategy();
745
+ }
746
+ /** Register a strategy for a node type. Overrides any existing strategy. */
747
+ register(strategy) {
748
+ this.strategies.set(strategy.type, strategy);
749
+ }
750
+ /** Unregister a strategy by node type. */
751
+ unregister(type) {
752
+ this.strategies.delete(type);
753
+ }
754
+ /** Get a strategy for the given node type, falling back to catch-all. */
755
+ get(type) {
756
+ return this.strategies.get(type) ?? this.fallback;
757
+ }
758
+ /** Check if a dedicated strategy exists for the given node type. */
759
+ has(type) {
760
+ return this.strategies.has(type);
761
+ }
762
+ /** Get all registered dedicated strategy types. */
763
+ get types() {
764
+ return Array.from(this.strategies.keys());
765
+ }
766
+ /** Replace the fallback strategy. */
767
+ setFallback(strategy) {
768
+ this.fallback = strategy;
769
+ }
770
+ }
355
771
  class HTMLRenderer {
356
772
  constructor(config = {}) {
357
773
  this.config = {
@@ -360,6 +776,11 @@ class HTMLRenderer {
360
776
  addHeadingIds: config.addHeadingIds ?? false,
361
777
  emitScopeAnchors: config.emitScopeAnchors ?? false
362
778
  };
779
+ this.strategyRegistry = new RendererStrategyRegistry();
780
+ }
781
+ /** Access the strategy registry for customization. */
782
+ get strategies() {
783
+ return this.strategyRegistry;
363
784
  }
364
785
  hasClassConfig() {
365
786
  return this.config.classPrefix !== "" || this.config.addHeadingIds;
@@ -386,68 +807,40 @@ class HTMLRenderer {
386
807
  const scopeValue = node.scope || nodeTypeToScope[node.type] || "container";
387
808
  return ` data-md-scope="${scopeValue}"`;
388
809
  }
389
- renderWithClass(tag, content, baseClass, nodeClass, extraAttrs) {
390
- const classAttr = this.hasClassConfig() && baseClass ? ` class="${this.getClass(baseClass, nodeClass)}"` : "";
391
- return `<${tag}${classAttr}${extraAttrs || ""}>${content}</${tag}>`;
810
+ /**
811
+ * Get the CSS class for a container's tag-based rendering.
812
+ * Returns just the tag name since renderWithClass applies the prefix.
813
+ */
814
+ getContainerClass(tag) {
815
+ if (!this.hasClassConfig()) return "";
816
+ return tag;
817
+ }
818
+ buildRenderContext() {
819
+ const self = this;
820
+ return {
821
+ get classPrefix() {
822
+ return self.config.classPrefix;
823
+ },
824
+ get addHeadingIds() {
825
+ return self.config.addHeadingIds;
826
+ },
827
+ get emitScopeAnchors() {
828
+ return self.config.emitScopeAnchors;
829
+ },
830
+ get customCSS() {
831
+ return self.config.customCSS;
832
+ },
833
+ hasClassConfig: () => self.hasClassConfig(),
834
+ getClass: (baseClass, nodeClass) => self.getClass(baseClass, nodeClass),
835
+ getScopeAttr: (node) => self.getScopeAttr(node),
836
+ generateHeadingId: (content) => self.generateHeadingId(content),
837
+ getContainerClass: (tag) => self.getContainerClass(tag)
838
+ };
392
839
  }
393
840
  renderNode(node) {
394
- const scopeAttr = this.getScopeAttr(node);
395
- switch (node.type) {
396
- case "heading":
397
- const level = node.attributes?.level || "2";
398
- const headingId = this.config.addHeadingIds ? ` id="${this.generateHeadingId(node.content)}"` : "";
399
- let headingClass = "";
400
- if (this.hasClassConfig()) {
401
- const prefix = this.config.classPrefix;
402
- const levelClass = level === "1" ? "h1" : level === "2" ? "h2" : level === "3" ? "h3" : level === "4" ? "h4" : level === "5" ? "h5" : "h6";
403
- headingClass = prefix ? `${prefix}${levelClass}` : levelClass;
404
- }
405
- if (!headingClass) {
406
- return `<h${level}${headingId}${scopeAttr}>${node.content || ""}</h${level}>`;
407
- }
408
- return `<h${level}${headingId}${scopeAttr} class="${headingClass}">${node.content || ""}</h${level}>`;
409
- case "paragraph":
410
- if (node.children) {
411
- const childrenHtml = node.children.map((child) => this.renderNode(child)).join("");
412
- return this.renderWithClass("p", childrenHtml, "paragraph", void 0, scopeAttr);
413
- }
414
- return this.renderWithClass("p", node.content || "", "paragraph", void 0, scopeAttr);
415
- case "list":
416
- const tag = node.ordered ? "ol" : "ul";
417
- const items = node.children?.map((child) => this.renderNode(child)).join("") || "";
418
- return this.renderWithClass(tag, items, "list", void 0, scopeAttr);
419
- case "list-item":
420
- return this.renderWithClass("li", node.content || "", "list-item", void 0, scopeAttr);
421
- case "image":
422
- const src = node.src || node.attributes?.src || "";
423
- const alt = node.alt || node.attributes?.alt || "";
424
- const classStr = this.getClass("image", node.className || void 0);
425
- return `<img src="${src}" alt="${alt}"${classStr ? ` class="${classStr}"` : ""}${scopeAttr}>`;
426
- case "code":
427
- const codeClass = this.hasClassConfig() ? ` class="${this.getClass("code")} language-${node.attributes?.lang || ""}"` : ` class="language-${node.attributes?.lang || ""}"`;
428
- return `<pre${scopeAttr}><code${codeClass}>${node.content || ""}</code></pre>`;
429
- case "container":
430
- if (node.attributes?.tag === "hr") return "<hr>";
431
- if (node.attributes?.tag === "blockquote") {
432
- const children = node.children?.map((child) => this.renderNode(child)).join("") || "";
433
- return this.renderWithClass("blockquote", children, "blockquote", void 0, scopeAttr);
434
- }
435
- if (node.rawHTML) {
436
- return node.rawHTML;
437
- }
438
- const containerChildren = node.children?.map((child) => this.renderNode(child)).join("") || "";
439
- return this.renderWithClass("div", containerChildren, "container", node.className || void 0, scopeAttr);
440
- case "strong":
441
- return `<strong${scopeAttr}>${node.content || ""}</strong>`;
442
- case "emphasis":
443
- return `<em${scopeAttr}>${node.content || ""}</em>`;
444
- case "link":
445
- const href = node.attributes?.href || "";
446
- return `<a href="${href}"${scopeAttr}>${node.content || ""}</a>`;
447
- case "text":
448
- default:
449
- return node.content || "";
450
- }
841
+ const ctx = this.buildRenderContext();
842
+ const strategy = this.strategyRegistry.get(node.type);
843
+ return strategy.render(node, (child) => this.renderNode(child), ctx);
451
844
  }
452
845
  renderNodes(nodes) {
453
846
  if (!nodes || nodes.length === 0) {
@@ -492,7 +885,8 @@ class MarkdownPipeline {
492
885
  onSlot: config.onSlot,
493
886
  errorRecovery: config.errorRecovery ?? "throw",
494
887
  maxRecursionDepth: config.maxRecursionDepth ?? 100,
495
- allowedHTMLTags: config.allowedHTMLTags ?? []
888
+ allowedHTMLTags: config.allowedHTMLTags ?? [],
889
+ allowedAttributes: config.allowedAttributes ?? {}
496
890
  };
497
891
  this.parser = new MarkdownParser({
498
892
  imagePathPrefix: this.config.imagePathPrefix,
@@ -502,7 +896,8 @@ class MarkdownPipeline {
502
896
  onSlot: this.config.onSlot,
503
897
  errorRecovery: this.config.errorRecovery,
504
898
  maxRecursionDepth: this.config.maxRecursionDepth,
505
- allowedHTMLTags: this.config.allowedHTMLTags
899
+ allowedHTMLTags: this.config.allowedHTMLTags,
900
+ allowedAttributes: this.config.allowedAttributes
506
901
  });
507
902
  this.renderer = new HTMLRenderer(this.config.styleOptions);
508
903
  }
@@ -540,10 +935,130 @@ class MarkdownPipeline {
540
935
  return this.renderer.getCustomCSS();
541
936
  }
542
937
  }
938
+ function walkTree(root, visitor) {
939
+ return walkNode(root, null, 0, visitor);
940
+ }
941
+ function walkNode(node, parent, depth, visitor) {
942
+ let processed = visitor.enter ? visitor.enter(node, parent, depth) : node;
943
+ if (processed === null) return null;
944
+ if (processed.children && processed.children.length > 0) {
945
+ processed = {
946
+ ...processed,
947
+ children: processed.children.map((child) => walkNode(child, processed, depth + 1, visitor)).filter(Boolean)
948
+ };
949
+ }
950
+ processed = visitor.exit ? visitor.exit(processed, parent, depth) : processed;
951
+ if (processed === null) return null;
952
+ return processed;
953
+ }
954
+ function collectFromTree(root, collector) {
955
+ const results = [];
956
+ collectNode(root, null, 0, collector, results);
957
+ return results;
958
+ }
959
+ function collectNode(node, parent, depth, collector, results) {
960
+ const result = collector(node, parent, depth);
961
+ if (result !== null) {
962
+ results.push(result);
963
+ }
964
+ if (node.children) {
965
+ for (const child of node.children) {
966
+ collectNode(child, node, depth + 1, collector, results);
967
+ }
968
+ }
969
+ }
970
+ function collectByType(root, type) {
971
+ return collectFromTree(
972
+ root,
973
+ (node) => node.type === type ? node : null
974
+ );
975
+ }
976
+ function collectHeadings(root) {
977
+ return collectFromTree(root, (node) => {
978
+ if (node.type === "heading") {
979
+ return {
980
+ level: node.attributes?.level || "2",
981
+ text: node.content || "",
982
+ id: node.attributes?.id
983
+ };
984
+ }
985
+ return null;
986
+ });
987
+ }
988
+ function collectImages(root) {
989
+ return collectFromTree(root, (node) => {
990
+ if (node.type === "image") {
991
+ return { src: node.src || "", alt: node.alt || "" };
992
+ }
993
+ return null;
994
+ });
995
+ }
996
+ class NodeFactory {
997
+ static heading(content, attributes) {
998
+ return {
999
+ type: "heading",
1000
+ content,
1001
+ attributes: { level: attributes?.level || "2", ...attributes }
1002
+ };
1003
+ }
1004
+ static paragraph(contentOrChildren, children) {
1005
+ if (typeof contentOrChildren === "string") {
1006
+ return { type: "paragraph", content: contentOrChildren };
1007
+ }
1008
+ return { type: "paragraph", children: contentOrChildren ?? children };
1009
+ }
1010
+ static list(items, ordered, attributes) {
1011
+ return {
1012
+ type: "list",
1013
+ ordered: ordered ?? false,
1014
+ children: items,
1015
+ ...attributes ? { attributes } : {}
1016
+ };
1017
+ }
1018
+ static listItem(content) {
1019
+ return { type: "list-item", content };
1020
+ }
1021
+ static image(src, alt, className) {
1022
+ return { type: "image", src, alt: alt || "", ...className ? { className } : {} };
1023
+ }
1024
+ static code(content, lang) {
1025
+ return {
1026
+ type: "code",
1027
+ content,
1028
+ attributes: { lang: lang || "" }
1029
+ };
1030
+ }
1031
+ static container(children, config) {
1032
+ const node = { type: "container" };
1033
+ if (children && children.length > 0) node.children = children;
1034
+ if (config?.rawHTML) node.rawHTML = config.rawHTML;
1035
+ if (config?.scope) node.scope = config.scope;
1036
+ const attrs = {};
1037
+ if (config?.tag) attrs.tag = config.tag;
1038
+ if (config?.id) attrs.id = config.id;
1039
+ if (Object.keys(attrs).length > 0) node.attributes = attrs;
1040
+ if (config?.className) node.className = config.className;
1041
+ return node;
1042
+ }
1043
+ static text(content) {
1044
+ return { type: "text", content };
1045
+ }
1046
+ static strong(content) {
1047
+ return { type: "strong", content };
1048
+ }
1049
+ static emphasis(content) {
1050
+ return { type: "emphasis", content };
1051
+ }
1052
+ static link(href, content) {
1053
+ return { type: "link", content, attributes: { href } };
1054
+ }
1055
+ }
543
1056
  export {
544
1057
  BlockquoteHandler,
545
1058
  CatchAllHandler,
546
1059
  CodeHandler,
1060
+ ContainerBlockHandler,
1061
+ FrontmatterHandler,
547
1062
  HTMLRenderer,
548
1063
  HeadingHandler,
549
1064
  HrHandler,
@@ -552,9 +1067,16 @@ export {
552
1067
  ListHandler,
553
1068
  MarkdownParser,
554
1069
  MarkdownPipeline,
1070
+ NodeFactory,
555
1071
  ParagraphHandler,
1072
+ RendererStrategyRegistry,
556
1073
  TokenHandlerRegistry,
1074
+ collectByType,
1075
+ collectFromTree,
1076
+ collectHeadings,
1077
+ collectImages,
557
1078
  defaultAllowedHTMLTags,
558
- nodeTypeToScope
1079
+ nodeTypeToScope,
1080
+ walkTree
559
1081
  };
560
1082
  //# sourceMappingURL=index.js.map