@leadertechie/md2html 0.1.0-alpha.16 → 0.1.0-alpha.18
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.d.ts +237 -2
- package/dist/index.js +554 -67
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -55,8 +55,9 @@ class ParagraphHandler {
|
|
|
55
55
|
handle(token, ctx) {
|
|
56
56
|
const tokens = token.tokens || [];
|
|
57
57
|
const hasInlineImage = tokens.some((t) => t.type === "image");
|
|
58
|
+
const hasInlineLink = tokens.some((t) => t.type === "link");
|
|
58
59
|
const hasInlineHTML = tokens.some((t) => t.type === "html");
|
|
59
|
-
if (hasInlineImage || ctx.preserveRawHTML && hasInlineHTML) {
|
|
60
|
+
if (hasInlineImage || hasInlineLink || ctx.preserveRawHTML && hasInlineHTML) {
|
|
60
61
|
const children = tokens.map((t) => {
|
|
61
62
|
if (t.type === "image") {
|
|
62
63
|
return {
|
|
@@ -65,6 +66,16 @@ class ParagraphHandler {
|
|
|
65
66
|
alt: t.text || ""
|
|
66
67
|
};
|
|
67
68
|
}
|
|
69
|
+
if (t.type === "link") {
|
|
70
|
+
return {
|
|
71
|
+
type: "link",
|
|
72
|
+
content: ctx.processSlots(ctx.processInlineFormatting(t.text || "")),
|
|
73
|
+
attributes: {
|
|
74
|
+
href: t.href || "",
|
|
75
|
+
...t.title ? { title: t.title } : {}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
68
79
|
if (t.type === "html" && ctx.preserveRawHTML) {
|
|
69
80
|
const processed = ctx.processRawHTML(t.raw);
|
|
70
81
|
if (processed.trim()) {
|
|
@@ -161,6 +172,24 @@ class HtmlHandler {
|
|
|
161
172
|
return { type: "container", content: token.raw };
|
|
162
173
|
}
|
|
163
174
|
}
|
|
175
|
+
class LinkHandler {
|
|
176
|
+
constructor() {
|
|
177
|
+
this.type = "link";
|
|
178
|
+
}
|
|
179
|
+
handle(token, ctx) {
|
|
180
|
+
const text = token.text || "";
|
|
181
|
+
const href = token.href || "";
|
|
182
|
+
const title = token.title || "";
|
|
183
|
+
return {
|
|
184
|
+
type: "link",
|
|
185
|
+
content: ctx.processSlots(ctx.processInlineFormatting(text)),
|
|
186
|
+
attributes: {
|
|
187
|
+
href,
|
|
188
|
+
...title ? { title } : {}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
164
193
|
class CatchAllHandler {
|
|
165
194
|
constructor() {
|
|
166
195
|
this.type = "*";
|
|
@@ -181,6 +210,77 @@ class CatchAllHandler {
|
|
|
181
210
|
};
|
|
182
211
|
}
|
|
183
212
|
}
|
|
213
|
+
class FrontmatterHandler {
|
|
214
|
+
constructor() {
|
|
215
|
+
this.type = "frontmatter";
|
|
216
|
+
}
|
|
217
|
+
handle(token, ctx) {
|
|
218
|
+
const raw = token.raw || "";
|
|
219
|
+
const lines = raw.split("\n");
|
|
220
|
+
const parsed = {};
|
|
221
|
+
let currentKey = null;
|
|
222
|
+
let currentArray = [];
|
|
223
|
+
for (const line of lines) {
|
|
224
|
+
const listMatch = line.match(/^\s+-\s+(.+)$/);
|
|
225
|
+
if (listMatch && currentKey) {
|
|
226
|
+
currentArray.push(listMatch[1].trim());
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (currentKey && currentArray.length > 0) {
|
|
230
|
+
parsed[currentKey] = [...currentArray];
|
|
231
|
+
currentArray = [];
|
|
232
|
+
currentKey = null;
|
|
233
|
+
}
|
|
234
|
+
const keyMatch = line.match(/^(\w[\w_-]*)\s*:\s*(.*)$/);
|
|
235
|
+
if (keyMatch) {
|
|
236
|
+
currentKey = keyMatch[1];
|
|
237
|
+
const val = keyMatch[2].trim();
|
|
238
|
+
if (val === "") {
|
|
239
|
+
continue;
|
|
240
|
+
} else if (val.startsWith("[") && val.endsWith("]")) {
|
|
241
|
+
parsed[currentKey] = val.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, ""));
|
|
242
|
+
currentKey = null;
|
|
243
|
+
} else {
|
|
244
|
+
parsed[currentKey] = val.replace(/^["']|["']$/g, "");
|
|
245
|
+
currentKey = null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (currentKey && currentArray.length > 0) {
|
|
250
|
+
parsed[currentKey] = [...currentArray];
|
|
251
|
+
}
|
|
252
|
+
if (ctx.metadata) {
|
|
253
|
+
Object.assign(ctx.metadata, parsed);
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
class ContainerBlockHandler {
|
|
259
|
+
constructor() {
|
|
260
|
+
this.type = "containerBlock";
|
|
261
|
+
}
|
|
262
|
+
handle(token, ctx) {
|
|
263
|
+
const specifier = token.specifier;
|
|
264
|
+
const childTokens = token.tokens;
|
|
265
|
+
if (!specifier) return null;
|
|
266
|
+
const tagMatch = specifier.match(/^(\w+)/);
|
|
267
|
+
const idMatch = specifier.match(/#([\w-]+)/);
|
|
268
|
+
const classMatches = [...specifier.matchAll(/\.([\w-]+)/g)];
|
|
269
|
+
const tag = tagMatch?.[1] || "div";
|
|
270
|
+
const id = idMatch?.[1] || "";
|
|
271
|
+
const classes = classMatches.map((m) => m[1]);
|
|
272
|
+
const children = ctx.parseTokens(childTokens, 0);
|
|
273
|
+
return {
|
|
274
|
+
type: "container",
|
|
275
|
+
children,
|
|
276
|
+
attributes: {
|
|
277
|
+
tag,
|
|
278
|
+
id: id || void 0
|
|
279
|
+
},
|
|
280
|
+
className: classes.length > 0 ? classes.join(" ") : void 0
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
184
284
|
class TokenHandlerRegistry {
|
|
185
285
|
constructor() {
|
|
186
286
|
this.handlers = /* @__PURE__ */ new Map();
|
|
@@ -192,6 +292,9 @@ class TokenHandlerRegistry {
|
|
|
192
292
|
this.register(new HrHandler());
|
|
193
293
|
this.register(new BlockquoteHandler());
|
|
194
294
|
this.register(new HtmlHandler());
|
|
295
|
+
this.register(new FrontmatterHandler());
|
|
296
|
+
this.register(new ContainerBlockHandler());
|
|
297
|
+
this.register(new LinkHandler());
|
|
195
298
|
this.catchAll = new CatchAllHandler();
|
|
196
299
|
}
|
|
197
300
|
/** Register a handler. Overrides any existing handler for the same token type. */
|
|
@@ -347,6 +450,7 @@ class MarkdownParser {
|
|
|
347
450
|
*/
|
|
348
451
|
createContext() {
|
|
349
452
|
const self = this;
|
|
453
|
+
const metadata = {};
|
|
350
454
|
return {
|
|
351
455
|
get preserveRawHTML() {
|
|
352
456
|
return self.preserveRawHTML;
|
|
@@ -364,10 +468,16 @@ class MarkdownParser {
|
|
|
364
468
|
parseTokens: (tokens, depth) => self.parseTokens(tokens, depth),
|
|
365
469
|
reportUnhandled: (type, token) => {
|
|
366
470
|
self.onUnhandledToken?.(type, token);
|
|
367
|
-
}
|
|
471
|
+
},
|
|
472
|
+
metadata
|
|
368
473
|
};
|
|
369
474
|
}
|
|
370
|
-
|
|
475
|
+
/**
|
|
476
|
+
* Process an array of marked tokens into ContentNodes.
|
|
477
|
+
* When depth === 0 (root), creates a shared context that accumulates metadata.
|
|
478
|
+
* For recursive calls (depth > 0), creates a fresh context for each level.
|
|
479
|
+
*/
|
|
480
|
+
parseTokens(tokens, depth = 0, sharedCtx) {
|
|
371
481
|
if (depth > this.maxRecursionDepth) {
|
|
372
482
|
const msg = `[md2html] Max recursion depth (${this.maxRecursionDepth}) exceeded, truncating`;
|
|
373
483
|
if (this.errorRecovery === "warn") {
|
|
@@ -376,7 +486,7 @@ class MarkdownParser {
|
|
|
376
486
|
return [];
|
|
377
487
|
}
|
|
378
488
|
const nodes = [];
|
|
379
|
-
const ctx = this.createContext();
|
|
489
|
+
const ctx = sharedCtx || this.createContext();
|
|
380
490
|
for (const token of tokens) {
|
|
381
491
|
const typedToken = token;
|
|
382
492
|
const handler = this.handlerRegistry.get(typedToken.type);
|
|
@@ -387,6 +497,82 @@ class MarkdownParser {
|
|
|
387
497
|
}
|
|
388
498
|
return nodes;
|
|
389
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* Pre-process markdown: convert `:::tag#id.class` container syntax
|
|
502
|
+
* into HTML comment markers that marked will preserve as html tokens,
|
|
503
|
+
* but won't affect markdown parsing of the inner content.
|
|
504
|
+
*
|
|
505
|
+
* Example:
|
|
506
|
+
* :::section#header
|
|
507
|
+
* # Heading inside container
|
|
508
|
+
* Some text
|
|
509
|
+
* :::
|
|
510
|
+
*
|
|
511
|
+
* Becomes:
|
|
512
|
+
* <!-- md-container:section#header -->
|
|
513
|
+
* # Heading inside container
|
|
514
|
+
* Some text
|
|
515
|
+
* <!-- /md-container -->
|
|
516
|
+
*/
|
|
517
|
+
preprocessContainerBlocks(markdown) {
|
|
518
|
+
return markdown.replace(/^:::(?:(\w+(?:[.#][\w-]+)*)\s*)?$/gm, (match, specifier) => {
|
|
519
|
+
if (!specifier) {
|
|
520
|
+
return "<!-- /md-container -->";
|
|
521
|
+
}
|
|
522
|
+
const normalized = specifier.match(/^\w/) ? specifier : `div${specifier}`;
|
|
523
|
+
return `<!-- md-container:${normalized} -->`;
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Post-process marked tokens to collapse container block markers
|
|
528
|
+
* into structured containerBlock tokens with proper nesting.
|
|
529
|
+
*
|
|
530
|
+
* This handles nesting depth up to maxRecursionDepth.
|
|
531
|
+
*/
|
|
532
|
+
postprocessTokens(tokens) {
|
|
533
|
+
const result = [];
|
|
534
|
+
const stack = [];
|
|
535
|
+
for (const token of tokens) {
|
|
536
|
+
const t = token;
|
|
537
|
+
if (t.type === "html") {
|
|
538
|
+
const raw = t.raw.trim();
|
|
539
|
+
const openMatch = raw.match(/^<!--\s*md-container:\s*(\S+)\s*-->$/);
|
|
540
|
+
const closeMatch = raw.match(/^<!--\s*\/md-container\s*-->$/);
|
|
541
|
+
if (openMatch) {
|
|
542
|
+
const newContainer = {
|
|
543
|
+
specifier: openMatch[1],
|
|
544
|
+
tokens: []
|
|
545
|
+
};
|
|
546
|
+
stack.push(newContainer);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (closeMatch) {
|
|
550
|
+
if (stack.length === 0) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const container = stack.pop();
|
|
554
|
+
const processedInner = this.postprocessTokens(container.tokens);
|
|
555
|
+
const containerToken = {
|
|
556
|
+
type: "containerBlock",
|
|
557
|
+
specifier: container.specifier,
|
|
558
|
+
tokens: processedInner
|
|
559
|
+
};
|
|
560
|
+
if (stack.length > 0) {
|
|
561
|
+
stack[stack.length - 1].tokens.push(containerToken);
|
|
562
|
+
} else {
|
|
563
|
+
result.push(containerToken);
|
|
564
|
+
}
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (stack.length > 0) {
|
|
569
|
+
stack[stack.length - 1].tokens.push(token);
|
|
570
|
+
} else {
|
|
571
|
+
result.push(token);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
390
576
|
parse(markdown, options) {
|
|
391
577
|
const parseOptions = {
|
|
392
578
|
gfm: options?.gfm ?? true,
|
|
@@ -394,10 +580,14 @@ class MarkdownParser {
|
|
|
394
580
|
pedantic: options?.pedantic ?? false
|
|
395
581
|
};
|
|
396
582
|
try {
|
|
397
|
-
const
|
|
398
|
-
const
|
|
583
|
+
const processed = this.preprocessContainerBlocks(markdown);
|
|
584
|
+
const rawTokens = marked.lexer(processed, parseOptions);
|
|
585
|
+
const tokens = this.postprocessTokens(rawTokens);
|
|
586
|
+
const ctx = this.createContext();
|
|
587
|
+
const content = this.parseTokens(tokens, 0, ctx);
|
|
399
588
|
return {
|
|
400
589
|
title: "",
|
|
590
|
+
metadata: { ...ctx.metadata },
|
|
401
591
|
content
|
|
402
592
|
};
|
|
403
593
|
} catch (err) {
|
|
@@ -416,6 +606,198 @@ class MarkdownParser {
|
|
|
416
606
|
return this.parse(markdown, options).content;
|
|
417
607
|
}
|
|
418
608
|
}
|
|
609
|
+
class HeadingRendererStrategy {
|
|
610
|
+
constructor() {
|
|
611
|
+
this.type = "heading";
|
|
612
|
+
}
|
|
613
|
+
render(node, _renderChild, ctx) {
|
|
614
|
+
const level = node.attributes?.level || "2";
|
|
615
|
+
const headingId = ctx.addHeadingIds ? ` id="${ctx.generateHeadingId(node.content)}"` : "";
|
|
616
|
+
const scopeAttr = ctx.getScopeAttr(node);
|
|
617
|
+
if (!ctx.hasClassConfig()) {
|
|
618
|
+
return `<h${level}${headingId}${scopeAttr}>${node.content || ""}</h${level}>`;
|
|
619
|
+
}
|
|
620
|
+
const prefix = ctx.classPrefix;
|
|
621
|
+
const levelClass = level === "1" ? "h1" : level === "2" ? "h2" : level === "3" ? "h3" : level === "4" ? "h4" : level === "5" ? "h5" : "h6";
|
|
622
|
+
const headingClass = prefix ? `${prefix}${levelClass}` : levelClass;
|
|
623
|
+
return `<h${level}${headingId}${scopeAttr} class="${headingClass}">${node.content || ""}</h${level}>`;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
class ParagraphRendererStrategy {
|
|
627
|
+
constructor() {
|
|
628
|
+
this.type = "paragraph";
|
|
629
|
+
}
|
|
630
|
+
render(node, renderChild, ctx) {
|
|
631
|
+
const scopeAttr = ctx.getScopeAttr(node);
|
|
632
|
+
if (node.children) {
|
|
633
|
+
const childrenHtml = node.children.map(renderChild).join("");
|
|
634
|
+
return ctx.hasClassConfig() && ctx.classPrefix ? `<p class="${ctx.classPrefix}paragraph"${scopeAttr}>${childrenHtml}</p>` : `<p${scopeAttr}>${childrenHtml}</p>`;
|
|
635
|
+
}
|
|
636
|
+
return ctx.hasClassConfig() && ctx.classPrefix ? `<p class="${ctx.classPrefix}paragraph"${scopeAttr}>${node.content || ""}</p>` : `<p${scopeAttr}>${node.content || ""}</p>`;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
class ListRendererStrategy {
|
|
640
|
+
constructor() {
|
|
641
|
+
this.type = "list";
|
|
642
|
+
}
|
|
643
|
+
render(node, renderChild, ctx) {
|
|
644
|
+
const tag = node.ordered ? "ol" : "ul";
|
|
645
|
+
const items = node.children?.map(renderChild).join("") || "";
|
|
646
|
+
const scopeAttr = ctx.getScopeAttr(node);
|
|
647
|
+
return ctx.hasClassConfig() && ctx.classPrefix ? `<${tag} class="${ctx.classPrefix}list"${scopeAttr}>${items}</${tag}>` : `<${tag}${scopeAttr}>${items}</${tag}>`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
class ListItemRendererStrategy {
|
|
651
|
+
constructor() {
|
|
652
|
+
this.type = "list-item";
|
|
653
|
+
}
|
|
654
|
+
render(node, _renderChild, ctx) {
|
|
655
|
+
const scopeAttr = ctx.getScopeAttr(node);
|
|
656
|
+
return ctx.hasClassConfig() && ctx.classPrefix ? `<li class="${ctx.classPrefix}list-item"${scopeAttr}>${node.content || ""}</li>` : `<li${scopeAttr}>${node.content || ""}</li>`;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
class ImageRendererStrategy {
|
|
660
|
+
constructor() {
|
|
661
|
+
this.type = "image";
|
|
662
|
+
}
|
|
663
|
+
render(node, _renderChild, ctx) {
|
|
664
|
+
const src = node.src || node.attributes?.src || "";
|
|
665
|
+
const alt = node.alt || node.attributes?.alt || "";
|
|
666
|
+
const scopeAttr = ctx.getScopeAttr(node);
|
|
667
|
+
let classStr = "";
|
|
668
|
+
if (ctx.hasClassConfig()) {
|
|
669
|
+
const prefix = ctx.classPrefix;
|
|
670
|
+
classStr = prefix ? `${prefix}image` : "image";
|
|
671
|
+
if (node.className) classStr += ` ${node.className}`;
|
|
672
|
+
return `<img src="${src}" alt="${alt}" class="${classStr}"${scopeAttr}>`;
|
|
673
|
+
}
|
|
674
|
+
if (node.className) {
|
|
675
|
+
return `<img src="${src}" alt="${alt}" class="${node.className}"${scopeAttr}>`;
|
|
676
|
+
}
|
|
677
|
+
return `<img src="${src}" alt="${alt}"${scopeAttr}>`;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
class CodeRendererStrategy {
|
|
681
|
+
constructor() {
|
|
682
|
+
this.type = "code";
|
|
683
|
+
}
|
|
684
|
+
render(node, _renderChild, ctx) {
|
|
685
|
+
const scopeAttr = ctx.getScopeAttr(node);
|
|
686
|
+
const lang = node.attributes?.lang || "";
|
|
687
|
+
if (ctx.hasClassConfig()) {
|
|
688
|
+
const prefix = ctx.classPrefix;
|
|
689
|
+
const codeClass = prefix ? `${prefix}code` : "code";
|
|
690
|
+
return `<pre${scopeAttr}><code class="${codeClass} language-${lang}">${node.content || ""}</code></pre>`;
|
|
691
|
+
}
|
|
692
|
+
return `<pre${scopeAttr}><code class="language-${lang}">${node.content || ""}</code></pre>`;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
class ContainerRendererStrategy {
|
|
696
|
+
constructor() {
|
|
697
|
+
this.type = "container";
|
|
698
|
+
}
|
|
699
|
+
render(node, renderChild, ctx) {
|
|
700
|
+
if (node.rawHTML) {
|
|
701
|
+
return node.rawHTML;
|
|
702
|
+
}
|
|
703
|
+
const tag = node.attributes?.tag || "div";
|
|
704
|
+
const children = node.children?.map(renderChild).join("") || "";
|
|
705
|
+
const id = node.attributes?.id;
|
|
706
|
+
const idAttr = id ? ` id="${id}"` : "";
|
|
707
|
+
const scopeAttr = ctx.getScopeAttr(node);
|
|
708
|
+
if (tag === "hr") return "<hr>";
|
|
709
|
+
if (ctx.hasClassConfig()) {
|
|
710
|
+
const containerClass = ctx.getContainerClass(tag);
|
|
711
|
+
const prefix = ctx.classPrefix;
|
|
712
|
+
if (prefix) {
|
|
713
|
+
const classes2 = [prefix + (containerClass || "container")];
|
|
714
|
+
if (node.className) classes2.push(node.className);
|
|
715
|
+
return `<${tag} class="${classes2.join(" ")}"${idAttr}${scopeAttr}>${children}</${tag}>`;
|
|
716
|
+
}
|
|
717
|
+
const classes = [containerClass || "container"];
|
|
718
|
+
if (node.className) classes.push(node.className);
|
|
719
|
+
return `<${tag} class="${classes.join(" ")}"${idAttr}${scopeAttr}>${children}</${tag}>`;
|
|
720
|
+
}
|
|
721
|
+
if (node.className) {
|
|
722
|
+
return `<${tag} class="${node.className}"${idAttr}${scopeAttr}>${children}</${tag}>`;
|
|
723
|
+
}
|
|
724
|
+
return `<${tag}${idAttr}${scopeAttr}>${children}</${tag}>`;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
class StrongRendererStrategy {
|
|
728
|
+
constructor() {
|
|
729
|
+
this.type = "strong";
|
|
730
|
+
}
|
|
731
|
+
render(node, _renderChild, ctx) {
|
|
732
|
+
return `<strong${ctx.getScopeAttr(node)}>${node.content || ""}</strong>`;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
class EmphasisRendererStrategy {
|
|
736
|
+
constructor() {
|
|
737
|
+
this.type = "emphasis";
|
|
738
|
+
}
|
|
739
|
+
render(node, _renderChild, ctx) {
|
|
740
|
+
return `<em${ctx.getScopeAttr(node)}>${node.content || ""}</em>`;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
class LinkRendererStrategy {
|
|
744
|
+
constructor() {
|
|
745
|
+
this.type = "link";
|
|
746
|
+
}
|
|
747
|
+
render(node, _renderChild, ctx) {
|
|
748
|
+
const href = node.attributes?.href || "";
|
|
749
|
+
return `<a href="${href}"${ctx.getScopeAttr(node)}>${node.content || ""}</a>`;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
class TextRendererStrategy {
|
|
753
|
+
constructor() {
|
|
754
|
+
this.type = "text";
|
|
755
|
+
}
|
|
756
|
+
render(node, _renderChild, _ctx) {
|
|
757
|
+
return node.content || "";
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
class RendererStrategyRegistry {
|
|
761
|
+
constructor() {
|
|
762
|
+
this.strategies = /* @__PURE__ */ new Map();
|
|
763
|
+
this.register(new HeadingRendererStrategy());
|
|
764
|
+
this.register(new ParagraphRendererStrategy());
|
|
765
|
+
this.register(new ListRendererStrategy());
|
|
766
|
+
this.register(new ListItemRendererStrategy());
|
|
767
|
+
this.register(new ImageRendererStrategy());
|
|
768
|
+
this.register(new CodeRendererStrategy());
|
|
769
|
+
this.register(new ContainerRendererStrategy());
|
|
770
|
+
this.register(new StrongRendererStrategy());
|
|
771
|
+
this.register(new EmphasisRendererStrategy());
|
|
772
|
+
this.register(new LinkRendererStrategy());
|
|
773
|
+
this.register(new TextRendererStrategy());
|
|
774
|
+
this.fallback = new TextRendererStrategy();
|
|
775
|
+
}
|
|
776
|
+
/** Register a strategy for a node type. Overrides any existing strategy. */
|
|
777
|
+
register(strategy) {
|
|
778
|
+
this.strategies.set(strategy.type, strategy);
|
|
779
|
+
}
|
|
780
|
+
/** Unregister a strategy by node type. */
|
|
781
|
+
unregister(type) {
|
|
782
|
+
this.strategies.delete(type);
|
|
783
|
+
}
|
|
784
|
+
/** Get a strategy for the given node type, falling back to catch-all. */
|
|
785
|
+
get(type) {
|
|
786
|
+
return this.strategies.get(type) ?? this.fallback;
|
|
787
|
+
}
|
|
788
|
+
/** Check if a dedicated strategy exists for the given node type. */
|
|
789
|
+
has(type) {
|
|
790
|
+
return this.strategies.has(type);
|
|
791
|
+
}
|
|
792
|
+
/** Get all registered dedicated strategy types. */
|
|
793
|
+
get types() {
|
|
794
|
+
return Array.from(this.strategies.keys());
|
|
795
|
+
}
|
|
796
|
+
/** Replace the fallback strategy. */
|
|
797
|
+
setFallback(strategy) {
|
|
798
|
+
this.fallback = strategy;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
419
801
|
class HTMLRenderer {
|
|
420
802
|
constructor(config = {}) {
|
|
421
803
|
this.config = {
|
|
@@ -424,6 +806,11 @@ class HTMLRenderer {
|
|
|
424
806
|
addHeadingIds: config.addHeadingIds ?? false,
|
|
425
807
|
emitScopeAnchors: config.emitScopeAnchors ?? false
|
|
426
808
|
};
|
|
809
|
+
this.strategyRegistry = new RendererStrategyRegistry();
|
|
810
|
+
}
|
|
811
|
+
/** Access the strategy registry for customization. */
|
|
812
|
+
get strategies() {
|
|
813
|
+
return this.strategyRegistry;
|
|
427
814
|
}
|
|
428
815
|
hasClassConfig() {
|
|
429
816
|
return this.config.classPrefix !== "" || this.config.addHeadingIds;
|
|
@@ -450,68 +837,40 @@ class HTMLRenderer {
|
|
|
450
837
|
const scopeValue = node.scope || nodeTypeToScope[node.type] || "container";
|
|
451
838
|
return ` data-md-scope="${scopeValue}"`;
|
|
452
839
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
840
|
+
/**
|
|
841
|
+
* Get the CSS class for a container's tag-based rendering.
|
|
842
|
+
* Returns just the tag name since renderWithClass applies the prefix.
|
|
843
|
+
*/
|
|
844
|
+
getContainerClass(tag) {
|
|
845
|
+
if (!this.hasClassConfig()) return "";
|
|
846
|
+
return tag;
|
|
847
|
+
}
|
|
848
|
+
buildRenderContext() {
|
|
849
|
+
const self = this;
|
|
850
|
+
return {
|
|
851
|
+
get classPrefix() {
|
|
852
|
+
return self.config.classPrefix;
|
|
853
|
+
},
|
|
854
|
+
get addHeadingIds() {
|
|
855
|
+
return self.config.addHeadingIds;
|
|
856
|
+
},
|
|
857
|
+
get emitScopeAnchors() {
|
|
858
|
+
return self.config.emitScopeAnchors;
|
|
859
|
+
},
|
|
860
|
+
get customCSS() {
|
|
861
|
+
return self.config.customCSS;
|
|
862
|
+
},
|
|
863
|
+
hasClassConfig: () => self.hasClassConfig(),
|
|
864
|
+
getClass: (baseClass, nodeClass) => self.getClass(baseClass, nodeClass),
|
|
865
|
+
getScopeAttr: (node) => self.getScopeAttr(node),
|
|
866
|
+
generateHeadingId: (content) => self.generateHeadingId(content),
|
|
867
|
+
getContainerClass: (tag) => self.getContainerClass(tag)
|
|
868
|
+
};
|
|
456
869
|
}
|
|
457
870
|
renderNode(node) {
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const level = node.attributes?.level || "2";
|
|
462
|
-
const headingId = this.config.addHeadingIds ? ` id="${this.generateHeadingId(node.content)}"` : "";
|
|
463
|
-
let headingClass = "";
|
|
464
|
-
if (this.hasClassConfig()) {
|
|
465
|
-
const prefix = this.config.classPrefix;
|
|
466
|
-
const levelClass = level === "1" ? "h1" : level === "2" ? "h2" : level === "3" ? "h3" : level === "4" ? "h4" : level === "5" ? "h5" : "h6";
|
|
467
|
-
headingClass = prefix ? `${prefix}${levelClass}` : levelClass;
|
|
468
|
-
}
|
|
469
|
-
if (!headingClass) {
|
|
470
|
-
return `<h${level}${headingId}${scopeAttr}>${node.content || ""}</h${level}>`;
|
|
471
|
-
}
|
|
472
|
-
return `<h${level}${headingId}${scopeAttr} class="${headingClass}">${node.content || ""}</h${level}>`;
|
|
473
|
-
case "paragraph":
|
|
474
|
-
if (node.children) {
|
|
475
|
-
const childrenHtml = node.children.map((child) => this.renderNode(child)).join("");
|
|
476
|
-
return this.renderWithClass("p", childrenHtml, "paragraph", void 0, scopeAttr);
|
|
477
|
-
}
|
|
478
|
-
return this.renderWithClass("p", node.content || "", "paragraph", void 0, scopeAttr);
|
|
479
|
-
case "list":
|
|
480
|
-
const tag = node.ordered ? "ol" : "ul";
|
|
481
|
-
const items = node.children?.map((child) => this.renderNode(child)).join("") || "";
|
|
482
|
-
return this.renderWithClass(tag, items, "list", void 0, scopeAttr);
|
|
483
|
-
case "list-item":
|
|
484
|
-
return this.renderWithClass("li", node.content || "", "list-item", void 0, scopeAttr);
|
|
485
|
-
case "image":
|
|
486
|
-
const src = node.src || node.attributes?.src || "";
|
|
487
|
-
const alt = node.alt || node.attributes?.alt || "";
|
|
488
|
-
const classStr = this.getClass("image", node.className || void 0);
|
|
489
|
-
return `<img src="${src}" alt="${alt}"${classStr ? ` class="${classStr}"` : ""}${scopeAttr}>`;
|
|
490
|
-
case "code":
|
|
491
|
-
const codeClass = this.hasClassConfig() ? ` class="${this.getClass("code")} language-${node.attributes?.lang || ""}"` : ` class="language-${node.attributes?.lang || ""}"`;
|
|
492
|
-
return `<pre${scopeAttr}><code${codeClass}>${node.content || ""}</code></pre>`;
|
|
493
|
-
case "container":
|
|
494
|
-
if (node.attributes?.tag === "hr") return "<hr>";
|
|
495
|
-
if (node.attributes?.tag === "blockquote") {
|
|
496
|
-
const children = node.children?.map((child) => this.renderNode(child)).join("") || "";
|
|
497
|
-
return this.renderWithClass("blockquote", children, "blockquote", void 0, scopeAttr);
|
|
498
|
-
}
|
|
499
|
-
if (node.rawHTML) {
|
|
500
|
-
return node.rawHTML;
|
|
501
|
-
}
|
|
502
|
-
const containerChildren = node.children?.map((child) => this.renderNode(child)).join("") || "";
|
|
503
|
-
return this.renderWithClass("div", containerChildren, "container", node.className || void 0, scopeAttr);
|
|
504
|
-
case "strong":
|
|
505
|
-
return `<strong${scopeAttr}>${node.content || ""}</strong>`;
|
|
506
|
-
case "emphasis":
|
|
507
|
-
return `<em${scopeAttr}>${node.content || ""}</em>`;
|
|
508
|
-
case "link":
|
|
509
|
-
const href = node.attributes?.href || "";
|
|
510
|
-
return `<a href="${href}"${scopeAttr}>${node.content || ""}</a>`;
|
|
511
|
-
case "text":
|
|
512
|
-
default:
|
|
513
|
-
return node.content || "";
|
|
514
|
-
}
|
|
871
|
+
const ctx = this.buildRenderContext();
|
|
872
|
+
const strategy = this.strategyRegistry.get(node.type);
|
|
873
|
+
return strategy.render(node, (child) => this.renderNode(child), ctx);
|
|
515
874
|
}
|
|
516
875
|
renderNodes(nodes) {
|
|
517
876
|
if (!nodes || nodes.length === 0) {
|
|
@@ -606,21 +965,149 @@ class MarkdownPipeline {
|
|
|
606
965
|
return this.renderer.getCustomCSS();
|
|
607
966
|
}
|
|
608
967
|
}
|
|
968
|
+
function walkTree(root, visitor) {
|
|
969
|
+
return walkNode(root, null, 0, visitor);
|
|
970
|
+
}
|
|
971
|
+
function walkNode(node, parent, depth, visitor) {
|
|
972
|
+
let processed = visitor.enter ? visitor.enter(node, parent, depth) : node;
|
|
973
|
+
if (processed === null) return null;
|
|
974
|
+
if (processed.children && processed.children.length > 0) {
|
|
975
|
+
processed = {
|
|
976
|
+
...processed,
|
|
977
|
+
children: processed.children.map((child) => walkNode(child, processed, depth + 1, visitor)).filter(Boolean)
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
processed = visitor.exit ? visitor.exit(processed, parent, depth) : processed;
|
|
981
|
+
if (processed === null) return null;
|
|
982
|
+
return processed;
|
|
983
|
+
}
|
|
984
|
+
function collectFromTree(root, collector) {
|
|
985
|
+
const results = [];
|
|
986
|
+
collectNode(root, null, 0, collector, results);
|
|
987
|
+
return results;
|
|
988
|
+
}
|
|
989
|
+
function collectNode(node, parent, depth, collector, results) {
|
|
990
|
+
const result = collector(node, parent, depth);
|
|
991
|
+
if (result !== null) {
|
|
992
|
+
results.push(result);
|
|
993
|
+
}
|
|
994
|
+
if (node.children) {
|
|
995
|
+
for (const child of node.children) {
|
|
996
|
+
collectNode(child, node, depth + 1, collector, results);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function collectByType(root, type) {
|
|
1001
|
+
return collectFromTree(
|
|
1002
|
+
root,
|
|
1003
|
+
(node) => node.type === type ? node : null
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
function collectHeadings(root) {
|
|
1007
|
+
return collectFromTree(root, (node) => {
|
|
1008
|
+
if (node.type === "heading") {
|
|
1009
|
+
return {
|
|
1010
|
+
level: node.attributes?.level || "2",
|
|
1011
|
+
text: node.content || "",
|
|
1012
|
+
id: node.attributes?.id
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
return null;
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
function collectImages(root) {
|
|
1019
|
+
return collectFromTree(root, (node) => {
|
|
1020
|
+
if (node.type === "image") {
|
|
1021
|
+
return { src: node.src || "", alt: node.alt || "" };
|
|
1022
|
+
}
|
|
1023
|
+
return null;
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
class NodeFactory {
|
|
1027
|
+
static heading(content, attributes) {
|
|
1028
|
+
return {
|
|
1029
|
+
type: "heading",
|
|
1030
|
+
content,
|
|
1031
|
+
attributes: { level: attributes?.level || "2", ...attributes }
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
static paragraph(contentOrChildren, children) {
|
|
1035
|
+
if (typeof contentOrChildren === "string") {
|
|
1036
|
+
return { type: "paragraph", content: contentOrChildren };
|
|
1037
|
+
}
|
|
1038
|
+
return { type: "paragraph", children: contentOrChildren ?? children };
|
|
1039
|
+
}
|
|
1040
|
+
static list(items, ordered, attributes) {
|
|
1041
|
+
return {
|
|
1042
|
+
type: "list",
|
|
1043
|
+
ordered: ordered ?? false,
|
|
1044
|
+
children: items,
|
|
1045
|
+
...attributes ? { attributes } : {}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
static listItem(content) {
|
|
1049
|
+
return { type: "list-item", content };
|
|
1050
|
+
}
|
|
1051
|
+
static image(src, alt, className) {
|
|
1052
|
+
return { type: "image", src, alt: alt || "", ...className ? { className } : {} };
|
|
1053
|
+
}
|
|
1054
|
+
static code(content, lang) {
|
|
1055
|
+
return {
|
|
1056
|
+
type: "code",
|
|
1057
|
+
content,
|
|
1058
|
+
attributes: { lang: lang || "" }
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
static container(children, config) {
|
|
1062
|
+
const node = { type: "container" };
|
|
1063
|
+
if (children && children.length > 0) node.children = children;
|
|
1064
|
+
if (config?.rawHTML) node.rawHTML = config.rawHTML;
|
|
1065
|
+
if (config?.scope) node.scope = config.scope;
|
|
1066
|
+
const attrs = {};
|
|
1067
|
+
if (config?.tag) attrs.tag = config.tag;
|
|
1068
|
+
if (config?.id) attrs.id = config.id;
|
|
1069
|
+
if (Object.keys(attrs).length > 0) node.attributes = attrs;
|
|
1070
|
+
if (config?.className) node.className = config.className;
|
|
1071
|
+
return node;
|
|
1072
|
+
}
|
|
1073
|
+
static text(content) {
|
|
1074
|
+
return { type: "text", content };
|
|
1075
|
+
}
|
|
1076
|
+
static strong(content) {
|
|
1077
|
+
return { type: "strong", content };
|
|
1078
|
+
}
|
|
1079
|
+
static emphasis(content) {
|
|
1080
|
+
return { type: "emphasis", content };
|
|
1081
|
+
}
|
|
1082
|
+
static link(href, content) {
|
|
1083
|
+
return { type: "link", content, attributes: { href } };
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
609
1086
|
export {
|
|
610
1087
|
BlockquoteHandler,
|
|
611
1088
|
CatchAllHandler,
|
|
612
1089
|
CodeHandler,
|
|
1090
|
+
ContainerBlockHandler,
|
|
1091
|
+
FrontmatterHandler,
|
|
613
1092
|
HTMLRenderer,
|
|
614
1093
|
HeadingHandler,
|
|
615
1094
|
HrHandler,
|
|
616
1095
|
HtmlHandler,
|
|
617
1096
|
ImageHandler,
|
|
1097
|
+
LinkHandler,
|
|
618
1098
|
ListHandler,
|
|
619
1099
|
MarkdownParser,
|
|
620
1100
|
MarkdownPipeline,
|
|
1101
|
+
NodeFactory,
|
|
621
1102
|
ParagraphHandler,
|
|
1103
|
+
RendererStrategyRegistry,
|
|
622
1104
|
TokenHandlerRegistry,
|
|
1105
|
+
collectByType,
|
|
1106
|
+
collectFromTree,
|
|
1107
|
+
collectHeadings,
|
|
1108
|
+
collectImages,
|
|
623
1109
|
defaultAllowedHTMLTags,
|
|
624
|
-
nodeTypeToScope
|
|
1110
|
+
nodeTypeToScope,
|
|
1111
|
+
walkTree
|
|
625
1112
|
};
|
|
626
1113
|
//# sourceMappingURL=index.js.map
|