@zzclub/pipeline 0.13.2 → 0.16.0
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/ARTICLE-THEMES.md +86 -0
- package/LICENSE +21 -0
- package/README.md +182 -788
- package/THIRD-PARTY-NOTICES.md +25 -0
- package/package.json +6 -2
- package/src/adapter-types.ts +3 -0
- package/src/adapters/builtin-markdown-renderer.ts +1 -0
- package/src/article-theme.test.ts +87 -0
- package/src/article-theme.ts +115 -0
- package/src/commands/abandon.test.ts +1 -1
- package/src/commands/article-theme.ts +16 -0
- package/src/commands/checkpoint.test.ts +2 -2
- package/src/commands/ingest-handoff.ts +6 -0
- package/src/commands/init.ts +10 -0
- package/src/commands/prepare.ts +5 -1
- package/src/commands/publish.ts +4 -0
- package/src/commands/reset.ts +2 -0
- package/src/commands/wechat-export.ts +4 -0
- package/src/commands/wechat-preview.ts +35 -0
- package/src/plugins.ts +2 -0
- package/src/providers/index.ts +17 -2
- package/src/providers/publish-core.ts +32 -1
- package/src/providers/wechat.test.ts +12 -0
- package/src/providers/wechat.ts +60 -39
- package/src/render-branding.ts +1 -1
- package/src/schema/config.ts +12 -0
- package/src/schema/state.ts +5 -0
- package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +21 -1
- package/src/wechat-preview/assets/browser-dist/assets/style-C9-YtTIm.css +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/wechat-renderer-D1jHoaM2.js +624 -0
- package/src/wechat-preview/assets/browser-dist/editor-export.js +1 -512
- package/src/wechat-preview/assets/browser-dist/src/wechat-preview/browser/studio/studio.html +293 -0
- package/src/wechat-preview/assets/browser-dist/studio.js +332 -0
- package/src/wechat-preview/browser/editor-export.ts +17 -2
- package/src/wechat-preview/browser/highlight.ts +2 -0
- package/src/wechat-preview/browser/studio/color-palettes.test.ts +31 -0
- package/src/wechat-preview/browser/studio/color-palettes.ts +115 -0
- package/src/wechat-preview/browser/studio/studio.css +638 -0
- package/src/wechat-preview/browser/studio/studio.html +292 -0
- package/src/wechat-preview/browser/studio/studio.ts +721 -0
- package/src/wechat-preview/browser/studio/syntax-presets.test.ts +74 -0
- package/src/wechat-preview/browser/studio/syntax-presets.ts +590 -0
- package/src/wechat-preview/fixtures/all-nodes.md +38 -0
- package/src/wechat-preview/index.ts +36 -5
- package/src/wechat-preview/plugins/builtin-badge.ts +152 -0
- package/src/wechat-preview/plugins/builtin-callout.ts +261 -0
- package/src/wechat-preview/plugins/builtin-highlight.ts +46 -0
- package/src/wechat-preview/plugins/builtin-kbd.ts +133 -0
- package/src/wechat-preview/plugins/index.test.ts +313 -0
- package/src/wechat-preview/plugins/index.ts +6 -0
- package/src/wechat-preview/plugins/registry.ts +62 -0
- package/src/wechat-preview/plugins/types.ts +31 -0
- package/src/wechat-preview/server/http.ts +160 -3
- package/src/wechat-preview/server/paths.ts +2 -2
- package/src/wechat-preview/server/singleton.ts +14 -12
- package/src/wechat-preview/server/studio.test.ts +92 -0
- package/src/wechat-preview/server/ui.ts +1 -0
- package/src/wechat-preview/wechat-export.integration.test.ts +9 -1
- package/src/wechat-preview/wechat-renderer.ts +72 -7
- package/src/workflow.test.ts +1 -0
|
@@ -20,8 +20,10 @@ import {
|
|
|
20
20
|
WECHAT_PREVIEW_DIR,
|
|
21
21
|
} from "../runtime-paths";
|
|
22
22
|
import { extractFrontmatter } from "./frontmatter-handler";
|
|
23
|
+
import { readArticleTheme } from "../article-theme";
|
|
23
24
|
import { getWechatPreviewStyleName, getWechatPreviewTheme } from "./themes";
|
|
24
25
|
import { stripLeadingH1 } from "../text";
|
|
26
|
+
import { buildCombinedPresetsCss } from "./browser/studio/syntax-presets";
|
|
25
27
|
|
|
26
28
|
interface ViteManifestEntry {
|
|
27
29
|
file: string;
|
|
@@ -90,9 +92,17 @@ export interface ExportMarkdownToWechatHtmlInput {
|
|
|
90
92
|
title?: string;
|
|
91
93
|
previewShellOutPath?: string;
|
|
92
94
|
customCss?: string | null;
|
|
95
|
+
articleThemePath?: string | null;
|
|
93
96
|
themeOverrides?: {
|
|
94
97
|
editorVars?: Record<string, string>;
|
|
95
98
|
exportTheme?: Record<string, string>;
|
|
99
|
+
syntaxPresets?: Record<string, string>;
|
|
100
|
+
structure?: {
|
|
101
|
+
numberedHeadings?: boolean;
|
|
102
|
+
headingLabel?: string;
|
|
103
|
+
quoteLabel?: string;
|
|
104
|
+
calloutPresets?: Record<string, { title?: string; icon?: string }>;
|
|
105
|
+
};
|
|
96
106
|
};
|
|
97
107
|
/** 浏览器真实等待上限,默认 15000 毫秒。 */
|
|
98
108
|
timeoutMs?: number;
|
|
@@ -130,6 +140,14 @@ export function resolveMilkdownArticleStylePath(): string {
|
|
|
130
140
|
export function getWechatPreviewBundleSources(): string[] {
|
|
131
141
|
return [
|
|
132
142
|
join(WECHAT_PREVIEW_DIR, "browser/editor-export.ts"),
|
|
143
|
+
join(WECHAT_PREVIEW_DIR, "browser/highlight.ts"),
|
|
144
|
+
join(WECHAT_PREVIEW_DIR, "browser/studio/studio.ts"),
|
|
145
|
+
join(WECHAT_PREVIEW_DIR, "browser/studio/studio.html"),
|
|
146
|
+
join(WECHAT_PREVIEW_DIR, "browser/studio/studio.css"),
|
|
147
|
+
join(WECHAT_PREVIEW_DIR, "plugins/registry.ts"),
|
|
148
|
+
join(WECHAT_PREVIEW_DIR, "plugins/builtin-callout.ts"),
|
|
149
|
+
join(WECHAT_PREVIEW_DIR, "plugins/builtin-kbd.ts"),
|
|
150
|
+
join(WECHAT_PREVIEW_DIR, "plugins/builtin-badge.ts"),
|
|
133
151
|
resolveMilkdownArticleStylePath(),
|
|
134
152
|
join(WECHAT_PREVIEW_DIR, "wechat-renderer.ts"),
|
|
135
153
|
];
|
|
@@ -377,18 +395,31 @@ export async function exportMarkdownToWechatHtml(
|
|
|
377
395
|
const sourceMarkdown = await readFile(input.markdownPath, "utf-8");
|
|
378
396
|
const stripped = extractFrontmatter(sourceMarkdown).content;
|
|
379
397
|
const bodyMarkdown = stripLeadingH1(stripped).trimStart();
|
|
380
|
-
const markdown =
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
:
|
|
398
|
+
const markdown = bodyMarkdown;
|
|
399
|
+
const articleTheme = input.articleThemePath ? await readArticleTheme(input.articleThemePath) : null;
|
|
400
|
+
const theme = getWechatPreviewTheme(input.account, articleTheme ? {
|
|
401
|
+
editorVars: { ...articleTheme.manifest.editorVars, ...input.themeOverrides?.editorVars },
|
|
402
|
+
exportTheme: { footerText: "", ...articleTheme.manifest.exportTheme, ...input.themeOverrides?.exportTheme },
|
|
403
|
+
} : input.themeOverrides);
|
|
404
|
+
const presetsCss = input.themeOverrides?.syntaxPresets
|
|
405
|
+
? buildCombinedPresetsCss(input.themeOverrides.syntaxPresets)
|
|
406
|
+
: "";
|
|
407
|
+
const customCss = [presetsCss, articleTheme?.css, input.customCss ? await readFile(input.customCss, "utf-8") : ""].filter(Boolean).join("\n\n");
|
|
408
|
+
const structure = articleTheme ? {
|
|
409
|
+
...articleTheme.manifest.structure,
|
|
410
|
+
...input.themeOverrides?.structure,
|
|
411
|
+
headerImageUrl: articleTheme.manifest.structure.headerImage
|
|
412
|
+
? `data:image/${articleTheme.manifest.structure.headerImage.split(".").pop()!.toLowerCase().replace("jpg", "jpeg")};base64,${Buffer.from(articleTheme.files.get(articleTheme.manifest.structure.headerImage)!).toString("base64")}` : null,
|
|
413
|
+
} : (input.themeOverrides?.structure ? { ...input.themeOverrides.structure } : undefined);
|
|
385
414
|
const shell = readUtf8(TEMPLATE_PATH);
|
|
386
415
|
const payloadJson = escapeInlineJson(
|
|
387
416
|
JSON.stringify({
|
|
388
417
|
markdown,
|
|
418
|
+
assetBaseUrl: pathToFileURL(`${dirname(resolve(input.markdownPath))}/`).href,
|
|
389
419
|
editorVars: theme.editorVars,
|
|
390
420
|
exportTheme: theme.exportTheme,
|
|
391
421
|
customCss,
|
|
422
|
+
structure,
|
|
392
423
|
}),
|
|
393
424
|
);
|
|
394
425
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { $markSchema, $remark } from "@milkdown/kit/utils";
|
|
2
|
+
import type { WechatMarkdownPlugin } from "./types";
|
|
3
|
+
import type { WechatElementRenderer } from "../wechat-renderer";
|
|
4
|
+
|
|
5
|
+
/** Remark plugin to match [badge:type|text] syntax. */
|
|
6
|
+
export function remarkBadge() {
|
|
7
|
+
return (tree: any) => {
|
|
8
|
+
function visit(node: any) {
|
|
9
|
+
if (!node || typeof node !== "object") return;
|
|
10
|
+
if (node.type === "paragraph" || node.type === "listItem" || node.type === "heading") {
|
|
11
|
+
if (Array.isArray(node.children)) {
|
|
12
|
+
const newChildren: any[] = [];
|
|
13
|
+
for (const child of node.children) {
|
|
14
|
+
if (child.type === "text" && typeof child.value === "string") {
|
|
15
|
+
const regex = /\[badge:(brand|success|warning|danger|info)\|([^\]]+)\]/gi;
|
|
16
|
+
let lastIndex = 0;
|
|
17
|
+
let match;
|
|
18
|
+
let found = false;
|
|
19
|
+
while ((match = regex.exec(child.value)) !== null) {
|
|
20
|
+
found = true;
|
|
21
|
+
if (match.index > lastIndex) {
|
|
22
|
+
newChildren.push({
|
|
23
|
+
type: "text",
|
|
24
|
+
value: child.value.slice(lastIndex, match.index),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const badgeType = match[1].toLowerCase();
|
|
28
|
+
const badgeText = match[2];
|
|
29
|
+
newChildren.push({
|
|
30
|
+
type: "badge",
|
|
31
|
+
badgeType,
|
|
32
|
+
children: [{ type: "text", value: badgeText }],
|
|
33
|
+
});
|
|
34
|
+
lastIndex = regex.lastIndex;
|
|
35
|
+
}
|
|
36
|
+
if (found) {
|
|
37
|
+
if (lastIndex < child.value.length) {
|
|
38
|
+
newChildren.push({
|
|
39
|
+
type: "text",
|
|
40
|
+
value: child.value.slice(lastIndex),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
newChildren.push(child);
|
|
47
|
+
}
|
|
48
|
+
node.children = newChildren;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(node.children)) {
|
|
52
|
+
for (const child of node.children) {
|
|
53
|
+
visit(child);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
visit(tree);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const badgeMilkdownPlugins = [
|
|
62
|
+
$remark("remarkBadge", () => remarkBadge as any),
|
|
63
|
+
$markSchema("badge", () => ({
|
|
64
|
+
attrs: {
|
|
65
|
+
type: { default: "brand" },
|
|
66
|
+
},
|
|
67
|
+
parseDOM: [
|
|
68
|
+
{
|
|
69
|
+
tag: "span[data-wechat-node='badge']",
|
|
70
|
+
getAttrs: (dom) => ({
|
|
71
|
+
type: (dom as HTMLElement).getAttribute("data-badge-type") || "brand",
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
toDOM: (mark) => [
|
|
76
|
+
"span",
|
|
77
|
+
{
|
|
78
|
+
class: `wechat-badge wechat-badge-${mark.attrs.type}`,
|
|
79
|
+
"data-wechat-node": "badge",
|
|
80
|
+
"data-badge-type": mark.attrs.type,
|
|
81
|
+
},
|
|
82
|
+
0,
|
|
83
|
+
],
|
|
84
|
+
parseMarkdown: {
|
|
85
|
+
match: (node) => node.type === "badge",
|
|
86
|
+
runner: (state, node, markType) => {
|
|
87
|
+
state.openMark(markType, { type: node.badgeType || "brand" });
|
|
88
|
+
state.next(node.children);
|
|
89
|
+
state.closeMark(markType);
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
toMarkdown: {
|
|
93
|
+
match: (mark) => mark.type.name === "badge",
|
|
94
|
+
runner: (state, mark) => {
|
|
95
|
+
state.withMark(mark, "badge");
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
})),
|
|
99
|
+
].flat();
|
|
100
|
+
|
|
101
|
+
export const badgeWechatRenderer: WechatElementRenderer = {
|
|
102
|
+
kind: "badge",
|
|
103
|
+
selector: 'span[data-wechat-node="badge"]',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const builtinBadgePlugin: WechatMarkdownPlugin = {
|
|
107
|
+
doc: {
|
|
108
|
+
id: "badge",
|
|
109
|
+
name: "状态徽章 (Badge)",
|
|
110
|
+
category: "inline",
|
|
111
|
+
description: "使用 [badge:类型|文字] 插入圆角状态胶囊标签(支持 brand/success/warning/danger/info)",
|
|
112
|
+
sampleMarkdown: "当前服务状态:[badge:success|运行正常],新版特性:[badge:brand|重点推荐]!",
|
|
113
|
+
syntaxGuide: "[badge:类型|标签文字]",
|
|
114
|
+
cssSelectors: ['span[data-wechat-node="badge"]', ".wechat-badge"],
|
|
115
|
+
},
|
|
116
|
+
milkdownPlugins: badgeMilkdownPlugins,
|
|
117
|
+
wechatRenderer: badgeWechatRenderer,
|
|
118
|
+
defaultCss: `
|
|
119
|
+
.milkdown .editor .wechat-badge,
|
|
120
|
+
.milkdown .editor span[data-wechat-node="badge"] {
|
|
121
|
+
display: inline-block;
|
|
122
|
+
padding: 1px 7px;
|
|
123
|
+
font-size: 0.78em;
|
|
124
|
+
font-weight: 600;
|
|
125
|
+
line-height: 1.4;
|
|
126
|
+
border-radius: 999px;
|
|
127
|
+
margin: 0 3px;
|
|
128
|
+
vertical-align: baseline;
|
|
129
|
+
box-sizing: border-box;
|
|
130
|
+
}
|
|
131
|
+
.milkdown .editor .wechat-badge-brand {
|
|
132
|
+
background-color: rgba(202, 96, 147, 0.12);
|
|
133
|
+
color: #a94473;
|
|
134
|
+
}
|
|
135
|
+
.milkdown .editor .wechat-badge-success {
|
|
136
|
+
background-color: #e8f6ec;
|
|
137
|
+
color: #2f7d4a;
|
|
138
|
+
}
|
|
139
|
+
.milkdown .editor .wechat-badge-warning {
|
|
140
|
+
background-color: #fef3c7;
|
|
141
|
+
color: #b45309;
|
|
142
|
+
}
|
|
143
|
+
.milkdown .editor .wechat-badge-danger {
|
|
144
|
+
background-color: #fee2e2;
|
|
145
|
+
color: #b91c1c;
|
|
146
|
+
}
|
|
147
|
+
.milkdown .editor .wechat-badge-info {
|
|
148
|
+
background-color: #eff6ff;
|
|
149
|
+
color: #1d4ed8;
|
|
150
|
+
}
|
|
151
|
+
`.trim(),
|
|
152
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { $nodeSchema, $remark } from "@milkdown/kit/utils";
|
|
2
|
+
import type { WechatMarkdownPlugin } from "./types";
|
|
3
|
+
import type { WechatElementRenderer } from "../wechat-renderer";
|
|
4
|
+
|
|
5
|
+
export interface CalloutMeta {
|
|
6
|
+
title: string;
|
|
7
|
+
icon: string;
|
|
8
|
+
borderColor: string;
|
|
9
|
+
bgColor: string;
|
|
10
|
+
textColor: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const CALLOUT_PRESETS: Record<string, CalloutMeta> = {
|
|
14
|
+
note: {
|
|
15
|
+
title: "提示",
|
|
16
|
+
icon: "💡",
|
|
17
|
+
borderColor: "#3b82f6",
|
|
18
|
+
bgColor: "rgba(59, 130, 246, 0.08)",
|
|
19
|
+
textColor: "#1d4ed8",
|
|
20
|
+
},
|
|
21
|
+
tip: {
|
|
22
|
+
title: "建议",
|
|
23
|
+
icon: "✨",
|
|
24
|
+
borderColor: "#10b981",
|
|
25
|
+
bgColor: "rgba(16, 185, 129, 0.08)",
|
|
26
|
+
textColor: "#047857",
|
|
27
|
+
},
|
|
28
|
+
important: {
|
|
29
|
+
title: "重点",
|
|
30
|
+
icon: "📌",
|
|
31
|
+
borderColor: "#8b5cf6",
|
|
32
|
+
bgColor: "rgba(139, 92, 246, 0.08)",
|
|
33
|
+
textColor: "#6d28d9",
|
|
34
|
+
},
|
|
35
|
+
warning: {
|
|
36
|
+
title: "警告",
|
|
37
|
+
icon: "⚠️",
|
|
38
|
+
borderColor: "#f59e0b",
|
|
39
|
+
bgColor: "rgba(245, 158, 11, 0.08)",
|
|
40
|
+
textColor: "#b45309",
|
|
41
|
+
},
|
|
42
|
+
caution: {
|
|
43
|
+
title: "注意",
|
|
44
|
+
icon: "🛑",
|
|
45
|
+
borderColor: "#ef4444",
|
|
46
|
+
bgColor: "rgba(239, 68, 68, 0.08)",
|
|
47
|
+
textColor: "#b91c1c",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Remark AST plugin: transforms GFM alert blockquote into callout node. */
|
|
52
|
+
export function remarkCallout() {
|
|
53
|
+
return (tree: any) => {
|
|
54
|
+
function visit(node: any) {
|
|
55
|
+
if (!node || typeof node !== "object") return;
|
|
56
|
+
if (node.type === "blockquote" && Array.isArray(node.children) && node.children.length > 0) {
|
|
57
|
+
const firstParagraph = node.children[0];
|
|
58
|
+
if (
|
|
59
|
+
firstParagraph &&
|
|
60
|
+
firstParagraph.type === "paragraph" &&
|
|
61
|
+
Array.isArray(firstParagraph.children) &&
|
|
62
|
+
firstParagraph.children.length > 0
|
|
63
|
+
) {
|
|
64
|
+
const firstText = firstParagraph.children[0];
|
|
65
|
+
if (firstText && firstText.type === "text" && typeof firstText.value === "string") {
|
|
66
|
+
const match = firstText.value.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:[ \t]+([^\r\n]+))?(?:\r?\n|$)/i);
|
|
67
|
+
if (match) {
|
|
68
|
+
const rawType = match[1].toLowerCase();
|
|
69
|
+
const customTitle = match[2] ? match[2].trim() : "";
|
|
70
|
+
// Strip the [!TAG] marker
|
|
71
|
+
firstText.value = firstText.value.slice(match[0].length);
|
|
72
|
+
if (!firstText.value.trim() && firstParagraph.children.length > 1) {
|
|
73
|
+
firstParagraph.children.shift();
|
|
74
|
+
} else if (!firstText.value.trim() && firstParagraph.children.length === 1) {
|
|
75
|
+
node.children.shift();
|
|
76
|
+
}
|
|
77
|
+
node.type = "callout";
|
|
78
|
+
node.calloutType = rawType;
|
|
79
|
+
node.customTitle = customTitle;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(node.children)) {
|
|
85
|
+
for (const child of node.children) {
|
|
86
|
+
visit(child);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
visit(tree);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const calloutMilkdownPlugins = [
|
|
95
|
+
$remark("remarkCallout", () => remarkCallout as any),
|
|
96
|
+
$nodeSchema("callout", () => ({
|
|
97
|
+
content: "block+",
|
|
98
|
+
group: "block",
|
|
99
|
+
defining: true,
|
|
100
|
+
attrs: {
|
|
101
|
+
type: { default: "note" },
|
|
102
|
+
title: { default: "" },
|
|
103
|
+
},
|
|
104
|
+
parseDOM: [
|
|
105
|
+
{
|
|
106
|
+
tag: "section[data-wechat-node='callout']",
|
|
107
|
+
getAttrs: (dom) => ({
|
|
108
|
+
type: (dom as HTMLElement).getAttribute("data-callout-type") || "note",
|
|
109
|
+
title: (dom as HTMLElement).getAttribute("data-callout-title") || "",
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
toDOM: (node) => [
|
|
114
|
+
"section",
|
|
115
|
+
{
|
|
116
|
+
class: `wechat-callout wechat-callout-${node.attrs.type}`,
|
|
117
|
+
"data-wechat-node": "callout",
|
|
118
|
+
"data-callout-type": node.attrs.type,
|
|
119
|
+
...(node.attrs.title ? { "data-callout-title": node.attrs.title } : {}),
|
|
120
|
+
},
|
|
121
|
+
0,
|
|
122
|
+
],
|
|
123
|
+
parseMarkdown: {
|
|
124
|
+
match: (node) => node.type === "callout",
|
|
125
|
+
runner: (state, node, type) => {
|
|
126
|
+
state.openNode(type, {
|
|
127
|
+
type: node.calloutType || "note",
|
|
128
|
+
title: node.customTitle || "",
|
|
129
|
+
});
|
|
130
|
+
state.next(node.children);
|
|
131
|
+
state.closeNode();
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
toMarkdown: {
|
|
135
|
+
match: (node) => node.type.name === "callout",
|
|
136
|
+
runner: (state, node) => {
|
|
137
|
+
state.openNode("blockquote");
|
|
138
|
+
state.next(node.content);
|
|
139
|
+
state.closeNode();
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
})),
|
|
143
|
+
].flat();
|
|
144
|
+
|
|
145
|
+
export const calloutWechatRenderer: WechatElementRenderer = {
|
|
146
|
+
kind: "callout",
|
|
147
|
+
selector: 'section[data-wechat-node="callout"]',
|
|
148
|
+
prepare(element, context) {
|
|
149
|
+
const type = element.getAttribute("data-callout-type") || "note";
|
|
150
|
+
const customTitleAttr = element.getAttribute("data-callout-title");
|
|
151
|
+
const configuredPreset = context.structure?.calloutPresets?.[type];
|
|
152
|
+
const basePreset = CALLOUT_PRESETS[type] || CALLOUT_PRESETS.note;
|
|
153
|
+
const preset = {
|
|
154
|
+
...basePreset,
|
|
155
|
+
...configuredPreset,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
let iconText = preset.icon;
|
|
159
|
+
let titleText = preset.title;
|
|
160
|
+
|
|
161
|
+
if (customTitleAttr && customTitleAttr.trim()) {
|
|
162
|
+
const trimmed = customTitleAttr.trim();
|
|
163
|
+
const emojiMatch = trimmed.match(/^(\p{Extended_Pictographic}|\p{Emoji_Presentation})\s*(.*)$/u);
|
|
164
|
+
if (emojiMatch) {
|
|
165
|
+
iconText = emojiMatch[1];
|
|
166
|
+
titleText = emojiMatch[2].trim() || preset.title;
|
|
167
|
+
} else {
|
|
168
|
+
titleText = trimmed;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Build callout header section
|
|
173
|
+
const header = context.document.createElement("section");
|
|
174
|
+
header.className = "wechat-callout-header";
|
|
175
|
+
header.setAttribute("data-wechat-node", "callout-header");
|
|
176
|
+
|
|
177
|
+
const icon = context.document.createElement("span");
|
|
178
|
+
icon.className = "wechat-callout-icon";
|
|
179
|
+
icon.textContent = iconText;
|
|
180
|
+
header.appendChild(icon);
|
|
181
|
+
|
|
182
|
+
const title = context.document.createElement("strong");
|
|
183
|
+
title.className = "wechat-callout-title";
|
|
184
|
+
title.textContent = titleText;
|
|
185
|
+
header.appendChild(title);
|
|
186
|
+
|
|
187
|
+
element.prepend(header);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const builtinCalloutPlugin: WechatMarkdownPlugin = {
|
|
192
|
+
doc: {
|
|
193
|
+
id: "callout",
|
|
194
|
+
name: "提示卡片 (Callout)",
|
|
195
|
+
category: "block",
|
|
196
|
+
description: "使用 GFM 规范语法 (> [!NOTE]) 生成带左侧边框与图标的微信高亮卡片",
|
|
197
|
+
sampleMarkdown: `> [!NOTE]
|
|
198
|
+
> 这里是一条标准提示,适用于操作备忘。
|
|
199
|
+
|
|
200
|
+
> [!TIP] 实用技巧
|
|
201
|
+
> 这是带自定义标题的建议卡片。
|
|
202
|
+
|
|
203
|
+
> [!WARNING] ⚠️ 重点防护
|
|
204
|
+
> 这是带自定义 Emoji 与标题的警告卡片。`,
|
|
205
|
+
syntaxGuide: "> [!NOTE] 自定义标题 (可选,可包含 Emoji)\n> 提示内容...",
|
|
206
|
+
cssSelectors: [
|
|
207
|
+
'section[data-wechat-node="callout"]',
|
|
208
|
+
".wechat-callout",
|
|
209
|
+
".wechat-callout-note",
|
|
210
|
+
".wechat-callout-tip",
|
|
211
|
+
".wechat-callout-warning",
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
milkdownPlugins: calloutMilkdownPlugins,
|
|
215
|
+
wechatRenderer: calloutWechatRenderer,
|
|
216
|
+
defaultCss: `
|
|
217
|
+
.milkdown .editor section[data-wechat-node="callout"],
|
|
218
|
+
.milkdown .editor .wechat-callout {
|
|
219
|
+
margin: 1.2em 0;
|
|
220
|
+
padding: 12px 16px;
|
|
221
|
+
border-radius: 6px;
|
|
222
|
+
border-left-width: 4px;
|
|
223
|
+
border-left-style: solid;
|
|
224
|
+
box-sizing: border-box;
|
|
225
|
+
}
|
|
226
|
+
.milkdown .editor .wechat-callout-note {
|
|
227
|
+
border-left-color: #3b82f6;
|
|
228
|
+
background-color: rgba(59, 130, 246, 0.08);
|
|
229
|
+
}
|
|
230
|
+
.milkdown .editor .wechat-callout-tip {
|
|
231
|
+
border-left-color: #10b981;
|
|
232
|
+
background-color: rgba(16, 185, 129, 0.08);
|
|
233
|
+
}
|
|
234
|
+
.milkdown .editor .wechat-callout-important {
|
|
235
|
+
border-left-color: #8b5cf6;
|
|
236
|
+
background-color: rgba(139, 92, 246, 0.08);
|
|
237
|
+
}
|
|
238
|
+
.milkdown .editor .wechat-callout-warning {
|
|
239
|
+
border-left-color: #f59e0b;
|
|
240
|
+
background-color: rgba(245, 158, 11, 0.08);
|
|
241
|
+
}
|
|
242
|
+
.milkdown .editor .wechat-callout-caution {
|
|
243
|
+
border-left-color: #ef4444;
|
|
244
|
+
background-color: rgba(239, 68, 68, 0.08);
|
|
245
|
+
}
|
|
246
|
+
.milkdown .editor section[data-wechat-node="callout-header"] {
|
|
247
|
+
font-size: 14px;
|
|
248
|
+
font-weight: bold;
|
|
249
|
+
margin-bottom: 6px;
|
|
250
|
+
line-height: 1.4;
|
|
251
|
+
}
|
|
252
|
+
.milkdown .editor .wechat-callout-icon {
|
|
253
|
+
margin-right: 6px;
|
|
254
|
+
}
|
|
255
|
+
.milkdown .editor .wechat-callout p {
|
|
256
|
+
margin: 0.4em 0;
|
|
257
|
+
font-size: 15px;
|
|
258
|
+
line-height: 1.7;
|
|
259
|
+
}
|
|
260
|
+
`.trim(),
|
|
261
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { $markSchema, $remark } from "@milkdown/kit/utils";
|
|
2
|
+
import { remarkHighlightMark } from "remark-highlight-mark";
|
|
3
|
+
import type { WechatMarkdownPlugin } from "./types";
|
|
4
|
+
|
|
5
|
+
export const highlightMilkdownPlugins = [
|
|
6
|
+
$remark("remarkHighlightMark", () => remarkHighlightMark as any),
|
|
7
|
+
$markSchema("highlight", () => ({
|
|
8
|
+
parseDOM: [{ tag: "mark" }],
|
|
9
|
+
toDOM: () => ["mark", 0],
|
|
10
|
+
parseMarkdown: {
|
|
11
|
+
match: (node) => node.type === "highlight",
|
|
12
|
+
runner: (state, node, markType) => {
|
|
13
|
+
state.openMark(markType, {});
|
|
14
|
+
state.next(node.children);
|
|
15
|
+
state.closeMark(markType);
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
toMarkdown: {
|
|
19
|
+
match: (mark) => mark.type.name === "highlight",
|
|
20
|
+
runner: (state, mark) => {
|
|
21
|
+
state.withMark(mark, "highlight");
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
})),
|
|
25
|
+
].flat();
|
|
26
|
+
|
|
27
|
+
export const builtinHighlightPlugin: WechatMarkdownPlugin = {
|
|
28
|
+
doc: {
|
|
29
|
+
id: "highlight",
|
|
30
|
+
name: "高亮标记 (Highlight)",
|
|
31
|
+
category: "inline",
|
|
32
|
+
description: "使用 ==文本== 添加荧光笔黄色高亮背景",
|
|
33
|
+
sampleMarkdown: "这里支持 ==重要结论== 与 ==关键观点== 的高亮展示。",
|
|
34
|
+
syntaxGuide: "==高亮文本==",
|
|
35
|
+
cssSelectors: ["mark", ".milkdown .editor mark"],
|
|
36
|
+
},
|
|
37
|
+
milkdownPlugins: highlightMilkdownPlugins,
|
|
38
|
+
defaultCss: `
|
|
39
|
+
.milkdown .editor mark {
|
|
40
|
+
background-color: #fff0a8;
|
|
41
|
+
color: inherit;
|
|
42
|
+
padding: 1px 3px;
|
|
43
|
+
border-radius: 2px;
|
|
44
|
+
}
|
|
45
|
+
`.trim(),
|
|
46
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { $markSchema, $remark } from "@milkdown/kit/utils";
|
|
2
|
+
import type { WechatMarkdownPlugin } from "./types";
|
|
3
|
+
import type { WechatElementRenderer } from "../wechat-renderer";
|
|
4
|
+
|
|
5
|
+
/** Remark plugin to match [[kbd:Key]] syntax. */
|
|
6
|
+
export function remarkKbd() {
|
|
7
|
+
return (tree: any) => {
|
|
8
|
+
function visit(node: any) {
|
|
9
|
+
if (!node || typeof node !== "object") return;
|
|
10
|
+
if (node.type === "paragraph" || node.type === "listItem" || node.type === "heading") {
|
|
11
|
+
if (Array.isArray(node.children)) {
|
|
12
|
+
const newChildren: any[] = [];
|
|
13
|
+
for (const child of node.children) {
|
|
14
|
+
if (child.type === "text" && typeof child.value === "string") {
|
|
15
|
+
const regex = /\[\[kbd:([^\]]+)\]\]/g;
|
|
16
|
+
let lastIndex = 0;
|
|
17
|
+
let match;
|
|
18
|
+
let found = false;
|
|
19
|
+
while ((match = regex.exec(child.value)) !== null) {
|
|
20
|
+
found = true;
|
|
21
|
+
if (match.index > lastIndex) {
|
|
22
|
+
newChildren.push({
|
|
23
|
+
type: "text",
|
|
24
|
+
value: child.value.slice(lastIndex, match.index),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
newChildren.push({
|
|
28
|
+
type: "kbd",
|
|
29
|
+
children: [{ type: "text", value: match[1] }],
|
|
30
|
+
});
|
|
31
|
+
lastIndex = regex.lastIndex;
|
|
32
|
+
}
|
|
33
|
+
if (found) {
|
|
34
|
+
if (lastIndex < child.value.length) {
|
|
35
|
+
newChildren.push({
|
|
36
|
+
type: "text",
|
|
37
|
+
value: child.value.slice(lastIndex),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
newChildren.push(child);
|
|
44
|
+
}
|
|
45
|
+
node.children = newChildren;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(node.children)) {
|
|
49
|
+
for (const child of node.children) {
|
|
50
|
+
visit(child);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
visit(tree);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const kbdMilkdownPlugins = [
|
|
59
|
+
$remark("remarkKbd", () => remarkKbd as any),
|
|
60
|
+
$markSchema("kbd", () => ({
|
|
61
|
+
parseDOM: [{ tag: "kbd" }, { tag: "span[data-wechat-node='kbd']" }],
|
|
62
|
+
toDOM: () => [
|
|
63
|
+
"span",
|
|
64
|
+
{
|
|
65
|
+
class: "wechat-kbd",
|
|
66
|
+
"data-wechat-node": "kbd",
|
|
67
|
+
},
|
|
68
|
+
0,
|
|
69
|
+
],
|
|
70
|
+
parseMarkdown: {
|
|
71
|
+
match: (node) => node.type === "kbd",
|
|
72
|
+
runner: (state, node, markType) => {
|
|
73
|
+
state.openMark(markType, {});
|
|
74
|
+
state.next(node.children);
|
|
75
|
+
state.closeMark(markType);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
toMarkdown: {
|
|
79
|
+
match: (mark) => mark.type.name === "kbd",
|
|
80
|
+
runner: (state, mark) => {
|
|
81
|
+
state.withMark(mark, "kbd");
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
})),
|
|
85
|
+
].flat();
|
|
86
|
+
|
|
87
|
+
export const kbdWechatRenderer: WechatElementRenderer = {
|
|
88
|
+
kind: "kbd",
|
|
89
|
+
selector: 'span[data-wechat-node="kbd"], kbd',
|
|
90
|
+
finalize(element) {
|
|
91
|
+
if (element.tagName.toLowerCase() === "kbd") {
|
|
92
|
+
const replacement = element.ownerDocument.createElement("span");
|
|
93
|
+
for (const attr of Array.from(element.attributes)) {
|
|
94
|
+
replacement.setAttribute(attr.name, attr.value);
|
|
95
|
+
}
|
|
96
|
+
while (element.firstChild) {
|
|
97
|
+
replacement.appendChild(element.firstChild);
|
|
98
|
+
}
|
|
99
|
+
element.replaceWith(replacement);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const builtinKbdPlugin: WechatMarkdownPlugin = {
|
|
105
|
+
doc: {
|
|
106
|
+
id: "kbd",
|
|
107
|
+
name: "按键标记 (Keyboard Key)",
|
|
108
|
+
category: "inline",
|
|
109
|
+
description: "使用 [[kbd:按键]] 输出带轻拟物阴影的键盘按键胶囊",
|
|
110
|
+
sampleMarkdown: "按下快捷键 [[kbd:Cmd]] + [[kbd:K]] 快速唤起全局指令台。",
|
|
111
|
+
syntaxGuide: "[[kbd:按键名称]]",
|
|
112
|
+
cssSelectors: ['span[data-wechat-node="kbd"]', ".wechat-kbd"],
|
|
113
|
+
},
|
|
114
|
+
milkdownPlugins: kbdMilkdownPlugins,
|
|
115
|
+
wechatRenderer: kbdWechatRenderer,
|
|
116
|
+
defaultCss: `
|
|
117
|
+
.milkdown .editor .wechat-kbd,
|
|
118
|
+
.milkdown .editor span[data-wechat-node="kbd"] {
|
|
119
|
+
display: inline-block;
|
|
120
|
+
padding: 2px 6px;
|
|
121
|
+
font-family: -apple-system, SFMono-Regular, Consolas, Menlo, monospace;
|
|
122
|
+
font-size: 0.85em;
|
|
123
|
+
line-height: 1.2;
|
|
124
|
+
color: #374151;
|
|
125
|
+
background-color: #f3f4f6;
|
|
126
|
+
border: 1px solid #d1d5db;
|
|
127
|
+
border-bottom-width: 2px;
|
|
128
|
+
border-radius: 4px;
|
|
129
|
+
margin: 0 2px;
|
|
130
|
+
box-sizing: border-box;
|
|
131
|
+
}
|
|
132
|
+
`.trim(),
|
|
133
|
+
};
|