@wenyan-md/core 3.0.6 → 3.0.8

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,40 @@ 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
+ const text = match[2];
450
+ const inner = match[3].trim();
451
+ let href = "";
452
+ let title = "";
453
+ const titleMatch = inner.match(/(.*?)\s+["']([^"']*)["']$/);
454
+ if (titleMatch) {
455
+ href = titleMatch[1].trim();
456
+ title = titleMatch[2];
457
+ } else {
458
+ href = inner;
459
+ }
460
+ return {
461
+ type: isImage ? "image" : "link",
462
+ raw: match[0],
463
+ text,
464
+ href,
465
+ title,
466
+ tokens: this.lexer.inlineTokens(text)
467
+ };
468
+ }
469
+ },
470
+ // 自定义图片语法扩展 ![](){...}
437
471
  {
438
472
  name: "attributeImage",
439
473
  level: "inline",
@@ -462,7 +496,8 @@ function createMarkedClient() {
462
496
  renderer(token) {
463
497
  const attrs = stringToMap(token.attrs);
464
498
  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}">`;
499
+ const href = normalizeHref(token.href);
500
+ return `<img src="${href}" alt="${token.text || ""}" title="${token.text || ""}" style="${styleStr}">`;
466
501
  }
467
502
  }
468
503
  ]
@@ -490,7 +525,12 @@ function createMarkedClient() {
490
525
  },
491
526
  // 重写普通图片 (处理标准 Markdown 图片)
492
527
  image(token) {
493
- return `<img src="${token.href}" alt="${token.text || ""}" title="${token.title || token.text || ""}">`;
528
+ const href = normalizeHref(token.href);
529
+ return `<img src="${href}" alt="${token.text || ""}" title="${token.title || token.text || ""}">`;
530
+ },
531
+ link(token) {
532
+ const href = normalizeHref(token.href);
533
+ return `<a href="${href}">${this.parser.parseInline(token.tokens)}</a>`;
494
534
  }
495
535
  }
496
536
  });
@@ -503,10 +543,21 @@ function createMarkedClient() {
503
543
  */
504
544
  async parse(markdown) {
505
545
  await configure();
506
- return md.parse(markdown);
546
+ return await md.parse(markdown);
507
547
  }
508
548
  };
509
549
  }
550
+ function normalizeHref(href) {
551
+ href = href.trim();
552
+ if (href.startsWith("<") && href.endsWith(">")) {
553
+ href = href.slice(1, -1);
554
+ }
555
+ try {
556
+ return encodeURI(href);
557
+ } catch {
558
+ return href;
559
+ }
560
+ }
510
561
  let htmlHandlerRegistered = false;
511
562
  function createMathJaxParser(options = {}) {
512
563
  const adaptor = liteAdaptor();
@@ -565,6 +616,36 @@ function createMathJaxParser(options = {}) {
565
616
  }
566
617
  };
567
618
  }
619
+ function createMermaidParser(options) {
620
+ const resolvedOptions = resolveMermaidOptions(options);
621
+ return {
622
+ async parser(html) {
623
+ if (!resolvedOptions.enabled || !containsMermaidCodeBlock(html)) {
624
+ return html;
625
+ }
626
+ if (!resolvedOptions.renderer) {
627
+ throw new Error("Mermaid 渲染已启用,但未配置 renderer");
628
+ }
629
+ return await resolvedOptions.renderer.renderHtml(html);
630
+ }
631
+ };
632
+ }
633
+ function resolveMermaidOptions(options) {
634
+ if (options === void 0) {
635
+ return { enabled: false };
636
+ }
637
+ if (typeof options === "boolean") {
638
+ return { enabled: options };
639
+ }
640
+ return {
641
+ enabled: options.enabled ?? true,
642
+ renderer: options.renderer
643
+ };
644
+ }
645
+ function containsMermaidCodeBlock(html) {
646
+ return html.includes("language-mermaid");
647
+ }
648
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
568
649
  function wechatPostRender(element) {
569
650
  const mathElements = element.querySelectorAll("mjx-container");
570
651
  mathElements.forEach((mathContainer) => {
@@ -583,6 +664,13 @@ function wechatPostRender(element) {
583
664
  }
584
665
  }
585
666
  });
