@wenyan-md/core 3.0.6 → 3.0.7

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/core.js CHANGED
@@ -434,6 +434,28 @@ function createMarkedClient() {
434
434
  md.use(highlightExtension);
435
435
  md.use({
436
436
  extensions: [
437
+ // 宽松 link/image tokenizer,允许 URL 中包含空格(不要求用 <> 包裹)
438
+ {
439
+ name: "looseLink",
440
+ level: "inline",
441
+ start(src) {
442
+ return src.match(/!?\[/)?.index;
443
+ },
444
+ tokenizer(src) {
445
+ const rule = /^(!?)\[([^\]]*)\]\(([^)]+)\)/;
446
+ const match = rule.exec(src);
447
+ if (!match) return;
448
+ const isImage = !!match[1];
449
+ return {
450
+ type: isImage ? "image" : "link",
451
+ raw: match[0],
452
+ text: match[2],
453
+ href: match[3],
454
+ tokens: this.lexer.inlineTokens(match[2])
455
+ };
456
+ }
457
+ },
458
+ // 自定义图片语法扩展 ![](){...}
437
459
  {
438
460
  name: "attributeImage",
439
461
  level: "inline",
@@ -462,7 +484,8 @@ function createMarkedClient() {
462
484
  renderer(token) {
463
485
  const attrs = stringToMap(token.attrs);
464
486
  const styleStr = Array.from(attrs).map(([k, v]) => /^\d+$/.test(v) ? `${k}:${v}px` : `${k}:${v}`).join("; ");
465
- return `<img src="${token.href}" alt="${token.text || ""}" title="${token.text || ""}" style="${styleStr}">`;
487
+ const href = normalizeHref(token.href);
488
+ return `<img src="${href}" alt="${token.text || ""}" title="${token.text || ""}" style="${styleStr}">`;
466
489
  }
467
490
  }
468
491
  ]
@@ -490,7 +513,12 @@ function createMarkedClient() {
490
513
  },
491
514
  // 重写普通图片 (处理标准 Markdown 图片)
492
515
  image(token) {
493
- return `<img src="${token.href}" alt="${token.text || ""}" title="${token.title || token.text || ""}">`;
516
+ const href = normalizeHref(token.href);
517
+ return `<img src="${href}" alt="${token.text || ""}" title="${token.title || token.text || ""}">`;
518
+ },
519
+ link(token) {
520
+ const href = normalizeHref(token.href);
521
+ return `<a href="${href}">${this.parser.parseInline(token.tokens)}</a>`;
494
522
  }
495
523
  }
496
524
  });
@@ -503,10 +531,21 @@ function createMarkedClient() {
503
531
  */
504
532
  async parse(markdown) {
505
533
  await configure();
506
- return md.parse(markdown);
534
+ return await md.parse(markdown);
507
535
  }
508
536
  };
509
537
  }
538
+ function normalizeHref(href) {
539
+ href = href.trim();
540
+ if (href.startsWith("<") && href.endsWith(">")) {
541
+ href = href.slice(1, -1);
542
+ }
543
+ try {
544
+ return encodeURI(href);
545
+ } catch {
546
+ return href;
547
+ }
548
+ }
510
549
  let htmlHandlerRegistered = false;
511
550
  function createMathJaxParser(options = {}) {
512
551
  const adaptor = liteAdaptor();
@@ -565,6 +604,36 @@ function createMathJaxParser(options = {}) {
565
604
  }
566
605
  };
567
606
  }
607
+ function createMermaidParser(options) {
608
+ const resolvedOptions = resolveMermaidOptions(options);
609
+ return {
610
+ async parser(html) {
611
+ if (!resolvedOptions.enabled || !containsMermaidCodeBlock(html)) {
612
+ return html;
613
+ }
614
+ if (!resolvedOptions.renderer) {
615
+ throw new Error("Mermaid 渲染已启用,但未配置 renderer");
616
+ }
617
+ return await resolvedOptions.renderer.renderHtml(html);
618
+ }
619
+ };
620
+ }
621
+ function resolveMermaidOptions(options) {
622
+ if (options === void 0) {
623
+ return { enabled: false };
624
+ }
625
+ if (typeof options === "boolean") {
626
+ return { enabled: options };
627
+ }
628
+ return {
629
+ enabled: options.enabled ?? true,
630
+ renderer: options.renderer
631
+ };
632
+ }
633
+ function containsMermaidCodeBlock(html) {
634
+ return html.includes("language-mermaid");
635
+ }
636
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
568
637
  function wechatPostRender(element) {
569
638
  const mathElements = element.querySelectorAll("mjx-container");
570
639
  mathElements.forEach((mathContainer) => {
@@ -583,6 +652,13 @@ function wechatPostRender(element) {
583
652
  }
584
653
  }
585
654
  });
