dsh-md-plus 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JosephHy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
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)。
@@ -0,0 +1,6 @@
1
+ # dsh-md-plus 的补丁层:把自己作为一个 bundle 行插进 profile。
2
+ # 加载器 import 到宿主半 lib/index.js,再按 package.json 的 dsh.client 声明
3
+ # 把 lib/client.js 作为浏览器半下发给页面。
4
+ - insert:
5
+ - id: dsh-md-plus
6
+ name: dsh-md-plus
package/lib/client.js ADDED
@@ -0,0 +1,535 @@
1
+ // src/client.js
2
+ window.__ModuleLoader__.load({
3
+ id: "dsh-md-plus",
4
+ factory: (require2) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ var HL_URL = "/dsh-md-plus/highlight-runtime.js";
8
+ var MMD_URL = "/dsh-md-plus/mermaid-runtime.js";
9
+ var HL_GLOBAL = "__DSH_MD_PLUS_HL__";
10
+ var MMD_GLOBAL = "__DSH_MD_PLUS_MMD__";
11
+ var BLOCK = ".md-code-block";
12
+ var CONTENT = "[data-code-block-content]";
13
+ var BANNER = "[data-code-block-banner]";
14
+ var HOST_CLASS = "md-plus-hl";
15
+ var TOOL_CLASS = "md-plus-tool";
16
+ var TOGGLE_CLASS = "md-plus-toggle";
17
+ var MMD_CLASS = "md-plus-mmd";
18
+ var BTN_CLASS = "md-plus-btn";
19
+ var ZOOM_IN_CLASS = "md-plus-zoom-in";
20
+ var ZOOM_OUT_CLASS = "md-plus-zoom-out";
21
+ var ZOOM_ATTR = "data-md-plus-zoom";
22
+ var ZOOM_STEP = 1.25;
23
+ var ZOOM_MAX = 6;
24
+ var DONE_ATTR = "data-md-plus";
25
+ var KEY_ATTR = "data-md-plus-key";
26
+ var WRAP_ATTR = "data-md-plus-wrap";
27
+ var MMD_ATTR = "data-md-plus-mmd";
28
+ var STYLE_ATTR = "data-md-plus-style";
29
+ var RUNTIME_ATTR = "data-md-plus-runtime";
30
+ var DEBOUNCE_MS = 160;
31
+ var DSH_LANGS = /* @__PURE__ */ new Set([
32
+ "typescript",
33
+ "ts",
34
+ "tsx",
35
+ "javascript",
36
+ "js",
37
+ "jsx",
38
+ "shellscript",
39
+ "bash",
40
+ "sh",
41
+ "shell",
42
+ "zsh",
43
+ "json",
44
+ "jsonc",
45
+ "py",
46
+ "python",
47
+ "rb",
48
+ "ruby",
49
+ "go",
50
+ "rs",
51
+ "rust",
52
+ "java",
53
+ "c",
54
+ "cpp",
55
+ "cs",
56
+ "csharp",
57
+ "kotlin",
58
+ "swift",
59
+ "php",
60
+ "yaml",
61
+ "yml",
62
+ "toml",
63
+ "ini",
64
+ "md",
65
+ "markdown",
66
+ "mdx",
67
+ "html",
68
+ "css",
69
+ "scss",
70
+ "less",
71
+ "sql",
72
+ "xml",
73
+ "lua"
74
+ ]);
75
+ var MMD_LANGS = /* @__PURE__ */ new Set(["mermaid", "mermaidjs", "mmd"]);
76
+ function icon(paths) {
77
+ 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
+ return 'url("data:image/svg+xml,' + encodeURIComponent(svg) + '")';
79
+ }
80
+ var ICON_CODE = icon(
81
+ "<path d='M8.5 7 3.5 12l5 5'/><path d='M15.5 7l5 5-5 5'/><path d='M13.6 5.2 10.4 18.8'/>"
82
+ );
83
+ var ICON_COPY = icon(
84
+ "<rect x='9' y='9' width='12' height='12' rx='2.5'/><path d='M6 15H5.5A2.5 2.5 0 0 1 3 12.5v-7A2.5 2.5 0 0 1 5.5 3h7A2.5 2.5 0 0 1 15 5.5V6'/>"
85
+ );
86
+ var ICON_NOWRAP = icon(
87
+ "<path d='M3 12h12.5'/><path d='M12.5 8.5 16.5 12l-4 3.5'/><path d='M20 6v12'/>"
88
+ );
89
+ var ICON_DIAGRAM = icon(
90
+ "<rect x='3' y='3' width='8' height='5.5' rx='1.8'/><rect x='13' y='15.5' width='8' height='5.5' rx='1.8'/><path d='M7 8.5v5a2.5 2.5 0 0 0 2.5 2.5H13'/><path d='M10.5 13.5 13 16l-2.5 2.5'/>"
91
+ );
92
+ var ICON_ZOOM_IN = icon(
93
+ "<circle cx='10.5' cy='10.5' r='7'/><path d='M20.5 20.5 15.6 15.6'/><path d='M7.5 10.5h6'/><path d='M10.5 7.5v6'/>"
94
+ );
95
+ var ICON_ZOOM_OUT = icon(
96
+ "<circle cx='10.5' cy='10.5' r='7'/><path d='M20.5 20.5 15.6 15.6'/><path d='M7.5 10.5h6'/>"
97
+ );
98
+ var TABLE = '[class*="_markdown_"] div:has(> table)';
99
+ var TABLE_VARS = "--md-plus-tbl-radius:8px;--md-plus-tbl-radius-inner:7px;--md-plus-tbl-line:#ebebeb;--md-plus-tbl-head:#f7f7f7;--md-plus-tbl-head-fg:#181818;--md-plus-tbl-hover:#fafafa";
100
+ var TABLE_VARS_DARK = "--md-plus-tbl-line:#2f2f2f;--md-plus-tbl-head:#1f1f1f;--md-plus-tbl-head-fg:#f9fafb;--md-plus-tbl-hover:#1f1f1f";
101
+ var CSS = [
102
+ // ── 卡片:整块一个底色,顶栏不再是独立色带 ──────────────────────
103
+ ".md-code-block{--md-plus-mmd-min-height:200px;--md-plus-icon:" + ICON_CODE + ";--md-plus-icon-copy:" + ICON_COPY + ";--md-plus-icon-nowrap:" + ICON_NOWRAP + ";--md-plus-icon-diagram:" + ICON_DIAGRAM + ";--md-plus-icon-zoom-in:" + ICON_ZOOM_IN + ";--md-plus-icon-zoom-out:" + ICON_ZOOM_OUT + "}",
104
+ // bannerWrap 的原底色是 --dsw-alias-bg-base(#fff),会跟卡片灰拉开一条带
105
+ ".md-code-block > div:has(> " + BANNER + "){background:transparent}",
106
+ // ── 头部:收紧,并把空着的左边填上 ─────────────────────────────
107
+ // 左右 16px 与代码区对齐(目标图里头部的 `</>` 和首行代码是左对齐的)
108
+ // 下内边距留给 pre 的 line-height 去做,避免两处叠加出「断层」
109
+ ".md-code-block " + BANNER + "{padding:8px 16px 0;gap:8px}",
110
+ ".md-code-block " + BANNER + " > :first-child{display:inline-flex;align-items:center;gap:6px;min-width:0;font-size:13px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary)}",
111
+ // `</>` 图标
112
+ ".md-code-block " + BANNER + " > :first-child::before{content:'';flex:none;width:14px;height:14px;background:currentColor;color:var(--dsw-alias-label-tertiary);-webkit-mask:var(--md-plus-icon) center/contain no-repeat;mask:var(--md-plus-icon) center/contain no-repeat}",
113
+ // 没有语言时补个占位名,否则整行左边就是一片空白
114
+ ".md-code-block " + BANNER + " > :first-child:empty::after{content:'Plain text'}",
115
+ // ── 间距:头部与首行代码之间补 6px ──────────────────────────────
116
+ // pre 的行高本身就在首行文字上方留了约半行空白;补 6px 后实测间距约 17px
117
+ // (原来给 0 时只有 11px,贴着看太挤)
118
+ ".md-code-block pre{padding:6px 16px 16px}",
119
+ // ── 头部按钮:文字换成图标 ──────────────────────────────────────
120
+ ".md-code-block " + BANNER + " button{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:0;border-radius:6px;font-size:0;cursor:pointer;background:transparent;color:var(--dsw-alias-label-tertiary);transition:background-color .12s ease,color .12s ease}",
121
+ // font-size:0 只藏住「复制 / 已复制」的字面,可访问名仍在
122
+ ".md-code-block " + BANNER + " button::before{content:'';width:15px;height:15px;background:currentColor;-webkit-mask:var(--md-plus-icon-copy) center/contain no-repeat;mask:var(--md-plus-icon-copy) center/contain no-repeat}",
123
+ ".md-code-block " + BANNER + " button:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover,rgb(127 127 127 / 12%))}",
124
+ ".md-code-block " + BANNER + " button:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,#4c6ef5);outline-offset:1px}",
125
+ // ── 普通块:不换行切换 ──────────────────────────────────────────
126
+ ".md-code-block " + BANNER + " ." + TOOL_CLASS + "::before{-webkit-mask:var(--md-plus-icon-nowrap) center/contain no-repeat;mask:var(--md-plus-icon-nowrap) center/contain no-repeat}",
127
+ "[" + WRAP_ATTR + '="on"] .' + TOOL_CLASS + "{color:var(--dsw-alias-button-primary-fill,#4c6ef5);background:var(--dsw-alias-interactive-bg-active,rgb(76 110 245 / 12%))}",
128
+ "[" + WRAP_ATTR + '="on"] pre{white-space:pre;word-break:normal;overflow-x:auto}',
129
+ // ── mermaid 块:图形 / 代码 切换 + 缩放 ────────────────────────
130
+ ".md-code-block " + BANNER + " ." + TOGGLE_CLASS + "::before{-webkit-mask:var(--md-plus-icon-diagram) center/contain no-repeat;mask:var(--md-plus-icon-diagram) center/contain no-repeat}",
131
+ ".md-code-block " + BANNER + " ." + ZOOM_IN_CLASS + "::before{-webkit-mask:var(--md-plus-icon-zoom-in) center/contain no-repeat;mask:var(--md-plus-icon-zoom-in) center/contain no-repeat}",
132
+ ".md-code-block " + BANNER + " ." + ZOOM_OUT_CLASS + "::before{-webkit-mask:var(--md-plus-icon-zoom-out) center/contain no-repeat;mask:var(--md-plus-icon-zoom-out) center/contain no-repeat}",
133
+ "[" + MMD_ATTR + '="diagram"] .' + TOGGLE_CLASS + "{color:var(--dsw-alias-button-primary-fill,#4c6ef5);background:var(--dsw-alias-interactive-bg-active,rgb(76 110 245 / 12%))}",
134
+ // 已经到最小 / 最大时,对应那颗按钮变淡且不可点
135
+ "[" + ZOOM_ATTR + '="min"] .' + ZOOM_OUT_CLASS + ",[" + ZOOM_ATTR + '="max"] .' + ZOOM_IN_CLASS + "{opacity:.3;pointer-events:none}",
136
+ // 只有画成功时才藏源码;code / failed 两态都让源码露出来
137
+ "[" + MMD_ATTR + '="diagram"] > ' + CONTENT + " > pre{display:none}",
138
+ "[" + MMD_ATTR + '="code"] > ' + CONTENT + " > ." + MMD_CLASS + "{display:none}",
139
+ // 图形外两层:viewport 负责裁剪与拖拽光标,stage 负责缩放平移
140
+ "." + MMD_CLASS + "{padding:4px 16px 16px}",
141
+ "." + MMD_CLASS + "-viewport{overflow:hidden;position:relative}",
142
+ "." + MMD_CLASS + '-viewport[data-zoomed="on"]{cursor:grab}',
143
+ "." + MMD_CLASS + '-viewport[data-dragging="on"]{cursor:grabbing}',
144
+ // 横排长链这类长扁图渲染出来只有几十像素高,放大后就是个「缝」。
145
+ // 给 stage 兜一个最低高度、并让图在里面垂直居中,放大后才有地方看。
146
+ "." + MMD_CLASS + "-stage{transform-origin:0 0;display:flex;flex-direction:column;justify-content:center;min-height:var(--md-plus-mmd-min-height)}",
147
+ "." + MMD_CLASS + " svg{display:block;margin:0 auto;max-width:100%;height:auto}",
148
+ "." + MMD_CLASS + "-error{margin:0 16px 12px;padding:8px 12px;border-radius:8px;font:12px/18px var(--ds-font-family-code,monospace);white-space:pre-wrap;color:var(--dsw-alias-state-error-primary,#d33);background:var(--dsw-alias-bg-layer-2,rgb(127 127 127 / 8%))}",
149
+ // 高亮宿主不参与布局,让里面的 <pre> 直接吃到卡片样式
150
+ "." + HOST_CLASS + "{display:contents}",
151
+ // 藏掉纯文本回退,只留着色结果;原节点仍在,复制读的还是它
152
+ "[" + DONE_ATTR + '="done"] > ' + CONTENT + " > pre:not(.md-plus-pre){display:none}",
153
+ // ── 表格 ────────────────────────────────────────────────────────
154
+ // 抓手用结构选择器而不是 DSH 的哈希类名:外层是「唯一子元素是 table 的 div」。
155
+ // 前面挂 [class*="_markdown_"] 有两个作用——只认 markdown 渲染出来的表格,
156
+ // 同时把特异性顶到 (0,1,4),压过 DSH 自己的 .tableScroll th (0,1,1)。
157
+ TABLE + "{margin-block:16px;" + TABLE_VARS + "}",
158
+ "body[data-ds-dark-theme] " + TABLE + "{" + TABLE_VARS_DARK + "}",
159
+ // 整张表一个 1px 边框 + 小圆角。
160
+ // 注意必须是 separate:collapse 模式下 Chrome 会忽略单元格的圆角,
161
+ // 表头的灰底会在四角戳出直角。border-spacing 归零所以不会有缝。
162
+ TABLE + " > table{border:1px solid var(--md-plus-tbl-line);border-collapse:separate;border-spacing:0;border-radius:var(--md-plus-tbl-radius);overflow:hidden}",
163
+ // 表头:浅灰底 + 加粗深色字(DSH 原本没有底色)
164
+ TABLE + " > table th{background:var(--md-plus-tbl-head);color:var(--md-plus-tbl-head-fg);font-weight:600;padding:10px 16px;border-bottom:1px solid var(--md-plus-tbl-line);border-top:0}",
165
+ TABLE + " > table td{padding:10px 16px;border-bottom:1px solid var(--md-plus-tbl-line)}",
166
+ // 四角单独收圆,内圆角比外圆角少 1px(正好让开那圈边框)。
167
+ // 必须写明 thead / tbody:裸写 tr:first-child 会同时命中表头行和第一行数据,
168
+ // 而表头行本身就是 thead 的 last-child——漏掉 tbody 的话表头下沿也会被收圆,
169
+ // 灰底在下沿两端各戳出一个缺口。
170
+ TABLE + " > table thead tr:first-child > :first-child{border-top-left-radius:var(--md-plus-tbl-radius-inner)}",
171
+ TABLE + " > table thead tr:first-child > :last-child{border-top-right-radius:var(--md-plus-tbl-radius-inner)}",
172
+ TABLE + " > table tbody tr:last-child > :first-child{border-bottom-left-radius:var(--md-plus-tbl-radius-inner)}",
173
+ TABLE + " > table tbody tr:last-child > :last-child{border-bottom-right-radius:var(--md-plus-tbl-radius-inner)}",
174
+ // DSH 把首列左边距、末列右边距清零了,参考图里是留着的
175
+ TABLE + " > table th:first-child," + TABLE + " > table td:first-child{padding-left:16px}",
176
+ TABLE + " > table th:last-child," + TABLE + " > table td:last-child{padding-right:16px}",
177
+ // 最后一行的下边线交给 table 自己的边框,免得叠成双线
178
+ TABLE + " > table tr:last-child td{border-bottom:0}",
179
+ // 宽表横向扫读时给个落点
180
+ TABLE + " > table tbody tr:hover td{background:var(--md-plus-tbl-hover)}"
181
+ ].join("\n");
182
+ var scripts = /* @__PURE__ */ new Map();
183
+ var pending = /* @__PURE__ */ new Set();
184
+ var timer = null;
185
+ var rescanQueued = false;
186
+ var injected = /* @__PURE__ */ new Set();
187
+ var diagramSeq = 0;
188
+ function injectStyle() {
189
+ if (document.querySelector("style[" + STYLE_ATTR + "]") !== null) return;
190
+ var tag = document.createElement("style");
191
+ tag.setAttribute(STYLE_ATTR, "1");
192
+ tag.textContent = CSS;
193
+ document.head.appendChild(tag);
194
+ }
195
+ function loadScript(url, globalName) {
196
+ if (window[globalName] !== void 0) return Promise.resolve(window[globalName]);
197
+ var cached = scripts.get(url);
198
+ if (cached !== void 0) return cached;
199
+ var promise = new Promise((resolve) => {
200
+ var script = document.createElement("script");
201
+ script.src = url;
202
+ script.async = true;
203
+ script.setAttribute(RUNTIME_ATTR, "1");
204
+ script.addEventListener("load", () => resolve(window[globalName] ?? null), { once: true });
205
+ script.addEventListener("error", () => resolve(null), { once: true });
206
+ document.head.appendChild(script);
207
+ });
208
+ scripts.set(url, promise);
209
+ return promise;
210
+ }
211
+ function readLang(block) {
212
+ var banner = block.querySelector(BANNER);
213
+ var info = banner?.firstElementChild;
214
+ return (info?.textContent ?? "").trim().toLowerCase();
215
+ }
216
+ function isOurs(node) {
217
+ var element = node?.nodeType === 1 ? node : node?.parentElement;
218
+ return element?.closest?.("." + HOST_CLASS + ", ." + BTN_CLASS + ", ." + MMD_CLASS) != null;
219
+ }
220
+ function schedule(block) {
221
+ if (block !== void 0) pending.add(block);
222
+ else rescanQueued = true;
223
+ if (timer !== null) return;
224
+ timer = window.setTimeout(flush, DEBOUNCE_MS);
225
+ }
226
+ function flush() {
227
+ timer = null;
228
+ if (rescanQueued) {
229
+ rescanQueued = false;
230
+ for (const block of document.querySelectorAll(BLOCK)) pending.add(block);
231
+ }
232
+ var batch = Array.from(pending);
233
+ pending.clear();
234
+ for (const block of batch) {
235
+ decorate(block);
236
+ void enhance(block);
237
+ }
238
+ }
239
+ function onMutations(records) {
240
+ for (const record of records) {
241
+ if (record.type === "attributes" && record.attributeName === "data-ds-dark-theme") {
242
+ redrawDiagrams();
243
+ return;
244
+ }
245
+ if (isOurs(record.target)) continue;
246
+ var added = record.addedNodes;
247
+ var oursOnly = added.length > 0;
248
+ for (const node of added) {
249
+ if (isOurs(node)) continue;
250
+ oursOnly = false;
251
+ break;
252
+ }
253
+ if (oursOnly) continue;
254
+ schedule();
255
+ return;
256
+ }
257
+ }
258
+ function redrawDiagrams() {
259
+ for (const block of document.querySelectorAll("[" + MMD_ATTR + '="diagram"]')) {
260
+ block.removeAttribute(KEY_ATTR);
261
+ pending.add(block);
262
+ }
263
+ schedule();
264
+ }
265
+ var PLAIN_BUTTONS = [
266
+ {
267
+ cls: TOOL_CLASS,
268
+ title: "\u4E0D\u6362\u884C\uFF08\u6A2A\u5411\u6EDA\u52A8\uFF09",
269
+ aria: "\u5207\u6362\u4EE3\u7801\u6362\u884C",
270
+ run: (block) => {
271
+ if (block.getAttribute(WRAP_ATTR) === "on") block.removeAttribute(WRAP_ATTR);
272
+ else block.setAttribute(WRAP_ATTR, "on");
273
+ }
274
+ }
275
+ ];
276
+ var MMD_BUTTONS = [
277
+ { cls: ZOOM_OUT_CLASS, title: "\u7F29\u5C0F", aria: "\u7F29\u5C0F\u6D41\u7A0B\u56FE", run: (block) => zoomBy(block, 1 / ZOOM_STEP) },
278
+ { cls: ZOOM_IN_CLASS, title: "\u653E\u5927", aria: "\u653E\u5927\u6D41\u7A0B\u56FE", run: (block) => zoomBy(block, ZOOM_STEP) },
279
+ {
280
+ cls: TOGGLE_CLASS,
281
+ title: "\u5207\u6362\u56FE\u5F62 / \u4EE3\u7801",
282
+ aria: "\u5207\u6362\u56FE\u5F62\u4E0E\u4EE3\u7801",
283
+ run: (block) => {
284
+ var next = block.getAttribute(MMD_ATTR) === "code" ? "diagram" : "code";
285
+ block.setAttribute(MMD_ATTR, next);
286
+ }
287
+ }
288
+ ];
289
+ function decorate(block) {
290
+ if (!block.isConnected) return;
291
+ var banner = block.querySelector(BANNER);
292
+ var copy = banner?.querySelector("button:not(." + BTN_CLASS + ")");
293
+ var action = copy?.parentElement;
294
+ if (action === null || action === void 0) return;
295
+ var specs = MMD_LANGS.has(readLang(block)) ? MMD_BUTTONS : PLAIN_BUTTONS;
296
+ for (const existing of action.querySelectorAll("." + BTN_CLASS)) {
297
+ if (specs.some((spec) => existing.classList.contains(spec.cls))) continue;
298
+ existing.remove();
299
+ injected.delete(existing);
300
+ }
301
+ for (const spec of specs) {
302
+ if (action.querySelector("." + spec.cls) !== null) continue;
303
+ var created = document.createElement("button");
304
+ created.type = "button";
305
+ created.className = BTN_CLASS + " " + spec.cls;
306
+ created.title = spec.title;
307
+ created.setAttribute("aria-label", spec.aria);
308
+ created.addEventListener("click", () => spec.run(block));
309
+ action.insertBefore(created, copy);
310
+ injected.add(created);
311
+ }
312
+ }
313
+ async function enhance(block) {
314
+ if (!block.isConnected) return;
315
+ if (block.closest("[data-streaming]") !== null) return;
316
+ var content = block.querySelector(CONTENT);
317
+ if (content === null) return;
318
+ var pre = content.querySelector("pre");
319
+ if (pre === null || pre.classList.contains("shiki")) return;
320
+ var lang = readLang(block);
321
+ if (lang === "") return;
322
+ if (MMD_LANGS.has(lang)) {
323
+ await renderDiagram(block, content, pre);
324
+ return;
325
+ }
326
+ if (DSH_LANGS.has(lang)) return;
327
+ var api = await loadScript(HL_URL, HL_GLOBAL);
328
+ if (api === null || !api.supports(lang)) return;
329
+ if (!pre.isConnected) return;
330
+ var code = pre.textContent ?? "";
331
+ if (code.trim() === "") return;
332
+ var key = lang + "\0" + code;
333
+ if (block.getAttribute(KEY_ATTR) === key) return;
334
+ var html;
335
+ try {
336
+ html = await api.highlight(code, lang);
337
+ } catch {
338
+ return;
339
+ }
340
+ if (typeof html !== "string" || html === "") return;
341
+ if (!pre.isConnected || (pre.textContent ?? "") !== code) {
342
+ schedule(block);
343
+ return;
344
+ }
345
+ apply(block, content, pre, html, key);
346
+ }
347
+ function apply(block, content, pre, html, key) {
348
+ var host = document.createElement("div");
349
+ host.className = HOST_CLASS;
350
+ host.innerHTML = html;
351
+ var shikiPre = host.querySelector("pre");
352
+ if (shikiPre === null) return;
353
+ shikiPre.classList.add("md-plus-pre");
354
+ content.querySelector("." + HOST_CLASS)?.remove();
355
+ pre.after(host);
356
+ block.setAttribute(KEY_ATTR, key);
357
+ block.setAttribute(DONE_ATTR, "done");
358
+ }
359
+ async function renderDiagram(block, content, pre) {
360
+ var code = (pre.textContent ?? "").trim();
361
+ if (code === "") return;
362
+ var key = "mmd\0" + code;
363
+ if (block.getAttribute(KEY_ATTR) === key) return;
364
+ var api = await loadScript(MMD_URL, MMD_GLOBAL);
365
+ if (api === null) return;
366
+ if (!pre.isConnected) return;
367
+ var svg;
368
+ try {
369
+ var dark = document.body.hasAttribute("data-ds-dark-theme");
370
+ svg = await api.render("dsh-md-plus-mmd-" + ++diagramSeq, code, dark);
371
+ } catch (error) {
372
+ failDiagram(block, content, pre, key, error);
373
+ return;
374
+ }
375
+ if (typeof svg !== "string" || svg === "") return;
376
+ if (!pre.isConnected || (pre.textContent ?? "").trim() !== code) {
377
+ schedule(block);
378
+ return;
379
+ }
380
+ applyDiagram(block, content, pre, svg, key);
381
+ }
382
+ function applyDiagram(block, content, pre, svg, key) {
383
+ var host = content.querySelector("." + MMD_CLASS) ?? document.createElement("div");
384
+ host.className = MMD_CLASS;
385
+ host.innerHTML = '<div class="' + MMD_CLASS + '-viewport"><div class="' + MMD_CLASS + '-stage">' + svg + "</div></div>";
386
+ var viewport = host.firstElementChild;
387
+ if (viewport !== null) bindViewport(block, viewport);
388
+ pre.before(host);
389
+ content.querySelector("." + MMD_CLASS + "-error")?.remove();
390
+ block.setAttribute(KEY_ATTR, key);
391
+ if (block.getAttribute(MMD_ATTR) !== "code") block.setAttribute(MMD_ATTR, "diagram");
392
+ zooms.delete(block);
393
+ paint(block);
394
+ }
395
+ var zooms = /* @__PURE__ */ new WeakMap();
396
+ function zoomState(block) {
397
+ var state = zooms.get(block);
398
+ if (state === void 0) {
399
+ state = { scale: 1, x: 0, y: 0 };
400
+ zooms.set(block, state);
401
+ }
402
+ return state;
403
+ }
404
+ function stageOf(block) {
405
+ var stage = block.querySelector("." + MMD_CLASS + "-stage");
406
+ return stage?.parentElement == null ? null : stage;
407
+ }
408
+ function paint(block) {
409
+ var stage = stageOf(block);
410
+ if (stage === null) return;
411
+ var state = zoomState(block);
412
+ stage.style.transform = "translate(" + state.x + "px," + state.y + "px) scale(" + state.scale + ")";
413
+ stage.parentElement.setAttribute("data-zoomed", state.scale > 1.001 ? "on" : "off");
414
+ block.setAttribute(
415
+ ZOOM_ATTR,
416
+ state.scale <= 1.001 ? "min" : state.scale >= ZOOM_MAX ? "max" : "mid"
417
+ );
418
+ }
419
+ function clampPan(block, state) {
420
+ var stage = stageOf(block);
421
+ if (stage === null) return;
422
+ var box = stage.parentElement.getBoundingClientRect();
423
+ var width = stage.offsetWidth * state.scale;
424
+ var height = stage.offsetHeight * state.scale;
425
+ state.x = width <= box.width ? (box.width - width) / 2 : Math.min(0, Math.max(box.width - width, state.x));
426
+ state.y = height <= box.height ? (box.height - height) / 2 : Math.min(0, Math.max(box.height - height, state.y));
427
+ }
428
+ function zoomBy(block, factor, clientX, clientY) {
429
+ var stage = stageOf(block);
430
+ if (stage === null) return;
431
+ var state = zoomState(block);
432
+ var next = Math.min(ZOOM_MAX, Math.max(1, state.scale * factor));
433
+ if (Math.abs(next - state.scale) < 1e-3) return;
434
+ var box = stage.parentElement.getBoundingClientRect();
435
+ var anchorX = (clientX ?? box.left + box.width / 2) - box.left;
436
+ var anchorY = (clientY ?? box.top + box.height / 2) - box.top;
437
+ var ratio = next / state.scale;
438
+ state.x = anchorX - (anchorX - state.x) * ratio;
439
+ state.y = anchorY - (anchorY - state.y) * ratio;
440
+ state.scale = next;
441
+ clampPan(block, state);
442
+ paint(block);
443
+ }
444
+ function bindViewport(block, viewport) {
445
+ var drag = null;
446
+ viewport.addEventListener(
447
+ "wheel",
448
+ (event) => {
449
+ event.preventDefault();
450
+ zoomBy(block, event.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP, event.clientX, event.clientY);
451
+ },
452
+ { passive: false }
453
+ );
454
+ viewport.addEventListener("pointerdown", (event) => {
455
+ if (event.button !== 0 || zoomState(block).scale <= 1.001) return;
456
+ event.preventDefault();
457
+ drag = { id: event.pointerId, x: event.clientX, y: event.clientY };
458
+ try {
459
+ viewport.setPointerCapture(event.pointerId);
460
+ } catch {
461
+ }
462
+ viewport.setAttribute("data-dragging", "on");
463
+ });
464
+ viewport.addEventListener("pointermove", (event) => {
465
+ if (drag === null || event.pointerId !== drag.id) return;
466
+ var state = zoomState(block);
467
+ state.x += event.clientX - drag.x;
468
+ state.y += event.clientY - drag.y;
469
+ drag.x = event.clientX;
470
+ drag.y = event.clientY;
471
+ clampPan(block, state);
472
+ paint(block);
473
+ });
474
+ var stopDrag = (event) => {
475
+ if (drag === null || event.pointerId !== drag.id) return;
476
+ drag = null;
477
+ try {
478
+ viewport.releasePointerCapture(event.pointerId);
479
+ } catch {
480
+ }
481
+ viewport.removeAttribute("data-dragging");
482
+ };
483
+ viewport.addEventListener("pointerup", stopDrag);
484
+ viewport.addEventListener("pointercancel", stopDrag);
485
+ viewport.addEventListener("dblclick", () => {
486
+ zooms.delete(block);
487
+ paint(block);
488
+ });
489
+ }
490
+ function failDiagram(block, content, pre, key, error) {
491
+ content.querySelector("." + MMD_CLASS)?.remove();
492
+ var note = content.querySelector("." + MMD_CLASS + "-error") ?? document.createElement("p");
493
+ note.className = MMD_CLASS + "-error";
494
+ note.textContent = "mermaid \u6E32\u67D3\u5931\u8D25\uFF1A" + (error?.message ?? String(error));
495
+ content.appendChild(note);
496
+ block.setAttribute(KEY_ATTR, key);
497
+ block.setAttribute(MMD_ATTR, "failed");
498
+ console.warn("[dsh-md-plus] mermaid \u6E32\u67D3\u5931\u8D25", error);
499
+ }
500
+ function apply0(ctx) {
501
+ if (typeof document === "undefined") return;
502
+ ctx.effect(() => {
503
+ injectStyle();
504
+ var root = document.body ?? document.documentElement;
505
+ var observer = new MutationObserver(onMutations);
506
+ observer.observe(root, {
507
+ childList: true,
508
+ subtree: true,
509
+ characterData: true,
510
+ attributes: true,
511
+ attributeFilter: ["data-streaming", "data-ds-dark-theme"]
512
+ });
513
+ schedule();
514
+ return () => {
515
+ observer.disconnect();
516
+ for (const node of injected) node.remove();
517
+ injected.clear();
518
+ for (const node of document.querySelectorAll(
519
+ "." + MMD_CLASS + ", ." + MMD_CLASS + "-error"
520
+ )) {
521
+ node.remove();
522
+ }
523
+ document.querySelector("style[" + STYLE_ATTR + "]")?.remove();
524
+ for (const script of document.querySelectorAll("script[" + RUNTIME_ATTR + "]")) {
525
+ script.remove();
526
+ }
527
+ scripts.clear();
528
+ };
529
+ }, "dsh-md-plus: code block styling, highlighting and diagrams");
530
+ }
531
+ exports.apply = apply0;
532
+ exports.inject = [];
533
+ return module.exports;
534
+ }
535
+ });