667
+ const mermaidSvgs = element.querySelectorAll('[data-wenyan-diagram="mermaid"] svg');
668
+ mermaidSvgs.forEach((svg) => {
669
+ svg.style.maxWidth = "100%";
670
+ svg.style.height = "auto";
671
+ inlineMermaidSvgStyles(svg);
672
+ flattenMermaidMarkers(svg);
673
+ });
586
674
  const codeElements = element.querySelectorAll("pre code");
587
675
  codeElements.forEach((codeEl) => {
588
676
  codeEl.innerHTML = codeEl.innerHTML.replace(/\n/g, "<br>").replace(/(>[^<]+)|(^[^<]+)/g, (str) => str.replace(/\s/g, "&nbsp;"));
@@ -599,6 +687,268 @@ function wechatPostRender(element) {
599
687
  element.style.color = "rgb(0, 0, 0)";
600
688
  element.style.caretColor = "rgb(0, 0, 0)";
601
689
  }
690
+ function inlineMermaidSvgStyles(svg) {
691
+ const styleElements = Array.from(svg.querySelectorAll("style"));
692
+ styleElements.forEach((styleElement) => {
693
+ applyInlineStylesFromCss(svg, styleElement.textContent ?? "");
694
+ styleElement.remove();
695
+ });
696
+ }
697
+ function flattenMermaidMarkers(svg) {
698
+ const convertedMarkerIds = /* @__PURE__ */ new Set();
699
+ const markedElements = Array.from(svg.querySelectorAll("[marker-start], [marker-end]"));
700
+ markedElements.forEach((markedElement) => {
701
+ convertMarkerReference(svg, markedElement, "start", convertedMarkerIds);
702
+ convertMarkerReference(svg, markedElement, "end", convertedMarkerIds);
703
+ });
704
+ convertedMarkerIds.forEach((markerId) => {
705
+ findMarkerById(svg, markerId)?.remove();
706
+ });
707
+ svg.querySelectorAll("defs").forEach((defs) => {
708
+ if (!defs.children.length) {
709
+ defs.remove();
710
+ }
711
+ });
712
+ }
713
+ function convertMarkerReference(svg, sourceElement, position, convertedMarkerIds) {
714
+ const markerAttribute = `marker-${position}`;
715
+ const markerValue = sourceElement.getAttribute(markerAttribute);
716
+ if (!markerValue) {
717
+ return;
718
+ }
719
+ const markerId = extractMarkerId(markerValue);
720
+ if (!markerId) {
721
+ return;
722
+ }
723
+ const marker = findMarkerById(svg, markerId);
724
+ if (!marker) {
725
+ return;
726
+ }
727
+ const flattenedMarker = createFlattenedMarker(svg, sourceElement, marker, position);
728
+ if (!flattenedMarker) {
729
+ return;
730
+ }
731
+ sourceElement.parentNode?.appendChild(flattenedMarker);
732
+ sourceElement.removeAttribute(markerAttribute);
733
+ convertedMarkerIds.add(markerId);
734
+ }
735
+ function createFlattenedMarker(svg, sourceElement, marker, position) {
736
+ const terminalPoints = getTerminalPoints(sourceElement, position);
737
+ if (!terminalPoints) {
738
+ return null;
739
+ }
740
+ const ownerDocument = svg.ownerDocument;
741
+ const markerGroup = ownerDocument.createElementNS(SVG_NAMESPACE, "g");
742
+ const markerStyle = marker.getAttribute("style");
743
+ const angle = getMarkerAngleDegrees(position, terminalPoints.anchor, terminalPoints.reference, marker);
744
+ markerGroup.setAttribute("data-wenyan-marker", position);
745
+ markerGroup.setAttribute("transform", buildMarkerTransform(marker, terminalPoints.anchor, angle, position));
746
+ if (markerStyle) {
747
+ markerGroup.setAttribute("style", markerStyle);
748
+ }
749
+ Array.from(marker.children).forEach((child) => {
750
+ markerGroup.appendChild(child.cloneNode(true));
751
+ });
752
+ return markerGroup;
753
+ }
754
+ function getTerminalPoints(sourceElement, position) {
755
+ const points = getEdgePoints(sourceElement);
756
+ if (points.length < 2) {
757
+ return null;
758
+ }
759
+ if (position === "end") {
760
+ const anchor2 = points.at(-1);
761
+ if (!anchor2) {
762
+ return null;
763
+ }
764
+ const reference2 = findDistinctPoint(points, points.length - 2, -1, anchor2);
765
+ return reference2 ? { anchor: anchor2, reference: reference2 } : null;
766
+ }
767
+ const anchor = points[0];
768
+ if (!anchor) {
769
+ return null;
770
+ }
771
+ const reference = findDistinctPoint(points, 1, 1, anchor);
772
+ return reference ? { anchor, reference } : null;
773
+ }
774
+ function getEdgePoints(sourceElement) {
775
+ const encodedPoints = sourceElement.getAttribute("data-points");
776
+ if (encodedPoints) {
777
+ const decodedPoints = decodeMermaidPoints(sourceElement, encodedPoints);
778
+ if (decodedPoints.length >= 2) {
779
+ return decodedPoints;
780
+ }
781
+ }
782
+ const polyPoints = sourceElement.getAttribute("points");
783
+ if (polyPoints) {
784
+ return parseCoordinatePairs(polyPoints);
785
+ }
786
+ const pathData = sourceElement.getAttribute("d");
787
+ if (pathData) {
788
+ return parseCoordinatePairs(pathData);
789
+ }
790
+ return [];
791
+ }
792
+ function decodeMermaidPoints(sourceElement, encodedPoints) {
793
+ const defaultView = sourceElement.ownerDocument.defaultView;
794
+ try {
795
+ const decoded = defaultView?.atob?.(encodedPoints) ?? (typeof atob === "function" ? atob(encodedPoints) : "");
796
+ const parsed = JSON.parse(decoded);
797
+ return parsed.filter((point) => typeof point.x === "number" && typeof point.y === "number").map((point) => ({
798
+ x: point.x,
799
+ y: point.y
800
+ }));
801
+ } catch {
802
+ return [];
803
+ }
804
+ }
805
+ function parseCoordinatePairs(value) {
806
+ const matches = Array.from(value.matchAll(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi), (match) => Number(match[0]));
807
+ const points = [];
808
+ for (let index = 0; index + 1 < matches.length; index += 2) {
809
+ points.push({
810
+ x: matches[index],
811
+ y: matches[index + 1]
812
+ });
813
+ }
814
+ return points;
815
+ }
816
+ function findDistinctPoint(points, startIndex, step, anchor) {
817
+ for (let index = startIndex; index >= 0 && index < points.length; index += step) {
818
+ const point = points[index];
819
+ if (!point) {
820
+ continue;
821
+ }
822
+ if (point.x !== anchor.x || point.y !== anchor.y) {
823
+ return point;
824
+ }
825
+ }
826
+ return null;
827
+ }
828
+ function getMarkerAngleDegrees(position, anchor, reference, marker) {
829
+ const baseAngle = position === "end" ? Math.atan2(anchor.y - reference.y, anchor.x - reference.x) : Math.atan2(reference.y - anchor.y, reference.x - anchor.x);
830
+ const orient = marker.getAttribute("orient")?.trim();
831
+ let angle = baseAngle * 180 / Math.PI;
832
+ if (orient === "auto-start-reverse" && position === "start") {
833
+ angle += 180;
834
+ } else if (orient && orient !== "auto") {
835
+ const offset = Number.parseFloat(orient);
836
+ if (Number.isFinite(offset)) {
837
+ angle += offset;
838
+ }
839
+ }
840
+ return angle;
841
+ }
842
+ function buildMarkerTransform(marker, anchor, angle, position) {
843
+ const viewBox = parseViewBox(marker.getAttribute("viewBox"));
844
+ const markerWidth = parseNumericAttribute(marker.getAttribute("markerWidth"), viewBox?.width ?? 1);
845
+ const markerHeight = parseNumericAttribute(marker.getAttribute("markerHeight"), viewBox?.height ?? 1);
846
+ const viewBoxWidth = viewBox?.width ?? markerWidth;
847
+ const viewBoxHeight = viewBox?.height ?? markerHeight;
848
+ const scaleX = viewBoxWidth === 0 ? 1 : markerWidth / viewBoxWidth;
849
+ const scaleY = viewBoxHeight === 0 ? 1 : markerHeight / viewBoxHeight;
850
+ const offsetX = parseNumericAttribute(marker.getAttribute("refX")) + (viewBox?.minX ?? 0);
851
+ const offsetY = parseNumericAttribute(marker.getAttribute("refY")) + (viewBox?.minY ?? 0);
852
+ const adjustedAnchor = adjustMarkerAnchor(anchor, angle, scaleX, viewBox, offsetX, position);
853
+ return [
854
+ `translate(${adjustedAnchor.x} ${adjustedAnchor.y})`,
855
+ `rotate(${angle})`,
856
+ `scale(${scaleX} ${scaleY})`,
857
+ `translate(${-offsetX} ${-offsetY})`
858
+ ].join(" ");
859
+ }
860
+ function adjustMarkerAnchor(anchor, angle, scaleX, viewBox, refX, position) {
861
+ const minX = viewBox?.minX ?? 0;
862
+ const maxX = viewBox ? viewBox.minX + viewBox.width : refX;
863
+ const overlap = position === "start" ? Math.max(0, refX - minX) : Math.max(0, maxX - refX);
864
+ if (overlap === 0) {
865
+ return anchor;
866
+ }
867
+ const distance = overlap * scaleX;
868
+ const radians = angle * Math.PI / 180;
869
+ const direction = position === "start" ? 1 : -1;
870
+ return {
871
+ x: anchor.x + Math.cos(radians) * distance * direction,
872
+ y: anchor.y + Math.sin(radians) * distance * direction
873
+ };
874
+ }
875
+ function parseViewBox(viewBoxValue) {
876
+ if (!viewBoxValue) {
877
+ return null;
878
+ }
879
+ const values = viewBoxValue.trim().split(/[\s,]+/).map((value) => Number.parseFloat(value));
880
+ if (values.length !== 4 || values.some((value) => !Number.isFinite(value))) {
881
+ return null;
882
+ }
883
+ return {
884
+ minX: values[0],
885
+ minY: values[1],
886
+ width: values[2],
887
+ height: values[3]
888
+ };
889
+ }
890
+ function parseNumericAttribute(value, fallback = 0) {
891
+ if (!value) {
892
+ return fallback;
893
+ }
894
+ const parsed = Number.parseFloat(value);
895
+ return Number.isFinite(parsed) ? parsed : fallback;
896
+ }
897
+ function extractMarkerId(markerValue) {
898
+ const match = markerValue.match(/url\(#([^)]+)\)/);
899
+ return match?.[1] ?? null;
900
+ }
901
+ function findMarkerById(svg, markerId) {
902
+ return Array.from(svg.querySelectorAll("marker")).find((marker) => marker.getAttribute("id") === markerId);
903
+ }
904
+ function applyInlineStylesFromCss(svg, cssText) {
905
+ const ast = csstree.parse(cssText, parseOptions);
906
+ csstree.walk(ast, {
907
+ visit: "Rule",
908
+ enter(node) {
909
+ if (node.prelude.type !== "SelectorList") return;
910
+ const declarations = node.block.children.toArray().filter((decl) => decl.type === "Declaration");
911
+ if (declarations.length === 0) return;
912
+ node.prelude.children.forEach((selectorNode) => {
913
+ const selector = csstree.generate(selectorNode).trim();
914
+ if (!selector || selector.includes("::")) return;
915
+ const targets = resolveInlineTargets(svg, selector);
916
+ targets.forEach((target) => {
917
+ declarations.forEach((decl) => {
918
+ const value = csstree.generate(decl.value);
919
+ const priority = decl.important ? "important" : "";
920
+ target.style.setProperty(decl.property, value, priority);
921
+ });
922
+ });
923
+ });
924
+ }
925
+ });
926
+ }
927
+ function resolveInlineTargets(svg, selector) {
928
+ if (selector === ":root") {
929
+ return [svg];
930
+ }
931
+ if (selector.includes(":")) {
932
+ return [];
933
+ }
934
+ const targets = /* @__PURE__ */ new Set();
935
+ if (svg.matches(selector)) {
936
+ targets.add(svg);
937
+ }
938
+ svg.querySelectorAll(selector).forEach((node) => {
939
+ if (isInlineStyleTarget(node)) {
940
+ targets.add(node);
941
+ }
942
+ });
943
+ return Array.from(targets);
944
+ }
945
+ function isInlineStyleTarget(node) {
946
+ const defaultView = node.ownerDocument.defaultView;
947
+ if (!defaultView) {
948
+ return false;
949
+ }
950
+ return node instanceof defaultView.SVGElement || node instanceof defaultView.HTMLElement;
951
+ }
602
952
  const themeModifier = createCssModifier({});
603
953
  function renderTheme(wenyanElement, themeCss) {
604
954
  const modifiedCss = themeModifier(themeCss);
@@ -1169,10 +1519,105 @@ function getContentForToutiao(wenyanElement) {
1169
1519
  });
1170
1520
  return wenyanElement.outerHTML;
1171
1521
  }
1522
+ const DEFAULT_MERMAID_CONFIG = {
1523
+ htmlLabels: false,
1524
+ flowchart: {
1525
+ htmlLabels: false
1526
+ }
1527
+ };
1528
+ async function replaceMermaidCodeBlocks(root, renderDiagram) {
1529
+ const codeBlocks = Array.from(root.querySelectorAll("pre > code.language-mermaid"));
1530
+ if (codeBlocks.length === 0) {
1531
+ return false;
1532
+ }
1533
+ const ownerDocument = getOwnerDocument(root);
1534
+ for (const [_, codeBlock] of codeBlocks.entries()) {
1535
+ const pre = codeBlock.parentElement;
1536
+ const parent = pre?.parentElement;
1537
+ if (!pre || !parent) {
1538
+ continue;
1539
+ }
1540
+ const code = codeBlock.textContent ?? "";
1541
+ const svg = await renderDiagram({
1542
+ id: `wenyan-mermaid-${crypto.randomUUID()}`,
1543
+ code
1544
+ });
1545
+ const figure = ownerDocument.createElement("figure");
1546
+ figure.setAttribute("data-wenyan-diagram", "mermaid");
1547
+ figure.innerHTML = svg.trim();
1548
+ pre.replaceWith(figure);
1549
+ }
1550
+ return true;
1551
+ }
1552
+ function createBrowserMermaidRenderer(options = {}) {
1553
+ let mermaidModulePromise = null;
1554
+ return {
1555
+ async renderHtml(html) {
1556
+ if (typeof DOMParser === "undefined") {
1557
+ throw new Error("当前环境不支持浏览器 Mermaid 渲染");
1558
+ }
1559
+ const parser = new DOMParser();
1560
+ const document = parser.parseFromString(`<body>${html}</body>`, "text/html");
1561
+ const root = document.body;
1562
+ const mermaid = await getMermaidModule();
1563
+ try {
1564
+ await replaceMermaidCodeBlocks(root, async ({ id, code }) => {
1565
+ const { svg } = await mermaid.render(id, code);
1566
+ return svg;
1567
+ });
1568
+ } catch (error) {
1569
+ throw createMermaidRenderError(error);
1570
+ }
1571
+ return root.innerHTML;
1572
+ }
1573
+ };
1574
+ async function getMermaidModule() {
1575
+ if (!mermaidModulePromise) {
1576
+ mermaidModulePromise = import("mermaid");
1577
+ }
1578
+ const module = await mermaidModulePromise;
1579
+ const mermaid = module.default;
1580
+ mermaid.initialize(createMermaidConfig(options.mermaidConfig));
1581
+ return mermaid;
1582
+ }
1583
+ }
1584
+ function createMermaidRenderError(error) {
1585
+ const message = error instanceof Error ? error.message : String(error);
1586
+ return new Error(`Mermaid 图表渲染失败: ${message}`);
1587
+ }
1588
+ function createMermaidConfig(overrides = {}) {
1589
+ const flowchartOverrides = getRecord(overrides.flowchart);
1590
+ return {
1591
+ ...DEFAULT_MERMAID_CONFIG,
1592
+ startOnLoad: false,
1593
+ securityLevel: "strict",
1594
+ ...overrides,
1595
+ flowchart: {
1596
+ ...getRecord(DEFAULT_MERMAID_CONFIG.flowchart),
1597
+ ...flowchartOverrides
1598
+ }
1599
+ };
1600
+ }
1601
+ function getOwnerDocument(root) {
1602
+ if ("createElement" in root) {
1603
+ return root;
1604
+ }
1605
+ if ("ownerDocument" in root && root.ownerDocument) {
1606
+ return root.ownerDocument;
1607
+ }
1608
+ throw new Error("无法获取 Mermaid 渲染所需的 Document");
1609
+ }
1610
+ function getRecord(value) {
1611
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1612
+ return value;
1613
+ }
1614
+ return {};
1615
+ }
1172
1616
  async function createWenyanCore(options = {}) {
1173
- const { isConvertMathJax = true, isWechat = true } = options;
1617
+ const { isConvertMathJax = true, isWechat = true, mermaid } = options;
1174
1618
  const markedClient = createMarkedClient();
1175
1619
  const mathJaxParser = createMathJaxParser();
1620
+ const mermaidParser = createMermaidParser(mermaid);
1176
1621
  registerAllBuiltInThemes();
1177
1622
  registerBuiltInHlThemes();
1178
1623
  registerBuiltInMacStyle();
@@ -1181,11 +1626,11 @@ async function createWenyanCore(options = {}) {
1181
1626
  return await handleFrontMatter(markdown);
1182
1627
  },
1183
1628
  async renderMarkdown(markdown) {
1184
- const html = await markedClient.parse(markdown);
1629
+ let html = await markedClient.parse(markdown);
1185
1630
  if (isConvertMathJax) {
1186
- return mathJaxParser.parser(html);
1631
+ html = mathJaxParser.parser(html);
1187
1632
  }
1188
- return html;
1633
+ return await mermaidParser.parser(html);
1189
1634
  },
1190
1635
  async applyStylesWithTheme(wenyanElement, options2 = {}) {
1191
1636
  const {
@@ -1288,7 +1733,10 @@ const DEFAULT_CSS_UPDATES = {
1288
1733
  };
1289
1734
  export {
1290
1735
  addFootnotes,
1736
+ createBrowserMermaidRenderer,
1291
1737
  createCssModifier,
1738
+ createMermaidConfig,
1739
+ createMermaidRenderError,
1292
1740
  createWenyanCore,
1293
1741
  getAllGzhThemes,
1294
1742
  getAllHlThemes,
@@ -1305,6 +1753,7 @@ export {
1305
1753
  registerHlTheme,
1306
1754
  registerMacStyle,
1307
1755
  registerTheme,
1756
+ replaceMermaidCodeBlocks,
1308
1757
  sansSerif,
1309
1758
  serif
1310
1759
  };
package/dist/publish.js CHANGED
@@ -16,7 +16,7 @@ class TokenStore {
16
16
  try {
17
17
  const loadedData = await this.adapter.loadToken();
18
18
  if (loadedData) {
19
- this.cache = loadedData;
19
+ this.cache = { ...defaultTokenCache, ...loadedData };
20
20
  }
21
21
  } catch (error) {
22
22
  throw new Error(`无法加载 token: ${error instanceof Error ? error.message : String(error)}`);
@@ -32,15 +32,16 @@ class TokenStore {
32
32
  async waitForInit() {
33
33
  await this.initPromise;
34
34
  }
35
- isValid(appid) {
35
+ async isValid(appid) {
36
+ await this.initPromise;
36
37
  const currentTime = Math.floor(Date.now() / 1e3);
37
38
  const bufferTime = 600;
38
39
  const isAppidMatch = this.cache.appid === appid;
39
- const isNotExpired = this.cache.expireAt > currentTime + bufferTime;
40
+ const isNotExpired = this.cache.expireAt < 0 ? true : this.cache.expireAt > currentTime + bufferTime;
40
41
  return isAppidMatch && isNotExpired;
41
42
  }
42
- getToken(appid) {
43
- return this.isValid(appid) ? this.cache.accessToken : null;
43
+ async getToken(appid) {
44
+ return await this.isValid(appid) ? this.cache.accessToken : null;
44
45
  }
45
46
  async setToken(appid, accessToken, expiresIn) {
46
47
  await this.initPromise;
@@ -52,6 +53,16 @@ class TokenStore {
52
53
  };
53
54
  await this.save();
54
55
  }
56
+ async setExternalToken(appid, accessToken) {
57
+ await this.initPromise;
58
+ this.cache = {
59
+ appid,
60
+ accessToken,
61
+ expireAt: -1
62
+ // 标记为外部托管,永不校验过期
63
+ };
64
+ await this.save();
65
+ }
55
66
  async clear() {
56
67
  await this.initPromise;
57
68
  this.cache = { ...defaultTokenCache };
@@ -115,12 +126,14 @@ class CredentialStore {
115
126
  constructor(adapter) {
116
127
  this.adapter = adapter;
117
128
  this.initPromise = this.load();
129
+ this.initPromise.catch(() => {
130
+ });
118
131
  }
119
132
  async load() {
120
133
  try {
121
134
  const loadedData = await this.adapter.loadCredential();
122
135
  if (loadedData) {
123
- this.credential = loadedData;
136
+ this.credential = { ...defaultCredential, ...loadedData };
124
137
  }
125
138
  } catch (error) {
126
139
  throw new Error(`无法加载凭据: ${error instanceof Error ? error.message : String(error)}`);
@@ -133,27 +146,47 @@ class CredentialStore {
133
146
  throw new Error(`无法保存凭据: ${error instanceof Error ? error.message : String(error)}`);
134
147
  }
135
148
  }
136
- async _getWechatCredential() {
149
+ /**
150
+ * 获取微信凭据 (通过 appId 或 alias)
151
+ */
152
+ async getWechatCredential(appIdOrAlias) {
137
153
  await this.initPromise;
138
- return this.credential.wechat ?? {};
139
- }
140
- async getWechatCredential(appId) {
141
- const wechat = await this._getWechatCredential();
142
- if (!wechat) return null;
143
- const appSecret = wechat[appId];
144
- if (!appSecret) return null;
145
- return { appId, appSecret };
154
+ const wechat = this.credential.wechat ?? {};
155
+ if (wechat[appIdOrAlias]) {
156
+ return {
157
+ appId: appIdOrAlias,
158
+ ...wechat[appIdOrAlias]
159
+ };
160
+ }
161
+ const entry = Object.entries(wechat).find(([_, item]) => item.alias === appIdOrAlias);
162
+ if (entry) {
163
+ return {
164
+ appId: entry[0],
165
+ ...entry[1]
166
+ };
167
+ }
168
+ return null;
146
169
  }
147
- async saveWechatCredential(appId, appSecret) {
170
+ /**
171
+ * 保存或更新微信凭据
172
+ */
173
+ async saveWechatCredential(appId, appSecret, alias) {
148
174
  await this.initPromise;
149
175
  this.credential.wechat ??= {};
150
- this.credential.wechat[appId] = appSecret;
176
+ const item = { appSecret };
177
+ if (alias) {
178
+ item.alias = alias;
179
+ }
180
+ this.credential.wechat[appId] = item;
151
181
  await this.save();
152
182
  }
153
- async deleteWechatCredential(appId) {
154
- await this.initPromise;
155
- if (this.credential.wechat) {
156
- delete this.credential.wechat[appId];
183
+ /**
184
+ * 删除微信凭据 (通过 appId 或 alias)
185
+ */
186
+ async deleteWechatCredential(appIdOrAlias) {
187
+ const target = await this.getWechatCredential(appIdOrAlias);
188
+ if (target && this.credential.wechat) {
189
+ delete this.credential.wechat[target.appId];
157
190
  await this.save();
158
191
  }
159
192
  }
@@ -177,7 +210,7 @@ class WechatPublisher {
177
210
  const result2 = await this.fetchAccessToken(appId, appSecret);
178
211
  return result2.access_token;
179
212
  }
180
- const cached = this.tokenStore.getToken(appId);
213
+ const cached = await this.tokenStore.getToken(appId);
181
214
  if (cached) {
182
215
  return cached;
183
216
  }
@@ -217,6 +250,11 @@ class WechatPublisher {
217
250
  await this.uploadCacheStore.clear();
218
251
  }
219
252
  }
253
+ async setExternalToken(appid, accessToken) {
254
+ if (this.tokenStore) {
255
+ await this.tokenStore.setExternalToken(appid, accessToken);
256
+ }
257
+ }
220
258
  }
221
259
  export {
222
260
  CredentialStore,
@@ -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;
@@ -1,5 +1,9 @@
1
+ export interface WechatCredentialItem {
2
+ appSecret: string;
3
+ alias?: string;
4
+ }
1
5
  export interface WenyanCredential {
2
- wechat?: Record<string, string>;
6
+ wechat?: Record<string, WechatCredentialItem>;
3
7
  }
4
8
  export declare const defaultCredential: WenyanCredential;
5
9
  export interface CredentialStorageAdapter {
@@ -14,11 +18,20 @@ export declare class CredentialStore {
14
18
  constructor(adapter: CredentialStorageAdapter);
15
19
  private load;
16
20
  private save;
17
- private _getWechatCredential;
18
- getWechatCredential(appId: string): Promise<{
21
+ /**
22
+ * 获取微信凭据 (通过 appId 或 alias)
23
+ */
24
+ getWechatCredential(appIdOrAlias: string): Promise<{
19
25
  appId: string;
20
26
  appSecret: string;
27
+ alias?: string;
21
28
  } | null>;
22
- saveWechatCredential(appId: string, appSecret: string): Promise<void>;
23
- deleteWechatCredential(appId: string): Promise<void>;
29
+ /**
30
+ * 保存或更新微信凭据
31
+ */
32
+ saveWechatCredential(appId: string, appSecret: string, alias?: string | null): Promise<void>;
33
+ /**
34
+ * 删除微信凭据 (通过 appId 或 alias)
35
+ */
36
+ deleteWechatCredential(appIdOrAlias: string): Promise<void>;
24
37
  }
@@ -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;
@@ -25,6 +25,7 @@ export declare class WechatPublisher {
25
25
  uploadImage(file: Blob, filename: string, accessToken: string, appId?: string): Promise<WechatUploadResponse>;
26
26
  publishToDraft(accessToken: string, options: WechatPublishOptions): Promise<WechatPublishResponse>;
27
27
  clearCache(): Promise<void>;
28
+ setExternalToken(appid: string, accessToken: string): Promise<void>;
28
29
  }
29
30
  export * from "./tokenStore.js";
30
31
  export * from "./uploadCacheStore.js";
@@ -17,8 +17,9 @@ export declare class TokenStore {
17
17
  private load;
18
18
  private save;
19
19
  waitForInit(): Promise<void>;
20
- isValid(appid: string): boolean;
21
- getToken(appid: string): string | null;
20
+ isValid(appid: string): Promise<boolean>;
21
+ getToken(appid: string): Promise<string | null>;
22
22
  setToken(appid: string, accessToken: string, expiresIn: number): Promise<void>;
23
+ setExternalToken(appid: string, accessToken: string): Promise<void>;
23
24
  clear(): Promise<void>;
24
25
  }
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.8",
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",