655
+ const mermaidSvgs = element.querySelectorAll('[data-wenyan-diagram="mermaid"] svg');
656
+ mermaidSvgs.forEach((svg) => {
657
+ svg.style.maxWidth = "100%";
658
+ svg.style.height = "auto";
659
+ inlineMermaidSvgStyles(svg);
660
+ flattenMermaidMarkers(svg);
661
+ });
586
662
  const codeElements = element.querySelectorAll("pre code");
587
663
  codeElements.forEach((codeEl) => {
588
664
  codeEl.innerHTML = codeEl.innerHTML.replace(/\n/g, "<br>").replace(/(>[^<]+)|(^[^<]+)/g, (str) => str.replace(/\s/g, "&nbsp;"));
@@ -599,6 +675,268 @@ function wechatPostRender(element) {
599
675
  element.style.color = "rgb(0, 0, 0)";
600
676
  element.style.caretColor = "rgb(0, 0, 0)";
601
677
  }
678
+ function inlineMermaidSvgStyles(svg) {
679
+ const styleElements = Array.from(svg.querySelectorAll("style"));
680
+ styleElements.forEach((styleElement) => {
681
+ applyInlineStylesFromCss(svg, styleElement.textContent ?? "");
682
+ styleElement.remove();
683
+ });
684
+ }
685
+ function flattenMermaidMarkers(svg) {
686
+ const convertedMarkerIds = /* @__PURE__ */ new Set();
687
+ const markedElements = Array.from(svg.querySelectorAll("[marker-start], [marker-end]"));
688
+ markedElements.forEach((markedElement) => {
689
+ convertMarkerReference(svg, markedElement, "start", convertedMarkerIds);
690
+ convertMarkerReference(svg, markedElement, "end", convertedMarkerIds);
691
+ });
692
+ convertedMarkerIds.forEach((markerId) => {
693
+ findMarkerById(svg, markerId)?.remove();
694
+ });
695
+ svg.querySelectorAll("defs").forEach((defs) => {
696
+ if (!defs.children.length) {
697
+ defs.remove();
698
+ }
699
+ });
700
+ }
701
+ function convertMarkerReference(svg, sourceElement, position, convertedMarkerIds) {
702
+ const markerAttribute = `marker-${position}`;
703
+ const markerValue = sourceElement.getAttribute(markerAttribute);
704
+ if (!markerValue) {
705
+ return;
706
+ }
707
+ const markerId = extractMarkerId(markerValue);
708
+ if (!markerId) {
709
+ return;
710
+ }
711
+ const marker = findMarkerById(svg, markerId);
712
+ if (!marker) {
713
+ return;
714
+ }
715
+ const flattenedMarker = createFlattenedMarker(svg, sourceElement, marker, position);
716
+ if (!flattenedMarker) {
717
+ return;
718
+ }
719
+ sourceElement.parentNode?.appendChild(flattenedMarker);
720
+ sourceElement.removeAttribute(markerAttribute);
721
+ convertedMarkerIds.add(markerId);
722
+ }
723
+ function createFlattenedMarker(svg, sourceElement, marker, position) {
724
+ const terminalPoints = getTerminalPoints(sourceElement, position);
725
+ if (!terminalPoints) {
726
+ return null;
727
+ }
728
+ const ownerDocument = svg.ownerDocument;
729
+ const markerGroup = ownerDocument.createElementNS(SVG_NAMESPACE, "g");
730
+ const markerStyle = marker.getAttribute("style");
731
+ const angle = getMarkerAngleDegrees(position, terminalPoints.anchor, terminalPoints.reference, marker);
732
+ markerGroup.setAttribute("data-wenyan-marker", position);
733
+ markerGroup.setAttribute("transform", buildMarkerTransform(marker, terminalPoints.anchor, angle, position));
734
+ if (markerStyle) {
735
+ markerGroup.setAttribute("style", markerStyle);
736
+ }
737
+ Array.from(marker.children).forEach((child) => {
738
+ markerGroup.appendChild(child.cloneNode(true));
739
+ });
740
+ return markerGroup;
741
+ }
742
+ function getTerminalPoints(sourceElement, position) {
743
+ const points = getEdgePoints(sourceElement);
744
+ if (points.length < 2) {
745
+ return null;
746
+ }
747
+ if (position === "end") {
748
+ const anchor2 = points.at(-1);
749
+ if (!anchor2) {
750
+ return null;
751
+ }
752
+ const reference2 = findDistinctPoint(points, points.length - 2, -1, anchor2);
753
+ return reference2 ? { anchor: anchor2, reference: reference2 } : null;
754
+ }
755
+ const anchor = points[0];
756
+ if (!anchor) {
757
+ return null;
758
+ }
759
+ const reference = findDistinctPoint(points, 1, 1, anchor);
760
+ return reference ? { anchor, reference } : null;
761
+ }
762
+ function getEdgePoints(sourceElement) {
763
+ const encodedPoints = sourceElement.getAttribute("data-points");
764
+ if (encodedPoints) {
765
+ const decodedPoints = decodeMermaidPoints(sourceElement, encodedPoints);
766
+ if (decodedPoints.length >= 2) {
767
+ return decodedPoints;
768
+ }
769
+ }
770
+ const polyPoints = sourceElement.getAttribute("points");
771
+ if (polyPoints) {
772
+ return parseCoordinatePairs(polyPoints);
773
+ }
774
+ const pathData = sourceElement.getAttribute("d");
775
+ if (pathData) {
776
+ return parseCoordinatePairs(pathData);
777
+ }
778
+ return [];
779
+ }
780
+ function decodeMermaidPoints(sourceElement, encodedPoints) {
781
+ const defaultView = sourceElement.ownerDocument.defaultView;
782
+ try {
783
+ const decoded = defaultView?.atob?.(encodedPoints) ?? (typeof atob === "function" ? atob(encodedPoints) : "");
784
+ const parsed = JSON.parse(decoded);
785
+ return parsed.filter((point) => typeof point.x === "number" && typeof point.y === "number").map((point) => ({
786
+ x: point.x,
787
+ y: point.y
788
+ }));
789
+ } catch {
790
+ return [];
791
+ }
792
+ }
793
+ function parseCoordinatePairs(value) {
794
+ const matches = Array.from(value.matchAll(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi), (match) => Number(match[0]));
795
+ const points = [];
796
+ for (let index = 0; index + 1 < matches.length; index += 2) {
797
+ points.push({
798
+ x: matches[index],
799
+ y: matches[index + 1]
800
+ });
801
+ }
802
+ return points;
803
+ }
804
+ function findDistinctPoint(points, startIndex, step, anchor) {
805
+ for (let index = startIndex; index >= 0 && index < points.length; index += step) {
806
+ const point = points[index];
807
+ if (!point) {
808
+ continue;
809
+ }
810
+ if (point.x !== anchor.x || point.y !== anchor.y) {
811
+ return point;
812
+ }
813
+ }
814
+ return null;
815
+ }
816
+ function getMarkerAngleDegrees(position, anchor, reference, marker) {
817
+ const baseAngle = position === "end" ? Math.atan2(anchor.y - reference.y, anchor.x - reference.x) : Math.atan2(reference.y - anchor.y, reference.x - anchor.x);
818
+ const orient = marker.getAttribute("orient")?.trim();
819
+ let angle = baseAngle * 180 / Math.PI;
820
+ if (orient === "auto-start-reverse" && position === "start") {
821
+ angle += 180;
822
+ } else if (orient && orient !== "auto") {
823
+ const offset = Number.parseFloat(orient);
824
+ if (Number.isFinite(offset)) {
825
+ angle += offset;
826
+ }
827
+ }
828
+ return angle;
829
+ }
830
+ function buildMarkerTransform(marker, anchor, angle, position) {
831
+ const viewBox = parseViewBox(marker.getAttribute("viewBox"));
832
+ const markerWidth = parseNumericAttribute(marker.getAttribute("markerWidth"), viewBox?.width ?? 1);
833
+ const markerHeight = parseNumericAttribute(marker.getAttribute("markerHeight"), viewBox?.height ?? 1);
834
+ const viewBoxWidth = viewBox?.width ?? markerWidth;
835
+ const viewBoxHeight = viewBox?.height ?? markerHeight;
836
+ const scaleX = viewBoxWidth === 0 ? 1 : markerWidth / viewBoxWidth;
837
+ const scaleY = viewBoxHeight === 0 ? 1 : markerHeight / viewBoxHeight;
838
+ const offsetX = parseNumericAttribute(marker.getAttribute("refX")) + (viewBox?.minX ?? 0);
839
+ const offsetY = parseNumericAttribute(marker.getAttribute("refY")) + (viewBox?.minY ?? 0);
840
+ const adjustedAnchor = adjustMarkerAnchor(anchor, angle, scaleX, viewBox, offsetX, position);
841
+ return [
842
+ `translate(${adjustedAnchor.x} ${adjustedAnchor.y})`,
843
+ `rotate(${angle})`,
844
+ `scale(${scaleX} ${scaleY})`,
845
+ `translate(${-offsetX} ${-offsetY})`
846
+ ].join(" ");
847
+ }
848
+ function adjustMarkerAnchor(anchor, angle, scaleX, viewBox, refX, position) {
849
+ const minX = viewBox?.minX ?? 0;
850
+ const maxX = viewBox ? viewBox.minX + viewBox.width : refX;
851
+ const overlap = position === "start" ? Math.max(0, refX - minX) : Math.max(0, maxX - refX);
852
+ if (overlap === 0) {
853
+ return anchor;
854
+ }
855
+ const distance = overlap * scaleX;
856
+ const radians = angle * Math.PI / 180;
857
+ const direction = position === "start" ? 1 : -1;
858
+ return {
859
+ x: anchor.x + Math.cos(radians) * distance * direction,
860
+ y: anchor.y + Math.sin(radians) * distance * direction
861
+ };
862
+ }
863
+ function parseViewBox(viewBoxValue) {
864
+ if (!viewBoxValue) {
865
+ return null;
866
+ }
867
+ const values = viewBoxValue.trim().split(/[\s,]+/).map((value) => Number.parseFloat(value));
868
+ if (values.length !== 4 || values.some((value) => !Number.isFinite(value))) {
869
+ return null;
870
+ }
871
+ return {
872
+ minX: values[0],
873
+ minY: values[1],
874
+ width: values[2],
875
+ height: values[3]
876
+ };
877
+ }
878
+ function parseNumericAttribute(value, fallback = 0) {
879
+ if (!value) {
880
+ return fallback;
881
+ }
882
+ const parsed = Number.parseFloat(value);
883
+ return Number.isFinite(parsed) ? parsed : fallback;
884
+ }
885
+ function extractMarkerId(markerValue) {
886
+ const match = markerValue.match(/url\(#([^)]+)\)/);
887
+ return match?.[1] ?? null;
888
+ }
889
+ function findMarkerById(svg, markerId) {
890
+ return Array.from(svg.querySelectorAll("marker")).find((marker) => marker.getAttribute("id") === markerId);
891
+ }
892
+ function applyInlineStylesFromCss(svg, cssText) {
893
+ const ast = csstree.parse(cssText, parseOptions);
894
+ csstree.walk(ast, {
895
+ visit: "Rule",
896
+ enter(node) {
897
+ if (node.prelude.type !== "SelectorList") return;
898
+ const declarations = node.block.children.toArray().filter((decl) => decl.type === "Declaration");
899
+ if (declarations.length === 0) return;
900
+ node.prelude.children.forEach((selectorNode) => {
901
+ const selector = csstree.generate(selectorNode).trim();
902
+ if (!selector || selector.includes("::")) return;
903
+ const targets = resolveInlineTargets(svg, selector);
904
+ targets.forEach((target) => {
905
+ declarations.forEach((decl) => {
906
+ const value = csstree.generate(decl.value);
907
+ const priority = decl.important ? "important" : "";
908
+ target.style.setProperty(decl.property, value, priority);
909
+ });
910
+ });
911
+ });
912
+ }
913
+ });
914
+ }
915
+ function resolveInlineTargets(svg, selector) {
916
+ if (selector === ":root") {
917
+ return [svg];
918
+ }
919
+ if (selector.includes(":")) {
920
+ return [];
921
+ }
922
+ const targets = /* @__PURE__ */ new Set();
923
+ if (svg.matches(selector)) {
924
+ targets.add(svg);
925
+ }
926
+ svg.querySelectorAll(selector).forEach((node) => {
927
+ if (isInlineStyleTarget(node)) {
928
+ targets.add(node);
929
+ }
930
+ });
931
+ return Array.from(targets);
932
+ }
933
+ function isInlineStyleTarget(node) {
934
+ const defaultView = node.ownerDocument.defaultView;
935
+ if (!defaultView) {
936
+ return false;
937
+ }
938
+ return node instanceof defaultView.SVGElement || node instanceof defaultView.HTMLElement;
939
+ }
602
940
  const themeModifier = createCssModifier({});
603
941
  function renderTheme(wenyanElement, themeCss) {
604
942
  const modifiedCss = themeModifier(themeCss);
@@ -1169,10 +1507,104 @@ function getContentForToutiao(wenyanElement) {
1169
1507
  });
1170
1508
  return wenyanElement.outerHTML;
1171
1509
  }
1510
+ const DEFAULT_MERMAID_CONFIG = {
1511
+ htmlLabels: false,
1512
+ flowchart: {
1513
+ htmlLabels: false
1514
+ }
1515
+ };
1516
+ async function replaceMermaidCodeBlocks(root, renderDiagram) {
1517
+ const codeBlocks = Array.from(root.querySelectorAll("pre > code.language-mermaid"));
1518
+ if (codeBlocks.length === 0) {
1519
+ return false;
1520
+ }
1521
+ const ownerDocument = getOwnerDocument(root);
1522
+ for (const [index, codeBlock] of codeBlocks.entries()) {
1523
+ const pre = codeBlock.parentElement;
1524
+ const parent = pre?.parentElement;
1525
+ if (!pre || !parent) {
1526
+ continue;
1527
+ }
1528
+ const code = codeBlock.textContent ?? "";
1529
+ const svg = await renderDiagram({
1530
+ id: `wenyan-mermaid-${index + 1}`,
1531
+ code
1532
+ });
1533
+ const figure = ownerDocument.createElement("figure");
1534
+ figure.setAttribute("data-wenyan-diagram", "mermaid");
1535
+ figure.innerHTML = svg.trim();
1536
+ pre.replaceWith(figure);
1537
+ }
1538
+ return true;
1539
+ }
1540
+ function createBrowserMermaidRenderer(options = {}) {
1541
+ let mermaidModulePromise = null;
1542
+ return {
1543
+ async renderHtml(html) {
1544
+ if (typeof DOMParser === "undefined") {
1545
+ throw new Error("当前环境不支持浏览器 Mermaid 渲染");
1546
+ }
1547
+ const parser = new DOMParser();
1548
+ const document = parser.parseFromString(`<body>${html}</body>`, "text/html");
1549
+ const root = document.body;
1550
+ const mermaid = await getMermaidModule();
1551
+ mermaid.initialize(createMermaidConfig(options.mermaidConfig));
1552
+ try {
1553
+ await replaceMermaidCodeBlocks(root, async ({ id, code }) => {
1554
+ const { svg } = await mermaid.render(id, code);
1555
+ return svg;
1556
+ });
1557
+ } catch (error) {
1558
+ throw createMermaidRenderError(error);
1559
+ }
1560
+ return root.innerHTML;
1561
+ }
1562
+ };
1563
+ async function getMermaidModule() {
1564
+ if (!mermaidModulePromise) {
1565
+ mermaidModulePromise = import("mermaid");
1566
+ }
1567
+ const module = await mermaidModulePromise;
1568
+ return module.default;
1569
+ }
1570
+ }
1571
+ function createMermaidRenderError(error) {
1572
+ const message = error instanceof Error ? error.message : String(error);
1573
+ return new Error(`Mermaid 图表渲染失败: ${message}`);
1574
+ }
1575
+ function createMermaidConfig(overrides = {}) {
1576
+ const flowchartOverrides = getRecord(overrides.flowchart);
1577
+ return {
1578
+ ...DEFAULT_MERMAID_CONFIG,
1579
+ startOnLoad: false,
1580
+ securityLevel: "strict",
1581
+ ...overrides,
1582
+ flowchart: {
1583
+ ...getRecord(DEFAULT_MERMAID_CONFIG.flowchart),
1584
+ ...flowchartOverrides
1585
+ }
1586
+ };
1587
+ }
1588
+ function getOwnerDocument(root) {
1589
+ if ("createElement" in root) {
1590
+ return root;
1591
+ }
1592
+ if ("ownerDocument" in root && root.ownerDocument) {
1593
+ return root.ownerDocument;
1594
+ }
1595
+ throw new Error("无法获取 Mermaid 渲染所需的 Document");
1596
+ }
1597
+ function getRecord(value) {
1598
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1599
+ return value;
1600
+ }
1601
+ return {};
1602
+ }
1172
1603
  async function createWenyanCore(options = {}) {
1173
- const { isConvertMathJax = true, isWechat = true } = options;
1604
+ const { isConvertMathJax = true, isWechat = true, mermaid } = options;
1174
1605
  const markedClient = createMarkedClient();
1175
1606
  const mathJaxParser = createMathJaxParser();
1607
+ const mermaidParser = createMermaidParser(mermaid);
1176
1608
  registerAllBuiltInThemes();
1177
1609
  registerBuiltInHlThemes();
1178
1610
  registerBuiltInMacStyle();
@@ -1181,11 +1613,11 @@ async function createWenyanCore(options = {}) {
1181
1613
  return await handleFrontMatter(markdown);
1182
1614
  },
1183
1615
  async renderMarkdown(markdown) {
1184
- const html = await markedClient.parse(markdown);
1616
+ let html = await markedClient.parse(markdown);
1185
1617
  if (isConvertMathJax) {
1186
- return mathJaxParser.parser(html);
1618
+ html = mathJaxParser.parser(html);
1187
1619
  }
1188
- return html;
1620
+ return await mermaidParser.parser(html);
1189
1621
  },
1190
1622
  async applyStylesWithTheme(wenyanElement, options2 = {}) {
1191
1623
  const {
@@ -1288,7 +1720,10 @@ const DEFAULT_CSS_UPDATES = {
1288
1720
  };
1289
1721
  export {
1290
1722
  addFootnotes,
1723
+ createBrowserMermaidRenderer,
1291
1724
  createCssModifier,
1725
+ createMermaidConfig,
1726
+ createMermaidRenderError,
1292
1727
  createWenyanCore,
1293
1728
  getAllGzhThemes,
1294
1729
  getAllHlThemes,
@@ -1305,6 +1740,7 @@ export {
1305
1740
  registerHlTheme,
1306
1741
  registerMacStyle,
1307
1742
  registerTheme,
1743
+ replaceMermaidCodeBlocks,
1308
1744
  sansSerif,
1309
1745
  serif
1310
1746
  };
@@ -1,7 +1,9 @@
1
- import { StyledContent } from "../node/types.js";
1
+ import { type FrontMatterResult } from "./parser/frontMatterParser.js";
2
+ import type { MermaidOptions } from "./mermaid.js";
2
3
  export interface WenyanOptions {
3
4
  isConvertMathJax?: boolean;
4
5
  isWechat?: boolean;
6
+ mermaid?: boolean | MermaidOptions;
5
7
  }
6
8
  export interface ApplyStylesOptions {
7
9
  themeId?: string;
@@ -12,7 +14,7 @@ export interface ApplyStylesOptions {
12
14
  isAddFootnote?: boolean;
13
15
  }
14
16
  export declare function createWenyanCore(options?: WenyanOptions): Promise<{
15
- handleFrontMatter(markdown: string): Promise<StyledContent>;
17
+ handleFrontMatter(markdown: string): Promise<FrontMatterResult>;
16
18
  renderMarkdown(markdown: string): Promise<string>;
17
19
  applyStylesWithTheme(wenyanElement: HTMLElement, options?: ApplyStylesOptions): Promise<string>;
18
20
  applyStylesWithResolvedCss(wenyanElement: HTMLElement, options: {
@@ -31,4 +33,6 @@ export * from "./platform/medium.js";
31
33
  export * from "./platform/zhihu.js";
32
34
  export * from "./platform/toutiao.js";
33
35
  export { addFootnotes } from "./renderer/footnotesRender.js";
36
+ export * from "./mermaid.js";
34
37
  export type WenyanCoreInstance = Awaited<ReturnType<typeof createWenyanCore>>;
38
+ export type { FrontMatterResult } from "./parser/frontMatterParser.js";
@@ -0,0 +1,19 @@
1
+ export interface MermaidDiagram {
2
+ id: string;
3
+ code: string;
4
+ }
5
+ export type MermaidDiagramRenderer = (diagram: MermaidDiagram) => Promise<string>;
6
+ export interface MermaidRenderer {
7
+ renderHtml(html: string): Promise<string>;
8
+ }
9
+ export interface MermaidOptions {
10
+ enabled?: boolean;
11
+ renderer?: MermaidRenderer;
12
+ }
13
+ export interface MermaidRendererFactoryOptions {
14
+ mermaidConfig?: Record<string, unknown>;
15
+ }
16
+ export declare function replaceMermaidCodeBlocks(root: ParentNode, renderDiagram: MermaidDiagramRenderer): Promise<boolean>;
17
+ export declare function createBrowserMermaidRenderer(options?: MermaidRendererFactoryOptions): MermaidRenderer;
18
+ export declare function createMermaidRenderError(error: unknown): Error;
19
+ export declare function createMermaidConfig(overrides?: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,9 @@
1
+ import type { MermaidOptions, MermaidRenderer } from "../mermaid.js";
2
+ export interface ResolvedMermaidOptions {
3
+ enabled: boolean;
4
+ renderer?: MermaidRenderer;
5
+ }
6
+ export declare function createMermaidParser(options?: boolean | MermaidOptions): {
7
+ parser(html: string): Promise<string>;
8
+ };
9
+ export declare function resolveMermaidOptions(options?: boolean | MermaidOptions): ResolvedMermaidOptions;
@@ -0,0 +1,2 @@
1
+ import { type MermaidRenderer, type MermaidRendererFactoryOptions } from "../core/mermaid.js";
2
+ export declare function createNodeMermaidRenderer(options?: MermaidRendererFactoryOptions): MermaidRenderer;
@@ -1,4 +1,4 @@
1
- import { ApplyStylesOptions } from "../core/index.js";
1
+ import { ApplyStylesOptions, WenyanCoreInstance } from "../core/index.js";
2
2
  import { GetInputContentFn, RenderContext, RenderOptions } from "./types.js";
3
- export declare function renderStyledContent(content: string, options?: ApplyStylesOptions): Promise<string>;
3
+ export declare function renderStyledContent(content: string, options?: ApplyStylesOptions, coreInstance?: WenyanCoreInstance): Promise<string>;
4
4
  export declare function prepareRenderContext(inputContent: string | undefined, options: RenderOptions, getInputContent: GetInputContentFn): Promise<RenderContext>;
@@ -1,3 +1,4 @@
1
+ import { FrontMatterResult } from "../core/parser/frontMatterParser.js";
1
2
  export interface RenderOptions {
2
3
  file?: string;
3
4
  theme?: string;
@@ -5,6 +6,7 @@ export interface RenderOptions {
5
6
  highlight: string;
6
7
  macStyle: boolean;
7
8
  footnote: boolean;
9
+ mermaid?: boolean;
8
10
  }
9
11
  export interface PublishOptions extends RenderOptions {
10
12
  appId?: string;
@@ -18,17 +20,7 @@ export interface RenderContext {
18
20
  gzhContent: StyledContent;
19
21
  absoluteDirPath: string | undefined;
20
22
  }
21
- export interface StyledContent {
22
- content: string;
23
- title?: string;
24
- description?: string;
25
- cover?: string;
26
- author?: string;
27
- source_url?: string;
28
- need_open_comment?: boolean;
29
- only_fans_can_comment?: boolean;
30
- image_list?: string[];
31
- }
23
+ export type StyledContent = FrontMatterResult;
32
24
  export type GetInputContentFn = (inputContent?: string, filePath?: string) => Promise<{
33
25
  content: string;
34
26
  absoluteDirPath?: string;
package/dist/wrapper.js CHANGED
@@ -10,7 +10,7 @@ import { FormDataEncoder } from "form-data-encoder";
10
10
  import { FormData } from "formdata-node";
11
11
  import { Readable } from "node:stream";
12
12
  import { defaultTokenCache, defaultCredential, WechatPublisher, CredentialStore } from "./publish.js";
13
- import { createWenyanCore, getAllGzhThemes } from "./core.js";
13
+ import { replaceMermaidCodeBlocks, createMermaidRenderError, createMermaidConfig, createWenyanCore, getAllGzhThemes } from "./core.js";
14
14
  const configDir = (() => {
15
15
  if (process.env.XDG_CONFIG_HOME) {
16
16
  return path.join(process.env.XDG_CONFIG_HOME, "wenyan-md");
@@ -660,7 +660,169 @@ class ConfigStore {
660
660
  }
661
661
  }
662
662
  const configStore = new ConfigStore();
663
- const wenyanCoreInstance = await createWenyanCore();
663
+ function createNodeMermaidRenderer(options = {}) {
664
+ const dom = new JSDOM("<body></body>", { pretendToBeVisual: true });
665
+ const runtimeWindow = dom.window;
666
+ let renderQueue = Promise.resolve();
667
+ let mermaidPromise = null;
668
+ installMermaidGlobals(runtimeWindow);
669
+ return {
670
+ async renderHtml(html) {
671
+ const task = renderQueue.then(async () => {
672
+ const mermaid = await getMermaid();
673
+ dom.window.document.body.innerHTML = html;
674
+ try {
675
+ await replaceMermaidCodeBlocks(dom.window.document.body, async ({ id, code }) => {
676
+ const { svg } = await mermaid.render(id, code);
677
+ return svg;
678
+ });
679
+ return dom.window.document.body.innerHTML;
680
+ } catch (error) {
681
+ throw createMermaidRenderError(error);
682
+ } finally {
683
+ dom.window.document.body.innerHTML = "";
684
+ }
685
+ });
686
+ renderQueue = task.then(
687
+ () => void 0,
688
+ () => void 0
689
+ );
690
+ return await task;
691
+ }
692
+ };
693
+ async function getMermaid() {
694
+ if (!mermaidPromise) {
695
+ mermaidPromise = import("mermaid").then((module) => module.default);
696
+ }
697
+ const mermaid = await mermaidPromise;
698
+ mermaid.initialize(createMermaidConfig(options.mermaidConfig));
699
+ return mermaid;
700
+ }
701
+ }
702
+ function installMermaidGlobals(window) {
703
+ ensureSvgPolyfills(window);
704
+ const globals = [
705
+ ["window", window],
706
+ ["document", window.document],
707
+ ["navigator", window.navigator],
708
+ ["Element", window.Element],
709
+ ["HTMLElement", window.HTMLElement],
710
+ ["SVGElement", window.SVGElement],
711
+ ["Node", window.Node],
712
+ ["Document", window.Document],
713
+ ["DOMParser", window.DOMParser],
714
+ ["XMLSerializer", window.XMLSerializer],
715
+ ["getComputedStyle", window.getComputedStyle.bind(window)]
716
+ ];
717
+ for (const [key, value] of globals) {
718
+ Object.defineProperty(globalThis, key, {
719
+ configurable: true,
720
+ writable: true,
721
+ value
722
+ });
723
+ }
724
+ }
725
+ function ensureSvgPolyfills(window) {
726
+ const svgElementPrototype = window.SVGElement.prototype;
727
+ if (!svgElementPrototype.getBBox) {
728
+ Object.defineProperty(svgElementPrototype, "getBBox", {
729
+ configurable: true,
730
+ writable: true,
731
+ value() {
732
+ const tagName = this.tagName.toLowerCase();
733
+ if (tagName === "text" || tagName === "tspan") {
734
+ const text = this.textContent ?? "";
735
+ const width = Math.max(text.length * 8, 16);
736
+ return {
737
+ x: 0,
738
+ y: -16,
739
+ width,
740
+ height: 16,
741
+ top: -16,
742
+ right: width,
743
+ bottom: 0,
744
+ left: 0,
745
+ toJSON() {
746
+ return this;
747
+ }
748
+ };
749
+ }
750
+ if (tagName === "style" || tagName === "defs" || tagName === "marker") {
751
+ return { x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0, toJSON() {
752
+ return this;
753
+ } };
754
+ }
755
+ if (tagName === "rect" || tagName === "image") {
756
+ const w = parseFloat(this.getAttribute("width") || "0");
757
+ const h = parseFloat(this.getAttribute("height") || "0");
758
+ const x = parseFloat(this.getAttribute("x") || "0");
759
+ const y = parseFloat(this.getAttribute("y") || "0");
760
+ return { x, y, width: w, height: h, top: y, right: x + w, bottom: y + h, left: x, toJSON() {
761
+ return this;
762
+ } };
763
+ }
764
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
765
+ let hasChildren = false;
766
+ for (let i = 0; i < this.children.length; i++) {
767
+ const child = this.children[i];
768
+ if (!child.getBBox) continue;
769
+ const childBox = child.getBBox();
770
+ if (childBox.width === 0 && childBox.height === 0) continue;
771
+ let tx = 0, ty = 0;
772
+ const transform = child.getAttribute("transform");
773
+ if (transform) {
774
+ const match = transform.match(/translate\(([-0-9.]+)(?:[,\s]+([-0-9.]+))?\)/);
775
+ if (match) {
776
+ tx = parseFloat(match[1]);
777
+ ty = match[2] ? parseFloat(match[2]) : 0;
778
+ }
779
+ }
780
+ minX = Math.min(minX, childBox.x + tx);
781
+ minY = Math.min(minY, childBox.y + ty);
782
+ maxX = Math.max(maxX, childBox.x + childBox.width + tx);
783
+ maxY = Math.max(maxY, childBox.y + childBox.height + ty);
784
+ hasChildren = true;
785
+ }
786
+ if (!hasChildren) {
787
+ return { x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0, toJSON() {
788
+ return this;
789
+ } };
790
+ }
791
+ return {
792
+ x: minX,
793
+ y: minY,
794
+ width: maxX - minX,
795
+ height: maxY - minY,
796
+ top: minY,
797
+ right: maxX,
798
+ bottom: maxY,
799
+ left: minX,
800
+ toJSON() {
801
+ return this;
802
+ }
803
+ };
804
+ }
805
+ });
806
+ }
807
+ if (!svgElementPrototype.getComputedTextLength) {
808
+ Object.defineProperty(svgElementPrototype, "getComputedTextLength", {
809
+ configurable: true,
810
+ writable: true,
811
+ value() {
812
+ const text = this.textContent ?? "";
813
+ return Math.max(text.length * 8, 16);
814
+ }
815
+ });
816
+ }
817
+ }
818
+ const nodeMermaidRenderer = createNodeMermaidRenderer();
819
+ const wenyanCoreInstance = await createWenyanCore({
820
+ mermaid: {
821
+ enabled: true,
822
+ renderer: nodeMermaidRenderer
823
+ }
824
+ });
825
+ const wenyanCoreInstanceWithoutMermaid = await createWenyanCore();
664
826
  async function renderWithTheme(markdownContent, options) {
665
827
  if (!markdownContent) {
666
828
  throw new Error("No content provided for rendering.");
@@ -676,21 +838,22 @@ async function renderWithTheme(markdownContent, options) {
676
838
  if (!handledCustomTheme && !theme) {
677
839
  throw new Error(`theme "${theme}" not found.`);
678
840
  }
841
+ const coreInstance = options.mermaid === false ? wenyanCoreInstanceWithoutMermaid : wenyanCoreInstance;
679
842
  const gzhContent = await renderStyledContent(markdownContent, {
680
843
  themeId: theme,
681
844
  hlThemeId: highlight,
682
845
  isMacStyle: macStyle,
683
846
  isAddFootnote: footnote,
684
847
  themeCss: handledCustomTheme
685
- });
848
+ }, coreInstance);
686
849
  return gzhContent;
687
850
  }
688
- async function renderStyledContent(content, options = {}) {
689
- const html = await wenyanCoreInstance.renderMarkdown(content);
851
+ async function renderStyledContent(content, options = {}, coreInstance = wenyanCoreInstance) {
852
+ const html = await coreInstance.renderMarkdown(content);
690
853
  const dom = new JSDOM(`<body><section id="wenyan">${html}</section></body>`);
691
854
  const document = dom.window.document;
692
855
  const wenyan = document.getElementById("wenyan");
693
- const result = await wenyanCoreInstance.applyStylesWithTheme(wenyan, options);
856
+ const result = await coreInstance.applyStylesWithTheme(wenyan, options);
694
857
  return result;
695
858
  }
696
859
  async function prepareRenderContext(inputContent, options, getInputContent) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenyan-md/core",
3
- "version": "3.0.6",
3
+ "version": "3.0.7",
4
4
  "description": "Core library for Wenyan markdown rendering & publishing",
5
5
  "author": "Lei <caol64@gmail.com> (https://github.com/caol64)",
6
6
  "license": "Apache-2.0",
@@ -65,7 +65,8 @@
65
65
  "highlight.js": "11.10.0",
66
66
  "marked": "^15.0.12",
67
67
  "marked-highlight": "^2.2.1",
68
- "mathjax-full": "3.2.2"
68
+ "mathjax-full": "3.2.2",
69
+ "mermaid": "11.14.0"
69
70
  },
70
71
  "peerDependencies": {
71
72
  "form-data-encoder": "^4.1.0",