pi-cc-extensions 1.0.6 → 1.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/README.en.md +2 -1
- package/README.md +2 -1
- package/extensions/config/config.ts +5 -0
- package/extensions/config/panel.ts +9 -0
- package/extensions/feature/shell/footer-layout.ts +1 -4
- package/extensions/feature/shell/token-rate.ts +21 -5
- package/extensions/feature/shell/working-message.ts +23 -4
- package/extensions/index.ts +2 -1
- package/extensions/renderer/markdown-enhance.ts +4 -128
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -28,7 +28,7 @@ Run `/reload` after installation.
|
|
|
28
28
|
| Feature | Description | Entry point |
|
|
29
29
|
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
30
30
|
| Claude Code UI | Tool summaries, expand/collapse, rich edit/write diffs, and `on` / `compact` / `off` modes | `/ccstyle` |
|
|
31
|
-
| Markdown enhancements | Mermaid
|
|
31
|
+
| Markdown enhancements | Mermaid dialects and circled-number conversion | Automatic; turn off with `enableMarkdownEnhance: false` |
|
|
32
32
|
| Fullscreen mode | Tool card/group expand and collapse on click, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
|
|
33
33
|
| Settings panel | `Style / Features / UI / Diff / Thinking / Footer` tabs | `/ccstyle` |
|
|
34
34
|
| Context inspection | Usage breakdown and previews for the system prompt, memory, skills, tools definition, and messages | `/context` |
|
|
@@ -55,6 +55,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
55
55
|
"enableAgentSummary": true, // per-turn tool summary
|
|
56
56
|
"enableWorkingMessage": true, // Working... bottom token/elapsed
|
|
57
57
|
"enableAliases": true, // /clear, /exit aliases
|
|
58
|
+
"enableMarkdownEnhance": true, // markdown pass: Mermaid dialects and circled-number conversion; false hands it to other extensions
|
|
58
59
|
"enableEditorPrefix": true, // editor ❯ prefix
|
|
59
60
|
|
|
60
61
|
// ui
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
28
28
|
| 功能 | 说明 | 入口 |
|
|
29
29
|
| --------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
30
30
|
| Claude Code UI | 工具摘要、折叠展开、rich edit/write diff,以及`on` / `compact` / `off` 三种模式 | `/ccstyle` |
|
|
31
|
-
| Markdown 增强 | Mermaid
|
|
31
|
+
| Markdown 增强 | Mermaid 方言渲染、圈数字转半角括号 | 自动生效;`enableMarkdownEnhance: false` 关闭 |
|
|
32
32
|
| Fullscreen mode | 工具卡/group 单击展开与收起、预览、hover 高亮、回到底部按钮 | `TUIMODE=fullscreen` 或 `--tui-mode fullscreen` |
|
|
33
33
|
| 配置面板 | `Style / Features / UI / Diff / Thinking / Footer` 六页签 | `/ccstyle` |
|
|
34
34
|
| 上下文检查 | 查看上下文占用,并预览 System prompt、Memory、Skills、Tools definition 和消息内容 | `/context` |
|
|
@@ -55,6 +55,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
55
55
|
"enableAgentSummary": true, // 每回合工具摘要
|
|
56
56
|
"enableWorkingMessage": true, // Working... 底部 token/耗时
|
|
57
57
|
"enableAliases": true, // /clear、/exit 别名
|
|
58
|
+
"enableMarkdownEnhance": true, // markdown 转换:Mermaid 方言渲染、圈数字转半角括号;false 让位给其他扩展
|
|
58
59
|
"enableEditorPrefix": true, // 输入框 ❯ 前缀
|
|
59
60
|
|
|
60
61
|
// ui
|
|
@@ -74,6 +74,8 @@ export type Config = {
|
|
|
74
74
|
enableAgentSummary: boolean;
|
|
75
75
|
enableWorkingMessage: boolean;
|
|
76
76
|
enableAliases: boolean;
|
|
77
|
+
/** markdown 转换:Mermaid 方言渲染、圈数字转半角括号。关掉让位给其他扩展。 */
|
|
78
|
+
enableMarkdownEnhance: boolean;
|
|
77
79
|
/** 输入框 `❯ ` 前缀(占左侧 2 列,颜色随 thinking/bash mode 联动)。 */
|
|
78
80
|
enableEditorPrefix: boolean;
|
|
79
81
|
enableCustomFooter: boolean;
|
|
@@ -157,6 +159,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
157
159
|
enableAgentSummary: true,
|
|
158
160
|
enableWorkingMessage: true,
|
|
159
161
|
enableAliases: true,
|
|
162
|
+
enableMarkdownEnhance: true,
|
|
160
163
|
enableEditorPrefix: true,
|
|
161
164
|
enableCustomFooter: true,
|
|
162
165
|
footerTokenRate: true,
|
|
@@ -290,6 +293,7 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
290
293
|
enableAgentSummary: source.enableAgentSummary !== false,
|
|
291
294
|
enableWorkingMessage: source.enableWorkingMessage !== false,
|
|
292
295
|
enableAliases: source.enableAliases !== false,
|
|
296
|
+
enableMarkdownEnhance: source.enableMarkdownEnhance !== false,
|
|
293
297
|
enableEditorPrefix: (source.enableEditorPrefix ?? source.enableInputPrompt) !== false, // 兼容旧配置键
|
|
294
298
|
enableCustomFooter: source.enableCustomFooter !== false,
|
|
295
299
|
footerTokenRate: source.footerTokenRate !== false,
|
|
@@ -361,6 +365,7 @@ export function formatConfigStatus(source: Config = config): string {
|
|
|
361
365
|
`agentSummary=${source.enableAgentSummary ? "on" : "off"}`,
|
|
362
366
|
`workingMsg=${source.enableWorkingMessage ? "on" : "off"}`,
|
|
363
367
|
`aliases=${source.enableAliases ? "on" : "off"}`,
|
|
368
|
+
`markdownEnhance=${source.enableMarkdownEnhance ? "on" : "off"}`,
|
|
364
369
|
`editorPrefix=${source.enableEditorPrefix ? "on" : "off"}`,
|
|
365
370
|
`footer=${source.enableCustomFooter ? "on" : "off"}`,
|
|
366
371
|
`footerTokenRate=${source.footerTokenRate ? "on" : "off"}`,
|
|
@@ -656,6 +656,13 @@ export async function showCcstylePanel(
|
|
|
656
656
|
"Aliases disabled.",
|
|
657
657
|
config.enableAliases,
|
|
658
658
|
);
|
|
659
|
+
const markdownEnhanceToggle = featureToggleSetting(
|
|
660
|
+
"enableMarkdownEnhance",
|
|
661
|
+
"Markdown enhance",
|
|
662
|
+
"Mermaid dialects and circled-number conversion. Next restart applies.",
|
|
663
|
+
"Markdown enhancements off; another extension can own the markdown pass.",
|
|
664
|
+
config.enableMarkdownEnhance,
|
|
665
|
+
);
|
|
659
666
|
const footerCurrencySetting = {
|
|
660
667
|
id: "footerCurrency",
|
|
661
668
|
label: "Cost currency",
|
|
@@ -798,6 +805,7 @@ export async function showCcstylePanel(
|
|
|
798
805
|
enableAgentSummary: agentSummaryToggle,
|
|
799
806
|
enableWorkingMessage: workingMessageToggle,
|
|
800
807
|
enableAliases: aliasesToggle,
|
|
808
|
+
enableMarkdownEnhance: markdownEnhanceToggle,
|
|
801
809
|
};
|
|
802
810
|
|
|
803
811
|
const onSettingChange = (id: string, value: string) => {
|
|
@@ -1049,6 +1057,7 @@ export async function showCcstylePanel(
|
|
|
1049
1057
|
agentSummaryToggle.setting,
|
|
1050
1058
|
workingMessageToggle.setting,
|
|
1051
1059
|
aliasesToggle.setting,
|
|
1060
|
+
markdownEnhanceToggle.setting,
|
|
1052
1061
|
],
|
|
1053
1062
|
},
|
|
1054
1063
|
{
|
|
@@ -40,10 +40,7 @@ const LEGACY_FIXED_FOOTER_KEYS = [
|
|
|
40
40
|
FOOTER_GIT_KEY,
|
|
41
41
|
] as const;
|
|
42
42
|
|
|
43
|
-
const BUILTIN_FOOTER_KEYS = new Set<string>(
|
|
44
|
-
...DEFAULT_FOOTER_LINE1_KEYS,
|
|
45
|
-
...DEFAULT_FOOTER_LINE2_KEYS,
|
|
46
|
-
]);
|
|
43
|
+
const BUILTIN_FOOTER_KEYS = new Set<string>(LEGACY_FIXED_FOOTER_KEYS);
|
|
47
44
|
|
|
48
45
|
export function isBuiltinFooterKey(key: string): boolean {
|
|
49
46
|
return BUILTIN_FOOTER_KEYS.has(key);
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* average:一次用户指令的墙钟平均(工具等待计入分母),显示 `avg`。
|
|
5
5
|
* live:最近 1 秒的瞬时速率,生成期暂停时钟,显示 `live`;指令结束定格为整段平均。
|
|
6
|
-
* token 与 working-message
|
|
7
|
-
* (CJK 1 token,其余 chars/4
|
|
6
|
+
* token 与 working-message 同源:变大的 provider `usage.output` 覆盖估算,否则按 delta 累加
|
|
7
|
+
* (CJK 1 token,其余 chars/4)。没有变大的 usage 不当作累计值:Anthropic 的 message_start
|
|
8
|
+
* 会给出一个很小的 output_tokens,并在每个 partial 上重复,直到最后的 message_delta。
|
|
9
|
+
* 消息结束时用 message_end 上的 usage 定格本段总量。
|
|
8
10
|
* Pi 的 turn 是单次 assistant 消息,工具调用结束会开新 turn。token 跨 turn 累加,
|
|
9
11
|
* 计时与 TTFT 直到下一条用户指令才重置。
|
|
10
12
|
* TTFT:用户指令开始到第一个内容块(text/thinking/toolcall)开始的毫秒数,
|
|
@@ -120,15 +122,29 @@ export function sampleTokenDelta(delta: string, now = Date.now()): void {
|
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
/**
|
|
123
|
-
* 记录本段 provider `usage.output
|
|
125
|
+
* 记录本段 provider `usage.output`。变大的 usage 按锚点摊回窗口:
|
|
124
126
|
* 首次报告从消息起点铺到现在,之后每次把增量摊在上次报告之后。
|
|
127
|
+
* 没有变大的报告直接忽略,避免 message_start 的占位值反复覆盖 delta 累计。
|
|
128
|
+
* `final` 用于消息结束:以本次 output 为本段总量,估算偏高偏低都按它修正。
|
|
129
|
+
* 结束值含未逐字流出的思考 token,它们不属于最近一秒,所以窗口只整体平移,不回填。
|
|
125
130
|
*/
|
|
126
|
-
export function recordProviderUsage(output: number, now = Date.now()): void {
|
|
131
|
+
export function recordProviderUsage(output: number, now = Date.now(), final = false): void {
|
|
127
132
|
if (!runStartTime || output <= 0) return;
|
|
133
|
+
if (final) {
|
|
134
|
+
const shift = output - currentTokens;
|
|
135
|
+
for (const sample of samples) sample.cum += shift;
|
|
136
|
+
countedUsage = output;
|
|
137
|
+
currentTokens = output;
|
|
138
|
+
usageAnchorTime = now;
|
|
139
|
+
usageAnchorTokens = output;
|
|
140
|
+
updateRates(now);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (output <= countedUsage) return;
|
|
128
144
|
const firstReport = countedUsage === 0;
|
|
129
145
|
const jump = output - countedUsage;
|
|
130
146
|
countedUsage = output;
|
|
131
|
-
// provider
|
|
147
|
+
// 变大的 provider 计数是权威值,覆盖本段估算。
|
|
132
148
|
currentTokens = output;
|
|
133
149
|
if (firstReport) {
|
|
134
150
|
samples = samples.filter((sample) => sample.t < usageAnchorTime);
|
|
@@ -49,9 +49,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
49
49
|
return totalTokens();
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function updateProviderUsage(message: StreamMessage): void {
|
|
52
|
+
function updateProviderUsage(message: StreamMessage, final = false): void {
|
|
53
53
|
const output = outputUsage(message);
|
|
54
|
-
if (output > 0) recordProviderUsage(output);
|
|
54
|
+
if (output > 0) recordProviderUsage(output, Date.now(), final);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function buildWorkingMessage(): string {
|
|
@@ -171,13 +171,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
171
171
|
// 分段锚点只在 turn_start 重置。这里再重置会把累计 usage.output 当成新的首报。
|
|
172
172
|
markFirstToken();
|
|
173
173
|
updateProviderUsage(evt.partial);
|
|
174
|
-
} else if (
|
|
174
|
+
} else if (
|
|
175
|
+
evt.type === "thinking_delta" ||
|
|
176
|
+
evt.type === "text_delta" ||
|
|
177
|
+
evt.type === "toolcall_delta"
|
|
178
|
+
) {
|
|
179
|
+
// 工具参数走 toolcall_delta;不计入的话,写长文件这类调用期间 token 一直不涨。
|
|
175
180
|
if (typeof evt.delta === "string") sampleTokenDelta(evt.delta);
|
|
176
181
|
updateProviderUsage(evt.partial);
|
|
177
182
|
} else if (evt.type === "text_end" || evt.type === "thinking_end") {
|
|
178
183
|
updateProviderUsage(evt.partial);
|
|
179
184
|
} else if (evt.type === "done") {
|
|
180
|
-
|
|
185
|
+
// Pi 目前不会把 done/error 转成 message_update,定格在下面的 message_end 里做。
|
|
186
|
+
// 这两个分支只在宿主直接转发 done/error 时生效;同一个值重复定格不改变结果。
|
|
187
|
+
updateProviderUsage(evt.message, true);
|
|
181
188
|
} else if (evt.type === "error") {
|
|
182
189
|
updateProviderUsage(evt.error);
|
|
183
190
|
} else {
|
|
@@ -188,6 +195,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
188
195
|
scheduleRefreshTick();
|
|
189
196
|
});
|
|
190
197
|
|
|
198
|
+
// message_delta 的累计 usage 只出现在 message_end 上,含未逐字流出的思考 token。
|
|
199
|
+
// message_end 在工具执行和 turn_end 之前,先于 carryTokenRate 结转。
|
|
200
|
+
pi.on("message_end", async (event, ctx) => {
|
|
201
|
+
const message = event?.message;
|
|
202
|
+
if (message?.role !== "assistant") return;
|
|
203
|
+
activeCtx = ctx;
|
|
204
|
+
// 出错/中断时 usage 可能还是 message_start 的占位值,只接受变大的值。
|
|
205
|
+
const failed = message.stopReason === "error" || message.stopReason === "aborted";
|
|
206
|
+
updateProviderUsage(message, !failed);
|
|
207
|
+
syncWorkingMessage();
|
|
208
|
+
});
|
|
209
|
+
|
|
191
210
|
pi.on("tool_execution_start", async () => {
|
|
192
211
|
pauseTokenRate();
|
|
193
212
|
});
|
package/extensions/index.ts
CHANGED
|
@@ -35,7 +35,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
// render stack:thinking controller 直接交给 style 作 query
|
|
38
|
-
markdownEnhance
|
|
38
|
+
// markdownEnhance 单独开关:其他扩展也注册 transformer 时会互相改写输出。
|
|
39
|
+
if (config.enableMarkdownEnhance) markdownEnhance(pi);
|
|
39
40
|
claudeCodeStyle(pi, undefined, installCompactThinking(pi, getCompactThinkingConfig()));
|
|
40
41
|
|
|
41
42
|
// features
|
|
@@ -41,99 +41,6 @@ function collectFence(lines: string[], i: number): { diagram: string; next: numb
|
|
|
41
41
|
return null; // 未闭合:放弃转换,保持原文
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// ============================================================================
|
|
45
|
-
// GitHub 风格提示框(admonition)
|
|
46
|
-
// ============================================================================
|
|
47
|
-
|
|
48
|
-
const ADMONITION = /^>\s*\[\s*!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\s*\]\s*(.*)$/i;
|
|
49
|
-
const ADMONITION_STYLE: Record<string, { icon: string; label: string }> = {
|
|
50
|
-
NOTE: { icon: "💡", label: "NOTE" },
|
|
51
|
-
TIP: { icon: "✅", label: "TIP" },
|
|
52
|
-
IMPORTANT: { icon: "❗", label: "IMPORTANT" },
|
|
53
|
-
WARNING: { icon: "⚠️", label: "WARNING" },
|
|
54
|
-
CAUTION: { icon: "⚠️", label: "CAUTION" },
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* 把 > [!TYPE] 及其后续 > 行转成带加粗标签的引用块,视觉上成提示框。
|
|
59
|
-
* 多行内容用空格合并为一行。返回 null 表示该行不是提示框。
|
|
60
|
-
*/
|
|
61
|
-
function renderAdmonition(lines: string[], i: number): { output: string[]; next: number } | null {
|
|
62
|
-
const m = lines[i].match(ADMONITION);
|
|
63
|
-
if (!m) return null;
|
|
64
|
-
const style = ADMONITION_STYLE[m[1].toUpperCase()];
|
|
65
|
-
const body: string[] = [m[2]];
|
|
66
|
-
let j = i + 1;
|
|
67
|
-
while (j < lines.length && /^>\s?/.test(lines[j]) && !ADMONITION.test(lines[j])) {
|
|
68
|
-
body.push(lines[j].replace(/^>\s?/, ""));
|
|
69
|
-
j++;
|
|
70
|
-
}
|
|
71
|
-
while (body.length > 0 && body[body.length - 1].trim() === "") body.pop();
|
|
72
|
-
// 内容合并为一行,作为引用块正文
|
|
73
|
-
const content = body.join(" ").trim();
|
|
74
|
-
// 保留引用块形态(pi 渲染为左侧竖线),标签加粗
|
|
75
|
-
return {
|
|
76
|
-
output: [`> **${style.icon} ${style.label}**${content ? ` ${content}` : ""}`, ""],
|
|
77
|
-
next: j,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ============================================================================
|
|
82
|
-
// 裸 URL 转可点击超链接
|
|
83
|
-
// ============================================================================
|
|
84
|
-
|
|
85
|
-
const URL_RE = /(?<!<)(?<!\]\()https?:\/\/[^\s<>'"|,。;:!?、」』】()【】《》『』「」]+/g;
|
|
86
|
-
const TRIM_URL_RE = /[.,;:!?】」』"'》)}]+$/;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 去掉 URL 尾部标点;括号按平衡保留(如 Wikipedia 链接含括号、
|
|
90
|
-
* IPv6 地址含 [::1]),尾部不成对的 ] 与 ) 会截掉。
|
|
91
|
-
*/
|
|
92
|
-
function trimUrl(url: string): string {
|
|
93
|
-
let t = url.replace(TRIM_URL_RE, "");
|
|
94
|
-
for (;;) {
|
|
95
|
-
if (t.endsWith(")") && (t.match(/\(/g)?.length ?? 0) < (t.match(/\)/g)?.length ?? 0)) {
|
|
96
|
-
t = t.slice(0, -1);
|
|
97
|
-
} else if (t.endsWith("]") && (t.match(/\[/g)?.length ?? 0) < (t.match(/\]/g)?.length ?? 0)) {
|
|
98
|
-
t = t.slice(0, -1);
|
|
99
|
-
} else {
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return t;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 把代码块和行内代码外的裸 URL 转成 markdown 链接。
|
|
108
|
-
* 已有的 [text](url) /  / <url> 自动链接用 lookbehind 排除。
|
|
109
|
-
*/
|
|
110
|
-
function linkifyUrls(markdown: string): string {
|
|
111
|
-
const lines = markdown.split("\n");
|
|
112
|
-
const out: string[] = [];
|
|
113
|
-
let fence: MarkdownFence | undefined;
|
|
114
|
-
for (const line of lines) {
|
|
115
|
-
const scanned = scanMarkdownFence(line, fence);
|
|
116
|
-
fence = scanned.fence;
|
|
117
|
-
if (scanned.protected) {
|
|
118
|
-
out.push(line);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
// 行内代码(`...`)按反引号分片,只处理偶数片(代码外)
|
|
122
|
-
const parts = line.split("`");
|
|
123
|
-
for (let p = 0; p < parts.length; p++) {
|
|
124
|
-
if (p % 2 === 0) {
|
|
125
|
-
parts[p] = parts[p].replace(URL_RE, (url) => {
|
|
126
|
-
const trimmed = trimUrl(url);
|
|
127
|
-
if (trimmed.length === 0) return url;
|
|
128
|
-
return `[${trimmed}](${trimmed})`;
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
out.push(parts.join("`"));
|
|
133
|
-
}
|
|
134
|
-
return out.join("\n");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
44
|
// ============================================================================
|
|
138
45
|
// 圈数字 → 半角括号
|
|
139
46
|
// ============================================================================
|
|
@@ -231,7 +138,9 @@ function normalizeMultilineLinks(markdown: string): string {
|
|
|
231
138
|
|
|
232
139
|
export default function (pi: ExtensionAPI): void {
|
|
233
140
|
// 注意:pi 每个扩展只有一个 markdownTransformer 槽位,多次注册会互相覆盖,
|
|
234
|
-
//
|
|
141
|
+
// 所以剩下的转换合并为一次注册,内部按序链式执行。
|
|
142
|
+
// 提示框和裸 URL 不接管:pi 的 marked 已自带裸 URL 链接化(带尾标点回退,
|
|
143
|
+
// 且在链接文本内不再嵌套),提示框交给默认引用块风格。
|
|
235
144
|
pi.registerMarkdownTransformer((markdown, context) => {
|
|
236
145
|
const { messageType, isStreaming = false } = context ?? {};
|
|
237
146
|
// thinking 保持原文;普通回复即使流式中也先关闭跨行链接的空白点击区。
|
|
@@ -241,11 +150,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
241
150
|
// 0. 圈数字转半角括号(Nerd Font 补丁字形缺陷规避)
|
|
242
151
|
markdown = deCircled(markdown);
|
|
243
152
|
// 1. Mermaid 方言渲染
|
|
244
|
-
|
|
245
|
-
// 2. GitHub 风格提示框
|
|
246
|
-
markdown = renderAdmonitions(markdown);
|
|
247
|
-
// 3. 裸 URL 转超链接
|
|
248
|
-
return linkifyUrls(markdown);
|
|
153
|
+
return renderDiagrams(markdown, context);
|
|
249
154
|
});
|
|
250
155
|
}
|
|
251
156
|
|
|
@@ -307,32 +212,3 @@ function renderDiagrams(
|
|
|
307
212
|
}
|
|
308
213
|
return out.join("\n");
|
|
309
214
|
}
|
|
310
|
-
|
|
311
|
-
/** GitHub 风格提示框 → 加粗标签引用块(跳过代码块)。 */
|
|
312
|
-
function renderAdmonitions(markdown: string): string {
|
|
313
|
-
const lines = markdown.split("\n");
|
|
314
|
-
const out: string[] = [];
|
|
315
|
-
let fence: MarkdownFence | undefined;
|
|
316
|
-
let i = 0;
|
|
317
|
-
while (i < lines.length) {
|
|
318
|
-
const line = lines[i];
|
|
319
|
-
const scanned = scanMarkdownFence(line, fence);
|
|
320
|
-
fence = scanned.fence;
|
|
321
|
-
if (scanned.protected) {
|
|
322
|
-
out.push(line);
|
|
323
|
-
i++;
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
if (/^>\s*\[\s*!/i.test(line)) {
|
|
327
|
-
const result = renderAdmonition(lines, i);
|
|
328
|
-
if (result) {
|
|
329
|
-
out.push(...result.output);
|
|
330
|
-
i = result.next;
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
out.push(line);
|
|
335
|
-
i++;
|
|
336
|
-
}
|
|
337
|
-
return out.join("\n");
|
|
338
|
-
}
|