dsh-md-plus 0.1.0 → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # 更新日志
2
+
3
+ ## 0.2.0 —— 2026-10-08
4
+
5
+ - 修掉 mermaid 围栏在 DSH 0.2.0-rc.2 上画不出来(issue #1):头部语言名被占位文案「代码块」顶掉,插件读不到围栏语言;现在认不出标签就看正文的类型声明。
6
+ - 修掉渲染失败在页面底部堆积「炸弹图」,也不再让 mermaid 的错误卡片被当成图。
7
+ - 修掉 ≥4 列的表格出血到面板宽、右边被裁掉、滚动条要悬停才出现的问题:现在跟正文同宽,超出在表内横滚。
8
+ - mermaid 引擎 11.17.0 → 12.1.0:新增 usecase-beta / agentflow-beta,旧图种实测无回归;运行时约 3.4 MB → 5.0 MB。
9
+ - 测试扩到 52 条,测试与预览页改用聊天区的真实头部形状。
10
+ - 注意:mermaid 12 的新图种标识符只认字母数字下划线,中文要写进标签,`actor Customer("用户")` 可以。
11
+ - 注意:DSH 0.2.0-rc.2 上「给 DSH 不认识的语言补高亮」仍然失效,得靠宿主露出语言名,详见 README。
12
+
13
+ ## 0.1.0 —— 2026-09-29
14
+
15
+ 首次发布:代码块卡片重做、补 Shiki 语法高亮、自带 mermaid 渲染、表格样式重做。
package/README.md CHANGED
@@ -1,102 +1,90 @@
1
- # dsh-md-plus
2
-
3
- 给 DeepSeek Harness Web / Desktop 的 Markdown 显示增强插件。目前做四件事:**代码块卡片重做**(含消掉头部空白)、**补上 DSH 不认识的语言的语法高亮**、**自己渲染 mermaid 图形**、**表格样式重做**。
4
-
5
- ![dsh-md-plus 显示效果预览](<docs/preview.png>)
6
-
7
- ## 它解决什么
8
-
9
- ### 一、代码块样式
10
-
11
- DSH 原生的代码块头部是「小灰字 + 文字复制按钮」,没写语言时左边整片是空的,卡顶到第一行代码之间还留着一大段纯空白。本插件把它重做成一张完整的卡片:
12
-
13
- | 元素 | 改前 | 改后 |
14
- |---|---|---|
15
- | 头部左侧 | 空(无语言时) | `</>` 图标 + 语言名;无语言显示 `Plain text` |
16
- | 头部右侧 | 文字「复制」 | 图标按钮:不换行 + 复制 |
17
- | 顶栏背景 | 与卡片之间有一条白带 | 与卡片同色,整块一个卡片 |
18
- | 卡顶到代码 | 一大段空白 | 约一行代码高 |
19
-
20
- 样式是纯 CSS 打在 `.md-code-block` 上,所以**所有语言**(包括 DSH 自己高亮的 JSON/TS/Python…)都吃同一套外观。想微调间距就改 `src/client.js` 里 `banner` 的 `padding` 末位和 `pre` 的 `padding` 首位。
21
-
22
- ### 二、语法高亮
23
-
24
- DSH 只认死 26 种语言,其余走 `pre.plain` 纯文本回退——**没有颜色,看上去就是一堆源码**。`dart` 就不在表里:
25
-
26
- ```dart
27
- class CodeCard extends StatelessWidget {
28
- const CodeCard({super.key, required this.language});
29
-
30
- final String language;
31
- }
32
- ```
33
-
34
- 本插件把这类块捡出来用自带的 Shiki 重新上色;DSH 认的语言一律不碰(但样式照样吃)。
35
-
36
- ### 三、mermaid 图形
37
-
38
- mermaid 围栏由本插件自己渲染成 SVG,**不需要 dsh-mermaid**。头部右侧可以在「图形 / 代码」之间切换,复制按钮复制的仍是图源。配色与节点圆角 token 都在 `src/mermaid-theme.js`。
39
-
40
- 顺带修掉了 dsh-mermaid 一个真 bug:它靠**代码第一个词**判断图类型,而那 25 个词的名单里漏了 `graph`(`graph LR` 和 `flowchart LR` 等价),所以 `graph LR` 的图它一律画不出来。本插件改成读头部显示的语言名,两种写法都能画。
41
-
42
- ### 四、表格样式
43
-
44
- DSH 原生的表格是「无边框 + 半透明分隔线 + 表头没有底色」,表头和数据分不开,首列还贴着左边缘。照参考图重做成:
45
-
46
- | 元素 | 取值 |
47
- |---|---|
48
- | 表头底 / 字 | `#f7f7f7` / `#181818` 加粗 |
49
- | 边框 | `#ebebeb` 1px,只有横线、无竖线 |
50
- | 圆角 | 外 8px / 内 7px,只收外圈四角 |
51
- | 首 / 末列内边距 | 16px(DSH 原本清零) |
52
- | 悬停行 | `#fafafa` 轻微高亮 |
53
-
54
- 两个坑:`border-collapse: collapse` 下 Chrome 会忽略单元格圆角,所以走 `separate` + `border-spacing: 0`;另外裸写 `tr:last-child` 会连表头一起收圆(表头行是 `thead` 的唯一一行),四个角必须按上沿 `thead`、下沿 `tbody` 分开写。想调圆角改 `TABLE_VARS` 里的 `--md-plus-tbl-radius`(内圆角跟着减 1px)。
55
-
56
- 深色主题另配了一套(`#1f1f1f` 表头 / `#2f2f2f` 线)。抓手用的是结构选择器 `div:has(> table)` 而不是 DSH 的哈希类名,所以 DSH 换哈希也不会失效。
57
-
58
- ## 安装
59
-
60
- ```powershell
61
- dsh plugin --profile desktop add dsh-md-plus
62
- ```
63
-
64
- 或者:
65
-
66
- ```powershell
67
- dsh plugin --profile desktop add github:JosephHy/dsh-md-plus
68
- ```
69
-
70
-
71
- ## 卸载
72
-
73
- ```powershell
74
- dsh plugin --profile desktop remove dsh-md-plus
75
- ```
76
-
77
- ## 开发
78
-
79
- ```powershell
80
- npm install # 会顺带跑 prepare → build,装完 lib/ 就是新的
81
- npm run build # 产出 lib/ 下四个文件
82
- npm run check # 语法检查 + 45 条 host/client 行为测试
83
- npm run preview # 离线生成 preview.html(含真渲出来的 mermaid 图)
84
- npm run shot # 用 headless Chrome 把 preview.html 截成 docs/preview.png
85
- ```
86
-
87
-
88
- 注意测试跑的是 `lib/` 里的构建产物,所以顺序永远是 **先 `npm run build` 再 `npm test`**(`npm run check` 不含 build)。
89
-
90
-
91
- ## 已知边界
92
-
93
- - 两个运行时按需加载:高亮约 6.5 MB(171 门语言语法),mermaid 约 3.4 MB。都只在真正用到时才下载;**首次**遇到新语言时多花约 100ms 解析。
94
- - 依赖 DSH 的 `.md-code-block`、`[data-code-block-content]`、`[data-code-block-banner]`、`data-streaming` 这几个稳定钩子。DSH 升级若改了它们,需要跟着改 `src/client.js` 顶部的选择器常量。
95
- - 头部图标按钮里那个「不换行」是插件自己加的(`decorate()` 往 `.action` 里插一个 `<button>`)。参考图里那个 `→|` 图标原功能未知,先做成了横向滚动切换,要换功能改一处即可。
96
- - **滚轮缩放会接管页面滚动**:鼠标悬在图上时滚轮用来缩放,页面不会跟着滚。想改成按住 `Ctrl/⌘` 才缩放,只需在 `bindViewport()` 的 wheel 回调开头加一句 `if (!event.ctrlKey && !event.metaKey) return;`。
97
- - 很宽的图会被缩到容器宽度,字会变小;放大后靠拖拽看细节。
98
- - 目前只做代码块、mermaid 与表格。引用、列表间距、标题层级、目录等属于后续阶段。
99
-
100
- ## 许可证
101
-
102
- MIT,见 [LICENSE](./LICENSE)。
1
+ # dsh-md-plus
2
+
3
+ 给 DeepSeek Harness Web / Desktop 的 Markdown 显示增强插件,做四件事:重做代码块卡片、给 DSH 不认识的语言补语法高亮、自己渲染 mermaid 图形、重做表格样式。
4
+
5
+ ![dsh-md-plus 显示效果预览](<docs/preview.png>)
6
+
7
+ ## 它解决什么
8
+
9
+ ### 代码块样式
10
+
11
+ DSH 原生的头部是「小灰字 + 文字复制按钮」,没写语言时左边空一片,卡顶到第一行代码之间还留着大段空白。重做成一张完整的卡片:
12
+
13
+ | 元素 | 改前 | 改后 |
14
+ |---|---|---|
15
+ | 头部左侧 | 空(无语言时) | `</>` 图标 + 语言名,无语言显示 `Plain text` |
16
+ | 头部右侧 | 文字「复制」 | 图标按钮:不换行 + 复制 |
17
+ | 顶栏背景 | 与卡片之间有一条白带 | 与卡片同色 |
18
+ | 卡顶到代码 | 一大段空白 | 约一行代码高 |
19
+
20
+ 样式是纯 CSS 打在 `.md-code-block` 上,所以所有语言(包括 DSH 自己高亮的 JSON/TS/Python)都吃同一套外观。想调间距,改 `src/client.js` 里 banner 的 `padding` 末位和 pre 的 `padding` 首位。
21
+
22
+ ### 语法高亮
23
+
24
+ DSH 只认它语言表里的语言,其余走纯文本回退,没颜色。`dart` 就不在表里:
25
+
26
+ ```dart
27
+ class CodeCard extends StatelessWidget {
28
+ const CodeCard({super.key, required this.language});
29
+
30
+ final String language;
31
+ }
32
+ ```
33
+
34
+ 插件把这类块捡出来用自带的 Shiki 上色,DSH 认的语言一律不碰。
35
+
36
+ ⚠️ 在 DSH 0.2.0-rc.2 上这套暂时失效,原因见文末「已知边界」。
37
+
38
+ ### mermaid 图形
39
+
40
+ mermaid 围栏由插件渲染成 SVG,头部右侧可在「图形 / 代码」之间切换,复制按钮复制的仍是图源。配色和节点圆角在 `src/mermaid-theme.js`。
41
+
42
+ 认图先看头部语言名(`mermaid` / `mmd` / `mermaidjs`),认不出再看正文里的类型声明(`flowchart`、`graph`、`sequenceDiagram` 等)。0.2.0-rc.2 上头部是占位文案,走的就是正文这条。
43
+
44
+ ### 表格样式
45
+
46
+ DSH 原生表格无边框、分隔线半透明、表头没底色,首列还贴着左边缘。重做成:
47
+
48
+ | 元素 | 取值 |
49
+ |---|---|
50
+ | 表头底 / 字 | `#f7f7f7` / `#181818` 加粗 |
51
+ | 边框 | `#ebebeb` 1px,只有横线 |
52
+ | 圆角 | 外 8px / 内 7px,只收外圈四角 |
53
+ | 首 / 末列内边距 | 16px(DSH 原本清零) |
54
+ | 悬停行 | `#fafafa` |
55
+
56
+ 深色主题另有一套(`#1f1f1f` 表头 / `#2f2f2f` 线)。抓手用结构选择器 `div:has(> table)` 而不是 DSH 的哈希类名,DSH 换哈希也不失效。
57
+
58
+ ## 安装与卸载
59
+
60
+ ```powershell
61
+ dsh plugin --profile desktop add dsh-md-plus # 也可以用 github:JosephHy/dsh-md-plus
62
+ dsh plugin --profile desktop remove dsh-md-plus
63
+ ```
64
+
65
+ ## 开发
66
+
67
+ ```powershell
68
+ npm install # 顺带跑 prepare → build
69
+ npm run build # 产出 lib/ 下四个文件
70
+ npm run check # 语法检查 + 52 条测试
71
+ npm run preview # 生成 preview.html(含真渲出来的 mermaid 图)
72
+ npm run shot # 用 headless Chrome 把 preview.html 截成 docs/preview.png
73
+ ```
74
+
75
+ 测试跑的是 `lib/` 里的产物,所以先 `npm run build` 再 `npm test`(`npm run check` 不含 build)。
76
+
77
+ 测试和预览页里的代码块都按聊天区的真实头部形状搭(`CodeToolbar`,语言名走占位回退)。
78
+
79
+ ## 已知边界
80
+
81
+ - 0.2.0-rc.2 起,头部那行语言名由宿主决定显示什么:认这门语言就显示语言名,不认就显示本地化占位文案(中文「代码块」)。头部永远只有「代码块」,插件读不到围栏语言,就无法正常转化,且还不报错。现在改看正文:跳过 `%%` 注释和开头的 `---` frontmatter,正文里有行以 mermaid 类型关键字开头就当图。这条只管认图,不参与补高亮的判断。
82
+ - 同一个原因让补高亮在 0.2.0-rc.2 上失效:拿不到真语言名,就没法问 Shiki 该用哪套语法,`dart`、`vue` 这类块只能没颜色。。
83
+ - mermaid 12 的新图种(`usecase-beta`、`agentflow-beta`)标识符只认字母数字下划线,中文得写进标签:`actor Customer("用户")` 可以,`actor 用户` 会解析失败。
84
+ - 高亮约 6.4 MB、mermaid 约 5.0 MB,按需下载,第一次用到时多花约 100ms 解析。
85
+ - 依赖宿主 `.md-code-block`、`[data-code-block-content]`、`[data-code-block-banner]`、`data-streaming` 这几个钩子,DSH 改了要跟着改 `src/client.js` 顶部的常量和 `MMD_TYPE`。
86
+ - 目前只管代码块、mermaid 和表格。
87
+
88
+ ## 许可证
89
+
90
+ MIT,见 [LICENSE](./LICENSE)。
package/lib/client.js CHANGED
@@ -73,6 +73,7 @@ window.__ModuleLoader__.load({
73
73
  "lua"
74
74
  ]);
75
75
  var MMD_LANGS = /* @__PURE__ */ new Set(["mermaid", "mermaidjs", "mmd"]);
76
+ var MMD_TYPE = /^\s*(flowchart|graph|sequenceDiagram|classDiagram|stateDiagram(-v2)?|erDiagram|journey|gantt|pie|mindmap|timeline|gitGraph|quadrantChart|xychart-beta|sankey-beta|block-beta|packet-beta|architecture-beta|usecase-beta|agentflow-beta|C4Context|requirementDiagram|kanban|radar|treemap|venn|ishikawa|wardley|cynefin|treeView|zenuml)\b/im;
76
77
  function icon(paths) {
77
78
  var svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>" + paths + "</svg>";
78
79
  return 'url("data:image/svg+xml,' + encodeURIComponent(svg) + '")';
@@ -177,7 +178,18 @@ window.__ModuleLoader__.load({
177
178
  // 最后一行的下边线交给 table 自己的边框,免得叠成双线
178
179
  TABLE + " > table tr:last-child td{border-bottom:0}",
179
180
  // 宽表横向扫读时给个落点
180
- TABLE + " > table tbody tr:hover td{background:var(--md-plus-tbl-hover)}"
181
+ TABLE + " > table tbody tr:hover td{background:var(--md-plus-tbl-hover)}",
182
+ // 宽表(≥4 列)不收着不行。宿主给这类表加了 .md-table-wide,并让它「出血」:
183
+ // 包裹层撑到面板宽,再用 padding-left 把表推到正文栏左侧。那个 padding 会吃掉
184
+ // 可用宽度,结果是表格比正文宽出一截、右边还被裁掉,而滚动条只在鼠标悬停时
185
+ // 才出现——文字表落进这个区间就很难读。这里按回正文栏宽,超出改为表内横滚。
186
+ // 特异性 (0,2,2),压得住宿主的 .Pg4CGa_body .md-table-wide (0,2,0)。
187
+ TABLE + ".md-table-wide{width:100%;max-width:100%;margin-inline:0;padding-inline:0;overflow-x:auto}",
188
+ // 塞得下就撑满正文栏,塞不下保持自然宽度让外面滚,不把列挤成一列一个字
189
+ TABLE + ".md-table-wide > table{width:100%;max-width:none;min-width:max-content}",
190
+ // 宿主原本是「悬停才出滚动条」,所以悬停时会把预留的滚动条高度收掉;
191
+ // 现在滚动条常驻,再收掉就会跳 5px,固定住。
192
+ TABLE + ".md-table-wide:hover{padding-bottom:var(--dsh-scrollbar-width,5px)}"
181
193
  ].join("\n");
182
194
  var scripts = /* @__PURE__ */ new Map();
183
195
  var pending = /* @__PURE__ */ new Set();
@@ -213,6 +225,22 @@ window.__ModuleLoader__.load({
213
225
  var info = banner?.firstElementChild;
214
226
  return (info?.textContent ?? "").trim().toLowerCase();
215
227
  }
228
+ function sniffDiagramType(code) {
229
+ var text = code;
230
+ if (text.startsWith("---")) {
231
+ var blank = text.indexOf("\n");
232
+ var close = blank < 0 ? -1 : text.indexOf("\n---", blank);
233
+ text = close < 0 ? "" : text.slice(close + 4);
234
+ }
235
+ var body = text.split("\n").filter((line) => !line.trimStart().startsWith("%%")).join("\n");
236
+ return MMD_TYPE.exec(body)?.[1] ?? null;
237
+ }
238
+ function diagramKind(block) {
239
+ if (MMD_LANGS.has(readLang(block))) return "diagram";
240
+ var pre = block.querySelector(CONTENT + " pre");
241
+ if (pre === null) return null;
242
+ return sniffDiagramType(pre.textContent ?? "") === null ? null : "diagram";
243
+ }
216
244
  function isOurs(node) {
217
245
  var element = node?.nodeType === 1 ? node : node?.parentElement;
218
246
  return element?.closest?.("." + HOST_CLASS + ", ." + BTN_CLASS + ", ." + MMD_CLASS) != null;
@@ -292,7 +320,7 @@ window.__ModuleLoader__.load({
292
320
  var copy = banner?.querySelector("button:not(." + BTN_CLASS + ")");
293
321
  var action = copy?.parentElement;
294
322
  if (action === null || action === void 0) return;
295
- var specs = MMD_LANGS.has(readLang(block)) ? MMD_BUTTONS : PLAIN_BUTTONS;
323
+ var specs = readLang(block) !== "" && diagramKind(block) !== null ? MMD_BUTTONS : PLAIN_BUTTONS;
296
324
  for (const existing of action.querySelectorAll("." + BTN_CLASS)) {
297
325
  if (specs.some((spec) => existing.classList.contains(spec.cls))) continue;
298
326
  existing.remove();
@@ -319,7 +347,7 @@ window.__ModuleLoader__.load({
319
347
  if (pre === null || pre.classList.contains("shiki")) return;
320
348
  var lang = readLang(block);
321
349
  if (lang === "") return;
322
- if (MMD_LANGS.has(lang)) {
350
+ if (diagramKind(block) !== null) {
323
351
  await renderDiagram(block, content, pre);
324
352
  return;
325
353
  }
@@ -373,6 +401,10 @@ window.__ModuleLoader__.load({
373
401
  return;
374
402
  }
375
403
  if (typeof svg !== "string" || svg === "") return;
404
+ if (svg.includes('aria-roledescription="error"')) {
405
+ failDiagram(block, content, pre, key, new Error("mermaid \u8FD4\u56DE\u7684\u662F\u9519\u8BEF\u5361\u7247"));
406
+ return;
407
+ }
376
408
  if (!pre.isConnected || (pre.textContent ?? "").trim() !== code) {
377
409
  schedule(block);
378
410
  return;
@@ -491,7 +523,8 @@ window.__ModuleLoader__.load({
491
523
  content.querySelector("." + MMD_CLASS)?.remove();
492
524
  var note = content.querySelector("." + MMD_CLASS + "-error") ?? document.createElement("p");
493
525
  note.className = MMD_CLASS + "-error";
494
- note.textContent = "mermaid \u6E32\u67D3\u5931\u8D25\uFF1A" + (error?.message ?? String(error));
526
+ var reason = (error?.message ?? String(error)).split("\n")[0] ?? "";
527
+ note.textContent = "mermaid \u6E32\u67D3\u5931\u8D25\uFF1A" + reason;
495
528
  content.appendChild(note);
496
529
  block.setAttribute(KEY_ATTR, key);
497
530
  block.setAttribute(MMD_ATTR, "failed");
@@ -525,6 +558,8 @@ window.__ModuleLoader__.load({
525
558
  script.remove();
526
559
  }
527
560
  scripts.clear();
561
+ delete window[HL_GLOBAL];
562
+ delete window[MMD_GLOBAL];
528
563
  };
529
564
  }, "dsh-md-plus: code block styling, highlighting and diagrams");
530
565
  }
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // dsh-md-plus 宿主半:只做一件事——把两个浏览器端运行时经同源路由发出去。
2
2
  // 皮肤与行为全在浏览器半(lib/client.js),这里不做别的。
3
- import { readFile } from "node:fs/promises";
3
+ import { readFile, stat } from "node:fs/promises";
4
4
 
5
5
  export const name = "dsh-md-plus";
6
6
  export const inject = ["webServer"];
@@ -26,10 +26,20 @@ export function apply(ctx) {
26
26
  return;
27
27
  }
28
28
  try {
29
+ // 文件名不带内容指纹,原来的一年 immutable 会让升级后的运行时发不出去:
30
+ // 改成每次回源校验,没变就 304。
31
+ const info = await stat(source);
32
+ const etag = `W/"${info.size.toString(16)}-${Math.floor(info.mtimeMs).toString(16)}"`;
33
+ if (req.headers?.["if-none-match"] === etag) {
34
+ res.writeHead(304, { etag, "cache-control": "no-cache" });
35
+ res.end();
36
+ return;
37
+ }
29
38
  const body = await readFile(source);
30
39
  res.writeHead(200, {
31
40
  "content-type": "text/javascript; charset=utf-8",
32
- "cache-control": "public, max-age=31536000, immutable",
41
+ "cache-control": "no-cache",
42
+ etag,
33
43
  });
34
44
  res.end(req.method === "HEAD" ? undefined : body);
35
45
  } catch {