@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.d.ts +249 -1
- package/dist/index.js +590 -68
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
334
|
-
const
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
395
|
-
|
|
396
|
-
|
|
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
|