@wenyan-md/core 3.0.5 → 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 +455 -10
- package/dist/types/core/index.d.ts +5 -1
- package/dist/types/core/mermaid.d.ts +19 -0
- package/dist/types/core/parser/frontMatterParser.d.ts +5 -2
- package/dist/types/core/parser/mermaidParser.d.ts +9 -0
- package/dist/types/node/clientPublish.d.ts +1 -0
- package/dist/types/node/mermaidRenderer.d.ts +2 -0
- package/dist/types/node/publish.d.ts +6 -2
- package/dist/types/node/render.d.ts +3 -4
- package/dist/types/node/types.d.ts +3 -8
- package/dist/types/node/wrapper.d.ts +1 -2
- package/dist/types/publish.d.ts +5 -0
- package/dist/types/wechat.d.ts +18 -0
- package/dist/wrapper.js +271 -43
- package/package.json +3 -2
package/dist/core.js
CHANGED
|
@@ -194,9 +194,9 @@ function renderListStyleFootnotes(footnotes) {
|
|
|
194
194
|
}
|
|
195
195
|
async function handleFrontMatter(markdown) {
|
|
196
196
|
const { attributes, body } = fm(markdown);
|
|
197
|
-
const result = {
|
|
197
|
+
const result = { content: body || "" };
|
|
198
198
|
let head = "";
|
|
199
|
-
const { title, description, cover, author, source_url } = attributes;
|
|
199
|
+
const { title, description, cover, author, source_url, need_open_comment, only_fans_can_comment, image_list } = attributes;
|
|
200
200
|
if (title) {
|
|
201
201
|
result.title = title;
|
|
202
202
|
}
|
|
@@ -208,7 +208,7 @@ async function handleFrontMatter(markdown) {
|
|
|
208
208
|
result.cover = cover;
|
|
209
209
|
}
|
|
210
210
|
if (head) {
|
|
211
|
-
result.
|
|
211
|
+
result.content = head + result.content;
|
|
212
212
|
}
|
|
213
213
|
if (author) {
|
|
214
214
|
result.author = author;
|
|
@@ -216,6 +216,15 @@ async function handleFrontMatter(markdown) {
|
|
|
216
216
|
if (source_url) {
|
|
217
217
|
result.source_url = source_url;
|
|
218
218
|
}
|
|
219
|
+
if (need_open_comment !== void 0) {
|
|
220
|
+
result.need_open_comment = need_open_comment;
|
|
221
|
+
}
|
|
222
|
+
if (only_fans_can_comment !== void 0) {
|
|
223
|
+
result.only_fans_can_comment = only_fans_can_comment;
|
|
224
|
+
}
|
|
225
|
+
if (image_list) {
|
|
226
|
+
result.image_list = image_list;
|
|
227
|
+
}
|
|
219
228
|
return result;
|
|
220
229
|
}
|
|
221
230
|
const parseOptions = {
|
|
@@ -425,6 +434,28 @@ function createMarkedClient() {
|
|
|
425
434
|
md.use(highlightExtension);
|
|
426
435
|
md.use({
|
|
427
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
|
+
// 自定义图片语法扩展 ![](){...}
|
|
428
459
|
{
|
|
429
460
|
name: "attributeImage",
|
|
430
461
|
level: "inline",
|
|
@@ -453,7 +484,8 @@ function createMarkedClient() {
|
|
|
453
484
|
renderer(token) {
|
|
454
485
|
const attrs = stringToMap(token.attrs);
|
|
455
486
|
const styleStr = Array.from(attrs).map(([k, v]) => /^\d+$/.test(v) ? `${k}:${v}px` : `${k}:${v}`).join("; ");
|
|
456
|
-
|
|
487
|
+
const href = normalizeHref(token.href);
|
|
488
|
+
return `<img src="${href}" alt="${token.text || ""}" title="${token.text || ""}" style="${styleStr}">`;
|
|
457
489
|
}
|
|
458
490
|
}
|
|
459
491
|
]
|
|
@@ -481,7 +513,12 @@ function createMarkedClient() {
|
|
|
481
513
|
},
|
|
482
514
|
// 重写普通图片 (处理标准 Markdown 图片)
|
|
483
515
|
image(token) {
|
|
484
|
-
|
|
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>`;
|
|
485
522
|
}
|
|
486
523
|
}
|
|
487
524
|
});
|
|
@@ -494,10 +531,21 @@ function createMarkedClient() {
|
|
|
494
531
|
*/
|
|
495
532
|
async parse(markdown) {
|
|
496
533
|
await configure();
|
|
497
|
-
return md.parse(markdown);
|
|
534
|
+
return await md.parse(markdown);
|
|
498
535
|
}
|
|
499
536
|
};
|
|
500
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
|
+
}
|
|
501
549
|
let htmlHandlerRegistered = false;
|
|
502
550
|
function createMathJaxParser(options = {}) {
|
|
503
551
|
const adaptor = liteAdaptor();
|
|
@@ -556,6 +604,36 @@ function createMathJaxParser(options = {}) {
|
|
|
556
604
|
}
|
|
557
605
|
};
|
|
558
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";
|
|
559
637
|
function wechatPostRender(element) {
|
|
560
638
|
const mathElements = element.querySelectorAll("mjx-container");
|
|
561
639
|
mathElements.forEach((mathContainer) => {
|
|
@@ -574,6 +652,13 @@ function wechatPostRender(element) {
|
|
|
574
652
|
}
|
|
575
653
|
}
|
|
576
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
|
+
});
|
|
577
662
|
const codeElements = element.querySelectorAll("pre code");
|
|
578
663
|
codeElements.forEach((codeEl) => {
|
|
579
664
|
codeEl.innerHTML = codeEl.innerHTML.replace(/\n/g, "<br>").replace(/(>[^<]+)|(^[^<]+)/g, (str) => str.replace(/\s/g, " "));
|
|
@@ -590,6 +675,268 @@ function wechatPostRender(element) {
|
|
|
590
675
|
element.style.color = "rgb(0, 0, 0)";
|
|
591
676
|
element.style.caretColor = "rgb(0, 0, 0)";
|
|
592
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
|
+
}
|
|
593
940
|
const themeModifier = createCssModifier({});
|
|
594
941
|
function renderTheme(wenyanElement, themeCss) {
|
|
595
942
|
const modifiedCss = themeModifier(themeCss);
|
|
@@ -1160,10 +1507,104 @@ function getContentForToutiao(wenyanElement) {
|
|
|
1160
1507
|
});
|
|
1161
1508
|
return wenyanElement.outerHTML;
|
|
1162
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
|
+
}
|
|
1163
1603
|
async function createWenyanCore(options = {}) {
|
|
1164
|
-
const { isConvertMathJax = true, isWechat = true } = options;
|
|
1604
|
+
const { isConvertMathJax = true, isWechat = true, mermaid } = options;
|
|
1165
1605
|
const markedClient = createMarkedClient();
|
|
1166
1606
|
const mathJaxParser = createMathJaxParser();
|
|
1607
|
+
const mermaidParser = createMermaidParser(mermaid);
|
|
1167
1608
|
registerAllBuiltInThemes();
|
|
1168
1609
|
registerBuiltInHlThemes();
|
|
1169
1610
|
registerBuiltInMacStyle();
|
|
@@ -1172,11 +1613,11 @@ async function createWenyanCore(options = {}) {
|
|
|
1172
1613
|
return await handleFrontMatter(markdown);
|
|
1173
1614
|
},
|
|
1174
1615
|
async renderMarkdown(markdown) {
|
|
1175
|
-
|
|
1616
|
+
let html = await markedClient.parse(markdown);
|
|
1176
1617
|
if (isConvertMathJax) {
|
|
1177
|
-
|
|
1618
|
+
html = mathJaxParser.parser(html);
|
|
1178
1619
|
}
|
|
1179
|
-
return html;
|
|
1620
|
+
return await mermaidParser.parser(html);
|
|
1180
1621
|
},
|
|
1181
1622
|
async applyStylesWithTheme(wenyanElement, options2 = {}) {
|
|
1182
1623
|
const {
|
|
@@ -1279,7 +1720,10 @@ const DEFAULT_CSS_UPDATES = {
|
|
|
1279
1720
|
};
|
|
1280
1721
|
export {
|
|
1281
1722
|
addFootnotes,
|
|
1723
|
+
createBrowserMermaidRenderer,
|
|
1282
1724
|
createCssModifier,
|
|
1725
|
+
createMermaidConfig,
|
|
1726
|
+
createMermaidRenderError,
|
|
1283
1727
|
createWenyanCore,
|
|
1284
1728
|
getAllGzhThemes,
|
|
1285
1729
|
getAllHlThemes,
|
|
@@ -1296,6 +1740,7 @@ export {
|
|
|
1296
1740
|
registerHlTheme,
|
|
1297
1741
|
registerMacStyle,
|
|
1298
1742
|
registerTheme,
|
|
1743
|
+
replaceMermaidCodeBlocks,
|
|
1299
1744
|
sansSerif,
|
|
1300
1745
|
serif
|
|
1301
1746
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { FrontMatterResult } from "./parser/frontMatterParser.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;
|
|
@@ -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>;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export interface FrontMatterResult {
|
|
2
|
-
|
|
2
|
+
content: string;
|
|
3
3
|
title?: string;
|
|
4
|
-
cover?: string;
|
|
5
4
|
description?: string;
|
|
5
|
+
cover?: string;
|
|
6
6
|
author?: string;
|
|
7
7
|
source_url?: string;
|
|
8
|
+
need_open_comment?: boolean;
|
|
9
|
+
only_fans_can_comment?: boolean;
|
|
10
|
+
image_list?: string[];
|
|
8
11
|
}
|
|
9
12
|
export declare function handleFrontMatter(markdown: string): Promise<FrontMatterResult>;
|
|
@@ -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;
|
|
@@ -13,3 +13,4 @@ export declare function uploadStyledContent(gzhContent: StyledContent, serverUrl
|
|
|
13
13
|
export declare function requestServerPublish(mdFileId: string, serverUrl: string, headers: Record<string, string>, options: ClientPublishOptions): Promise<string>;
|
|
14
14
|
export declare function uploadLocalImages(content: string, serverUrl: string, headers: Record<string, string>, relativePath?: string): Promise<string>;
|
|
15
15
|
export declare function uploadCover(serverUrl: string, headers: Record<string, string>, cover?: string, relativePath?: string): Promise<string | undefined>;
|
|
16
|
+
export declare function uploadImageList(serverUrl: string, headers: Record<string, string>, imageList?: string[], relativePath?: string): Promise<string[]>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { WechatPublishResponse } from "../wechat.js";
|
|
2
|
-
import { ArticleOptions, WechatPublisher } from "../publish.js";
|
|
1
|
+
import type { WechatPublishResponse } from "../wechat.js";
|
|
2
|
+
import { ArticleOptions, ImageTextArticleOptions, WechatPublisher } from "../publish.js";
|
|
3
3
|
import { CredentialStore } from "../credentialStore.js";
|
|
4
4
|
export declare const wechatPublisher: WechatPublisher;
|
|
5
5
|
export declare const credentialStore: CredentialStore;
|
|
@@ -9,5 +9,9 @@ interface PublishOptions {
|
|
|
9
9
|
relativePath?: string;
|
|
10
10
|
}
|
|
11
11
|
export declare function publishToWechatDraft(articleOptions: ArticleOptions, publishOptions?: PublishOptions): Promise<WechatPublishResponse>;
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated use publishToWechatDraft instead
|
|
14
|
+
*/
|
|
12
15
|
export declare function publishToDraft(title: string, content: string, cover?: string, options?: PublishOptions): Promise<WechatPublishResponse>;
|
|
16
|
+
export declare function publishImageTextToWechatDraft(articleOptions: ImageTextArticleOptions, publishOptions?: PublishOptions): Promise<WechatPublishResponse>;
|
|
13
17
|
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { ApplyStylesOptions } from "../core/index.js";
|
|
2
|
-
import { GetInputContentFn, RenderContext, RenderOptions
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function renderStyledContent(content: string, options?: ApplyStylesOptions): Promise<StyledContent>;
|
|
1
|
+
import { ApplyStylesOptions, WenyanCoreInstance } from "../core/index.js";
|
|
2
|
+
import { GetInputContentFn, RenderContext, RenderOptions } from "./types.js";
|
|
3
|
+
export declare function renderStyledContent(content: string, options?: ApplyStylesOptions, coreInstance?: WenyanCoreInstance): Promise<string>;
|
|
5
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,14 +20,7 @@ export interface RenderContext {
|
|
|
18
20
|
gzhContent: StyledContent;
|
|
19
21
|
absoluteDirPath: string | undefined;
|
|
20
22
|
}
|
|
21
|
-
export
|
|
22
|
-
content: string;
|
|
23
|
-
title?: string;
|
|
24
|
-
cover?: string;
|
|
25
|
-
description?: string;
|
|
26
|
-
author?: string;
|
|
27
|
-
source_url?: string;
|
|
28
|
-
}
|
|
23
|
+
export type StyledContent = FrontMatterResult;
|
|
29
24
|
export type GetInputContentFn = (inputContent?: string, filePath?: string) => Promise<{
|
|
30
25
|
content: string;
|
|
31
26
|
absoluteDirPath?: string;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { ClientPublishOptions, GetInputContentFn, PublishOptions
|
|
2
|
-
export declare function getGzhContent(content: string, themeId: string, hlThemeId: string, isMacStyle?: boolean, isAddFootnote?: boolean): Promise<StyledContent>;
|
|
1
|
+
import { ClientPublishOptions, GetInputContentFn, PublishOptions } from "./types.js";
|
|
3
2
|
export declare function renderAndPublish(inputContent: string | undefined, options: PublishOptions, getInputContent: GetInputContentFn): Promise<string>;
|
|
4
3
|
export declare function renderAndPublishToServer(inputContent: string | undefined, options: ClientPublishOptions, getInputContent: GetInputContentFn): Promise<string>;
|
|
5
4
|
export * from "./configStore.js";
|
package/dist/types/publish.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export interface ArticleOptions {
|
|
|
8
8
|
cover?: string;
|
|
9
9
|
author?: string;
|
|
10
10
|
source_url?: string;
|
|
11
|
+
need_open_comment?: boolean;
|
|
12
|
+
only_fans_can_comment?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ImageTextArticleOptions extends ArticleOptions {
|
|
15
|
+
images: string[];
|
|
11
16
|
}
|
|
12
17
|
export declare class WechatPublisher {
|
|
13
18
|
private tokenStore;
|
package/dist/types/wechat.d.ts
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import type { HttpAdapter } from "./http.js";
|
|
2
|
+
export interface ImageCropPercent {
|
|
3
|
+
ratio: string;
|
|
4
|
+
x1: number;
|
|
5
|
+
y1: number;
|
|
6
|
+
x2: number;
|
|
7
|
+
y2: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ImageListItem {
|
|
10
|
+
image_media_id: string;
|
|
11
|
+
crop_percent_list?: ImageCropPercent[];
|
|
12
|
+
}
|
|
13
|
+
export interface ImageInfo {
|
|
14
|
+
image_list: ImageListItem[];
|
|
15
|
+
}
|
|
2
16
|
export interface WechatPublishOptions {
|
|
3
17
|
title: string;
|
|
4
18
|
author?: string;
|
|
5
19
|
content: string;
|
|
6
20
|
thumb_media_id: string;
|
|
7
21
|
content_source_url?: string;
|
|
22
|
+
article_type?: "news" | "newspic";
|
|
23
|
+
image_info?: ImageInfo;
|
|
24
|
+
need_open_comment?: 0 | 1;
|
|
25
|
+
only_fans_can_comment?: 0 | 1;
|
|
8
26
|
}
|
|
9
27
|
export interface WechatErrorResponse {
|
|
10
28
|
errcode: number;
|
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");
|
|
@@ -327,6 +327,19 @@ async function uploadCover(serverUrl, headers, cover, relativePath) {
|
|
|
327
327
|
}
|
|
328
328
|
return cover;
|
|
329
329
|
}
|
|
330
|
+
async function uploadImageList(serverUrl, headers, imageList, relativePath) {
|
|
331
|
+
if (imageList && imageList.length > 0) {
|
|
332
|
+
const uploadPromises = imageList.map(async (image) => {
|
|
333
|
+
if (needUpload(image)) {
|
|
334
|
+
const newImageUrl = await uploadLocalImage(image, serverUrl, headers, relativePath);
|
|
335
|
+
return newImageUrl || image;
|
|
336
|
+
}
|
|
337
|
+
return image;
|
|
338
|
+
});
|
|
339
|
+
return await Promise.all(uploadPromises);
|
|
340
|
+
}
|
|
341
|
+
return imageList || [];
|
|
342
|
+
}
|
|
330
343
|
const nodeHttpAdapter = {
|
|
331
344
|
fetch,
|
|
332
345
|
createMultipart(field, file, filename) {
|
|
@@ -463,7 +476,7 @@ async function uploadImages(content, accessToken, relativePath, appId) {
|
|
|
463
476
|
return { html: updatedHtml, firstImageId };
|
|
464
477
|
}
|
|
465
478
|
async function publishToWechatDraft(articleOptions, publishOptions = {}) {
|
|
466
|
-
const { title, content, cover, author, source_url } = articleOptions;
|
|
479
|
+
const { title, content, cover, author, source_url, need_open_comment, only_fans_can_comment } = articleOptions;
|
|
467
480
|
const { appId, appSecret, relativePath } = publishOptions;
|
|
468
481
|
const { appId: appIdFinal, appSecret: appSecretFinal } = await getAppIdAndSecret(appId, appSecret);
|
|
469
482
|
const accessToken = await wechatPublisher.getAccessTokenWithCache(appIdFinal, appSecretFinal);
|
|
@@ -498,7 +511,9 @@ async function publishToWechatDraft(articleOptions, publishOptions = {}) {
|
|
|
498
511
|
content: html,
|
|
499
512
|
thumb_media_id: thumbMediaId,
|
|
500
513
|
author,
|
|
501
|
-
content_source_url: source_url
|
|
514
|
+
content_source_url: source_url,
|
|
515
|
+
need_open_comment: need_open_comment ? 1 : 0,
|
|
516
|
+
only_fans_can_comment: only_fans_can_comment ? 1 : 0
|
|
502
517
|
});
|
|
503
518
|
if (data.media_id) {
|
|
504
519
|
return data;
|
|
@@ -526,6 +541,52 @@ async function getAppIdAndSecret(appId, appSecret) {
|
|
|
526
541
|
}
|
|
527
542
|
throw new Error(`未能找到 AppID 为 "${appId}" 的公众号凭据,请检查配置文件。`);
|
|
528
543
|
}
|
|
544
|
+
async function publishImageTextToWechatDraft(articleOptions, publishOptions = {}) {
|
|
545
|
+
const { title, content, images, cover, author, need_open_comment, only_fans_can_comment } = articleOptions;
|
|
546
|
+
const { appId, appSecret, relativePath } = publishOptions;
|
|
547
|
+
const { appId: appIdFinal, appSecret: appSecretFinal } = await getAppIdAndSecret(appId, appSecret);
|
|
548
|
+
if (!images || images.length === 0) {
|
|
549
|
+
throw new Error("图片消息至少需要一张图片");
|
|
550
|
+
}
|
|
551
|
+
const accessToken = await wechatPublisher.getAccessTokenWithCache(appIdFinal, appSecretFinal);
|
|
552
|
+
const imageInfoList = [];
|
|
553
|
+
for (const img of images) {
|
|
554
|
+
const resp = await uploadImage(img, accessToken, void 0, relativePath);
|
|
555
|
+
imageInfoList.push({ image_media_id: resp.media_id });
|
|
556
|
+
}
|
|
557
|
+
let thumbMediaId = "";
|
|
558
|
+
if (cover) {
|
|
559
|
+
const cachedThumbMediaId = mediaIdMapping.get(cover);
|
|
560
|
+
if (cachedThumbMediaId) {
|
|
561
|
+
thumbMediaId = cachedThumbMediaId;
|
|
562
|
+
} else {
|
|
563
|
+
const resp = await uploadImage(cover, accessToken, "cover.jpg", relativePath);
|
|
564
|
+
thumbMediaId = resp.media_id;
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
thumbMediaId = imageInfoList[0].image_media_id;
|
|
568
|
+
}
|
|
569
|
+
if (!thumbMediaId) {
|
|
570
|
+
throw new Error("未能获取封面图的 media_id");
|
|
571
|
+
}
|
|
572
|
+
const data = await wechatPublisher.publishToDraft(
|
|
573
|
+
accessToken,
|
|
574
|
+
{
|
|
575
|
+
title,
|
|
576
|
+
content,
|
|
577
|
+
thumb_media_id: thumbMediaId,
|
|
578
|
+
author,
|
|
579
|
+
article_type: "newspic",
|
|
580
|
+
image_info: { image_list: imageInfoList },
|
|
581
|
+
need_open_comment: need_open_comment ? 1 : 0,
|
|
582
|
+
only_fans_can_comment: only_fans_can_comment ? 1 : 0
|
|
583
|
+
}
|
|
584
|
+
);
|
|
585
|
+
if (data.media_id) {
|
|
586
|
+
return data;
|
|
587
|
+
}
|
|
588
|
+
throw new Error(`上传图片消息到公众号草稿失败: ${JSON.stringify(data)}`);
|
|
589
|
+
}
|
|
529
590
|
const defaultConfig = {};
|
|
530
591
|
const configPath = path.join(configDir, "config.json");
|
|
531
592
|
class ConfigStore {
|
|
@@ -599,7 +660,169 @@ class ConfigStore {
|
|
|
599
660
|
}
|
|
600
661
|
}
|
|
601
662
|
const configStore = new ConfigStore();
|
|
602
|
-
|
|
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();
|
|
603
826
|
async function renderWithTheme(markdownContent, options) {
|
|
604
827
|
if (!markdownContent) {
|
|
605
828
|
throw new Error("No content provided for rendering.");
|
|
@@ -615,35 +838,33 @@ async function renderWithTheme(markdownContent, options) {
|
|
|
615
838
|
if (!handledCustomTheme && !theme) {
|
|
616
839
|
throw new Error(`theme "${theme}" not found.`);
|
|
617
840
|
}
|
|
841
|
+
const coreInstance = options.mermaid === false ? wenyanCoreInstanceWithoutMermaid : wenyanCoreInstance;
|
|
618
842
|
const gzhContent = await renderStyledContent(markdownContent, {
|
|
619
843
|
themeId: theme,
|
|
620
844
|
hlThemeId: highlight,
|
|
621
845
|
isMacStyle: macStyle,
|
|
622
846
|
isAddFootnote: footnote,
|
|
623
847
|
themeCss: handledCustomTheme
|
|
624
|
-
});
|
|
848
|
+
}, coreInstance);
|
|
625
849
|
return gzhContent;
|
|
626
850
|
}
|
|
627
|
-
async function renderStyledContent(content, options = {}) {
|
|
628
|
-
const
|
|
629
|
-
const html = await wenyanCoreInstance.renderMarkdown(preHandlerContent.body);
|
|
851
|
+
async function renderStyledContent(content, options = {}, coreInstance = wenyanCoreInstance) {
|
|
852
|
+
const html = await coreInstance.renderMarkdown(content);
|
|
630
853
|
const dom = new JSDOM(`<body><section id="wenyan">${html}</section></body>`);
|
|
631
854
|
const document = dom.window.document;
|
|
632
855
|
const wenyan = document.getElementById("wenyan");
|
|
633
|
-
const result = await
|
|
634
|
-
return
|
|
635
|
-
content: result,
|
|
636
|
-
title: preHandlerContent.title,
|
|
637
|
-
cover: preHandlerContent.cover,
|
|
638
|
-
description: preHandlerContent.description,
|
|
639
|
-
author: preHandlerContent.author,
|
|
640
|
-
source_url: preHandlerContent.source_url
|
|
641
|
-
};
|
|
856
|
+
const result = await coreInstance.applyStylesWithTheme(wenyan, options);
|
|
857
|
+
return result;
|
|
642
858
|
}
|
|
643
859
|
async function prepareRenderContext(inputContent, options, getInputContent) {
|
|
644
860
|
const { content, absoluteDirPath } = await getInputContent(inputContent, options.file);
|
|
645
|
-
const
|
|
646
|
-
|
|
861
|
+
const preHandlerContent = await wenyanCoreInstance.handleFrontMatter(content);
|
|
862
|
+
if (preHandlerContent.image_list && preHandlerContent.image_list.length > 0) {
|
|
863
|
+
return { gzhContent: preHandlerContent, absoluteDirPath };
|
|
864
|
+
}
|
|
865
|
+
const styledContent = await renderWithTheme(preHandlerContent.content, options);
|
|
866
|
+
preHandlerContent.content = styledContent;
|
|
867
|
+
return { gzhContent: preHandlerContent, absoluteDirPath };
|
|
647
868
|
}
|
|
648
869
|
async function listThemes() {
|
|
649
870
|
const themes = getAllGzhThemes();
|
|
@@ -705,30 +926,34 @@ async function checkCustomThemeExists(themeId) {
|
|
|
705
926
|
const customThemes = await configStore.getThemes();
|
|
706
927
|
return customThemes.some((theme) => theme.id === themeId);
|
|
707
928
|
}
|
|
708
|
-
async function getGzhContent(content, themeId, hlThemeId, isMacStyle = true, isAddFootnote = true) {
|
|
709
|
-
return await renderStyledContent(content, {
|
|
710
|
-
themeId,
|
|
711
|
-
hlThemeId,
|
|
712
|
-
isMacStyle,
|
|
713
|
-
isAddFootnote
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
929
|
async function renderAndPublish(inputContent, options, getInputContent) {
|
|
717
930
|
const { gzhContent, absoluteDirPath } = await prepareRenderContext(inputContent, options, getInputContent);
|
|
718
931
|
if (!gzhContent.title) throw new Error("未能找到文章标题");
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
932
|
+
let data;
|
|
933
|
+
if (gzhContent.image_list && gzhContent.image_list.length > 0) {
|
|
934
|
+
data = await publishImageTextToWechatDraft(
|
|
935
|
+
{
|
|
936
|
+
...gzhContent,
|
|
937
|
+
title: gzhContent.title,
|
|
938
|
+
images: gzhContent.image_list
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
appId: options.appId,
|
|
942
|
+
relativePath: absoluteDirPath
|
|
943
|
+
}
|
|
944
|
+
);
|
|
945
|
+
} else {
|
|
946
|
+
data = await publishToWechatDraft(
|
|
947
|
+
{
|
|
948
|
+
...gzhContent,
|
|
949
|
+
title: gzhContent.title
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
appId: options.appId,
|
|
953
|
+
relativePath: absoluteDirPath
|
|
954
|
+
}
|
|
955
|
+
);
|
|
956
|
+
}
|
|
732
957
|
if (data.media_id) {
|
|
733
958
|
return data.media_id;
|
|
734
959
|
} else {
|
|
@@ -743,7 +968,11 @@ async function renderAndPublishToServer(inputContent, options, getInputContent)
|
|
|
743
968
|
await verifyAuth(serverUrl, headers);
|
|
744
969
|
const { gzhContent, absoluteDirPath } = await prepareRenderContext(inputContent, options, getInputContent);
|
|
745
970
|
if (!gzhContent.title) throw new Error("未能找到文章标题");
|
|
746
|
-
gzhContent.
|
|
971
|
+
if (gzhContent.image_list && gzhContent.image_list.length > 0) {
|
|
972
|
+
gzhContent.image_list = await uploadImageList(serverUrl, headers, gzhContent.image_list, absoluteDirPath);
|
|
973
|
+
} else {
|
|
974
|
+
gzhContent.content = await uploadLocalImages(gzhContent.content, serverUrl, headers, absoluteDirPath);
|
|
975
|
+
}
|
|
747
976
|
gzhContent.cover = await uploadCover(serverUrl, headers, gzhContent.cover, absoluteDirPath);
|
|
748
977
|
const mdFileId = await uploadStyledContent(gzhContent, serverUrl, headers);
|
|
749
978
|
return await requestServerPublish(mdFileId, serverUrl, headers, options);
|
|
@@ -755,7 +984,6 @@ export {
|
|
|
755
984
|
configStore,
|
|
756
985
|
credentialStore,
|
|
757
986
|
ensureDir,
|
|
758
|
-
getGzhContent,
|
|
759
987
|
getNormalizeFilePath,
|
|
760
988
|
isAbsolutePath,
|
|
761
989
|
listThemes,
|
|
@@ -763,6 +991,7 @@ export {
|
|
|
763
991
|
md5FromFile,
|
|
764
992
|
normalizePath,
|
|
765
993
|
prepareRenderContext,
|
|
994
|
+
publishImageTextToWechatDraft,
|
|
766
995
|
publishToDraft,
|
|
767
996
|
publishToWechatDraft,
|
|
768
997
|
readBinaryFile,
|
|
@@ -771,7 +1000,6 @@ export {
|
|
|
771
1000
|
renderAndPublish,
|
|
772
1001
|
renderAndPublishToServer,
|
|
773
1002
|
renderStyledContent,
|
|
774
|
-
renderWithTheme,
|
|
775
1003
|
safeReadJson,
|
|
776
1004
|
safeWriteJson,
|
|
777
1005
|
wechatPublisher
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenyan-md/core",
|
|
3
|
-
"version": "3.0.
|
|
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",
|