dsh-md-plus 0.4.5 → 0.5.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # 更新日志
2
2
 
3
+ ## 0.5.0 —— 2026-11-04
4
+
5
+ - 表格支持按设置冻结前 N 行 / 前 M 列,默认冻结 1 行表头、0 列。
6
+ - 修复表格展开后「收起」按钮横向跟着滚动条跑的问题:展开态按钮横向改用 sticky 钉在可视区中间,纵向仍保持滚到表尾才出现。
7
+ - 表格复制按钮增加 TSV 和富文本(HTML)格式。
8
+ - 设置面板增加冻结行数 / 列数设置、「全部展开 / 全部收起」折叠操作和设置导入 / 导出;底部操作区重新分组,工具操作与主操作分层排列。
9
+ - Mermaid 卡片增加「复制图源」按钮;普通代码块增加下载为文件按钮。
10
+ - 修复「代码字号」留空或 0 时并未真正跟随宿主的问题:此前回退 `inherit` 会把代码块字号撑成正文大小,现在回退宿主 `--dsh-code-font-size` 变量。
11
+ - 测试补到 91 条,补强冻结行列、TSV / 富文本复制、全局折叠、复制图源和下载按钮等回归用例。
12
+
3
13
  ## 0.4.5 —— 2026-11-03
4
14
 
5
15
  - 修复保存设置时无条件重绘所有 Mermaid 图、重置所有折叠状态的问题:只对真正变化的设置项执行重绘或重新测量,用户手动展开的长代码块和长表格不会被折叠回去。
package/README.md CHANGED
@@ -6,12 +6,12 @@
6
6
 
7
7
  ## 功能
8
8
 
9
- - **代码块**:统一成完整卡片,头部显示 `</>` 图标和语言名(无语言显示 `Plain text`),保留宿主原生的换行与复制按钮,顶栏与卡片同色。
10
- - **Mermaid**:围栏渲染成 SVG,可在「图形 / 代码」间切换;支持全屏查看(滚轮缩放、拖拽、双击适应、Esc 关闭)、四套配色,以及下载 SVG / PNG。
9
+ - **代码块**:统一成完整卡片,头部显示 `</>` 图标和语言名(无语言显示 `Plain text`),保留宿主原生的换行与复制按钮,并支持下载为文件;顶栏与卡片同色。
10
+ - **Mermaid**:围栏渲染成 SVG,可在「图形 / 代码」间切换;支持复制图源、全屏查看(滚轮缩放、拖拽、双击适应、Esc 关闭)、四套配色,以及下载 SVG / PNG。
11
11
  - **长内容折叠**:代码块、表格、图表超过阈值自动折叠,底部出现「展开 / 收起」,长日志和长表格不会撑满聊天。
12
- - **表格**:浅色 / 深色两套样式,表头吸顶,宽表支持表内横向滚动;每张表上方右侧有「CSV / Markdown」复制按钮。
12
+ - **表格**:浅色 / 深色两套样式,支持表内横向滚动;可按设置冻结前 N 行 / 前 M 列(默认冻结 1 行表头、0 列);每张表上方右侧支持 CSV / TSV / Markdown / 富文本复制。
13
13
  - **图片放大**:只在旧宿主没有原生预览按钮时兜底,点击图片打开暗色预览,Esc 或点遮罩关闭;新版 DSH 的原生预览、头像、图标、代码块和链接里的图片都不接管。
14
- - **设置面板**:右下角齿轮可调 Mermaid 配色、代码字号、图表默认视图和三类折叠阈值,阈值下方会注明按高度判断。设置存在 `localStorage`,界面文案跟随页面 `<html lang>` 切换中英文。
14
+ - **设置面板**:右下角齿轮可调 Mermaid 配色、代码字号、图表默认视图、表格冻结行数 / 列数和三类折叠阈值,也能一键全部展开 / 收起、导出 / 导入设置文件;阈值下方会注明按高度判断。设置存在 `localStorage`,界面文案跟随页面 `<html lang>` 切换中英文。
15
15
 
16
16
  ## 安装与卸载
17
17
 
@@ -27,7 +27,7 @@ dsh plugin --profile desktop remove dsh-md-plus
27
27
  ```powershell
28
28
  npm install # 顺带跑 prepare → build
29
29
  npm run build # 产出 lib/ 下四个文件
30
- npm run check # 构建 + 语法检查 + 85 条测试(check 会先 build)
30
+ npm run check # 构建 + 语法检查 + 91 条测试(check 会先 build)
31
31
  npm run release:dry # 维护者发版:先看 CHANGELOG 日期 + 提交 + tag + push 计划
32
32
  npm run release # 维护者发版:改 CHANGELOG 日期 + 提交 + 打 tag + 推送(tag 会触发 GitHub Actions 自动发 npm)
33
33
  npm run preview # 生成 preview.html(含真渲出来的 mermaid 图)
package/lib/client.js CHANGED
@@ -18,6 +18,8 @@ window.__ModuleLoader__.load({
18
18
  var BTN_CLASS = "md-plus-btn";
19
19
  var ZOOM_IN_CLASS = "md-plus-zoom-in";
20
20
  var ZOOM_OUT_CLASS = "md-plus-zoom-out";
21
+ var COPY_SOURCE_CLASS = "md-plus-copy-source";
22
+ var DOWNLOAD_CLASS = "md-plus-download";
21
23
  var ZOOM_ATTR = "data-md-plus-zoom";
22
24
  var ZOOM_STEP = 1.25;
23
25
  var ZOOM_MAX = 6;
@@ -52,6 +54,10 @@ window.__ModuleLoader__.load({
52
54
  toggleAria: "\u5207\u6362\u56FE\u5F62\u4E0E\u4EE3\u7801",
53
55
  fullscreen: "\u5168\u5C4F\u67E5\u770B",
54
56
  fullscreenAria: "\u5168\u5C4F\u67E5\u770B\u6D41\u7A0B\u56FE",
57
+ copySource: "\u590D\u5236\u56FE\u6E90",
58
+ copySourceAria: "\u590D\u5236 Mermaid \u56FE\u6E90",
59
+ downloadCode: "\u4E0B\u8F7D\u4EE3\u7801",
60
+ downloadCodeAria: "\u4E0B\u8F7D\u4EE3\u7801\u4E3A\u6587\u4EF6",
55
61
  theme: "\u5207\u6362 Mermaid \u914D\u8272",
56
62
  themeAria: "\u5207\u6362 Mermaid \u914D\u8272",
57
63
  themePrefix: "\u914D\u8272\uFF1A",
@@ -68,9 +74,13 @@ window.__ModuleLoader__.load({
68
74
  exportFailed: "\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u6539\u7528 SVG",
69
75
  close: "\u5173\u95ED",
70
76
  tableCsv: "CSV",
77
+ tableTsv: "TSV",
71
78
  tableMd: "Markdown",
79
+ tableHtml: "\u5BCC\u6587\u672C",
72
80
  copyCsvTitle: "\u590D\u5236\u8868\u683C\u4E3A CSV",
81
+ copyTsvTitle: "\u590D\u5236\u8868\u683C\u4E3A TSV",
73
82
  copyMdTitle: "\u590D\u5236\u8868\u683C\u4E3A Markdown",
83
+ copyHtmlTitle: "\u590D\u5236\u8868\u683C\u4E3A\u5BCC\u6587\u672C",
74
84
  copied: "\u5DF2\u590D\u5236",
75
85
  copyFailed: "\u590D\u5236\u5931\u8D25",
76
86
  imageViewer: "\u56FE\u7247\u9884\u89C8",
@@ -84,6 +94,8 @@ window.__ModuleLoader__.load({
84
94
  settingsViewDiagram: "\u56FE\u5F62",
85
95
  settingsViewCode: "\u4EE3\u7801",
86
96
  settingsFold: "\u957F\u5185\u5BB9\u6298\u53E0\u9608\u503C",
97
+ settingsFoldGroup: "\u6298\u53E0\u5185\u5BB9",
98
+ settingsFileGroup: "\u8BBE\u7F6E\u6587\u4EF6",
87
99
  settingsFoldCode: "\u4EE3\u7801\u5757",
88
100
  settingsFoldTable: "\u8868\u683C",
89
101
  settingsFoldDiagram: "\u56FE\u8868",
@@ -91,6 +103,15 @@ window.__ModuleLoader__.load({
91
103
  settingsFoldTableHint: "\u8868\u683C\u9AD8\u5EA6\u8D85\u8FC7\u6B64\u503C\u540E\u6298\u53E0",
92
104
  settingsFoldDiagramHint: "\u56FE\u5F62\u9AD8\u5EA6\u8D85\u8FC7\u6B64\u503C\u540E\u6298\u53E0",
93
105
  settingsFoldHint: "\u5355\u4F4D px\uFF0C0 \u8868\u793A\u4E0D\u6298\u53E0",
106
+ settingsFreezeRows: "\u51BB\u7ED3\u884C\u6570",
107
+ settingsFreezeRowsHint: "\u9ED8\u8BA4\u51BB\u7ED3 1 \u884C\u8868\u5934\uFF0C0 \u8868\u793A\u4E0D\u51BB\u7ED3",
108
+ settingsFreezeCols: "\u51BB\u7ED3\u5217\u6570",
109
+ settingsFreezeColsHint: "\u9ED8\u8BA4 0 \u5217\uFF0C\u6309\u9700\u6307\u5B9A",
110
+ foldExpandAll: "\u5168\u90E8\u5C55\u5F00",
111
+ foldCollapseAll: "\u5168\u90E8\u6536\u8D77",
112
+ settingsExport: "\u5BFC\u51FA\u8BBE\u7F6E",
113
+ settingsImport: "\u5BFC\u5165\u8BBE\u7F6E",
114
+ settingsImportFailed: "\u5BFC\u5165\u5931\u8D25",
94
115
  settingsSave: "\u4FDD\u5B58",
95
116
  settingsReset: "\u6062\u590D\u9ED8\u8BA4",
96
117
  settingsClose: "\u5173\u95ED"
@@ -106,6 +127,10 @@ window.__ModuleLoader__.load({
106
127
  toggleAria: "Switch diagram and code",
107
128
  fullscreen: "Fullscreen",
108
129
  fullscreenAria: "View diagram fullscreen",
130
+ copySource: "Copy source",
131
+ copySourceAria: "Copy Mermaid source",
132
+ downloadCode: "Download code",
133
+ downloadCodeAria: "Download code as a file",
109
134
  theme: "Change Mermaid theme",
110
135
  themeAria: "Change Mermaid theme",
111
136
  themePrefix: "Theme: ",
@@ -122,9 +147,13 @@ window.__ModuleLoader__.load({
122
147
  exportFailed: "Export failed; try SVG instead",
123
148
  close: "Close",
124
149
  tableCsv: "CSV",
150
+ tableTsv: "TSV",
125
151
  tableMd: "Markdown",
152
+ tableHtml: "Rich",
126
153
  copyCsvTitle: "Copy table as CSV",
154
+ copyTsvTitle: "Copy table as TSV",
127
155
  copyMdTitle: "Copy table as Markdown",
156
+ copyHtmlTitle: "Copy table as rich text",
128
157
  copied: "Copied",
129
158
  copyFailed: "Copy failed",
130
159
  imageViewer: "Image preview",
@@ -138,6 +167,8 @@ window.__ModuleLoader__.load({
138
167
  settingsViewDiagram: "Diagram",
139
168
  settingsViewCode: "Code",
140
169
  settingsFold: "Fold thresholds",
170
+ settingsFoldGroup: "Fold content",
171
+ settingsFileGroup: "Settings file",
141
172
  settingsFoldCode: "Code block",
142
173
  settingsFoldTable: "Table",
143
174
  settingsFoldDiagram: "Diagram",
@@ -145,6 +176,15 @@ window.__ModuleLoader__.load({
145
176
  settingsFoldTableHint: "Fold when the table is taller than this",
146
177
  settingsFoldDiagramHint: "Fold when the diagram is taller than this",
147
178
  settingsFoldHint: "Pixels; 0 disables folding",
179
+ settingsFreezeRows: "Freeze rows",
180
+ settingsFreezeRowsHint: "Defaults to 1 header row; 0 disables freezing",
181
+ settingsFreezeCols: "Freeze columns",
182
+ settingsFreezeColsHint: "Defaults to 0 columns; set as needed",
183
+ foldExpandAll: "Expand all",
184
+ foldCollapseAll: "Collapse all",
185
+ settingsExport: "Export settings",
186
+ settingsImport: "Import settings",
187
+ settingsImportFailed: "Import failed",
148
188
  settingsSave: "Save",
149
189
  settingsReset: "Reset to defaults",
150
190
  settingsClose: "Close"
@@ -164,6 +204,9 @@ window.__ModuleLoader__.load({
164
204
  var FOLD_LIMITS = { code: 480, table: 520, diagram: 720 };
165
205
  var TABLE_TOOLS_CLASS = "md-plus-table-tools";
166
206
  var TABLE_DONE_ATTR = "data-md-plus-table";
207
+ var FREEZE_ROW_CLASS = "md-plus-freeze-row";
208
+ var FREEZE_COL_CLASS = "md-plus-freeze-col";
209
+ var FREEZE_CROSS_CLASS = "md-plus-freeze-cross";
167
210
  var IMG_MODAL_CLASS = "md-plus-img-modal";
168
211
  var NATIVE_IMAGE_BUTTON = 'button[class*="imageButton"]';
169
212
  var SETTINGS_BTN_CLASS = "md-plus-settings-btn";
@@ -174,7 +217,9 @@ window.__ModuleLoader__.load({
174
217
  diagramView: "diagram",
175
218
  foldCode: FOLD_LIMITS.code,
176
219
  foldTable: FOLD_LIMITS.table,
177
- foldDiagram: FOLD_LIMITS.diagram
220
+ foldDiagram: FOLD_LIMITS.diagram,
221
+ freezeRows: 1,
222
+ freezeCols: 0
178
223
  };
179
224
  var STYLE_ATTR = "data-md-plus-style";
180
225
  var RUNTIME_ATTR = "data-md-plus-runtime";
@@ -199,6 +244,12 @@ window.__ModuleLoader__.load({
199
244
  var value = Number(saved[key]);
200
245
  if (Number.isFinite(value) && value >= 0) settings[key] = Math.min(4e3, Math.round(value));
201
246
  }
247
+ for (var freezeKey of ["freezeRows", "freezeCols"]) {
248
+ var freezeValue = Number(saved[freezeKey]);
249
+ if (saved[freezeKey] !== void 0 && Number.isFinite(freezeValue) && freezeValue >= 0) {
250
+ settings[freezeKey] = Math.min(5, Math.round(freezeValue));
251
+ }
252
+ }
202
253
  }
203
254
  memorySettings = { ...settings };
204
255
  } catch {
@@ -356,6 +407,9 @@ window.__ModuleLoader__.load({
356
407
  var ICON_COPY = icon(
357
408
  "<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'/>"
358
409
  );
410
+ var ICON_DOWNLOAD = icon(
411
+ "<path d='M12 3v12'/><path d='m7 10 5 5 5-5'/><path d='M5 21h14'/>"
412
+ );
359
413
  var ICON_NOWRAP = icon(
360
414
  "<path d='M3 12h12.5'/><path d='M12.5 8.5 16.5 12l-4 3.5'/><path d='M20 6v12'/>"
361
415
  );
@@ -385,7 +439,7 @@ window.__ModuleLoader__.load({
385
439
  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";
386
440
  var CSS = [
387
441
  // ── 卡片:整块一个底色,顶栏不再是独立色带 ──────────────────────
388
- ".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 + ";--md-plus-icon-expand:" + ICON_EXPAND + ";--md-plus-icon-theme:" + ICON_THEME + ";--md-plus-icon-retry:" + ICON_RETRY + "}",
442
+ ".md-code-block{--md-plus-mmd-min-height:200px;--md-plus-icon:" + ICON_CODE + ";--md-plus-icon-copy:" + ICON_COPY + ";--md-plus-icon-download:" + ICON_DOWNLOAD + ";--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 + ";--md-plus-icon-expand:" + ICON_EXPAND + ";--md-plus-icon-theme:" + ICON_THEME + ";--md-plus-icon-retry:" + ICON_RETRY + "}",
389
443
  // bannerWrap 的原底色是 --dsw-alias-bg-base(#fff),会跟卡片灰拉开一条带
390
444
  ".md-code-block > div:has(> " + BANNER + "){background:transparent}",
391
445
  // ── 头部:收紧,并把空着的左边填上 ─────────────────────────────
@@ -401,8 +455,10 @@ window.__ModuleLoader__.load({
401
455
  // ── 间距:头部与首行代码之间补 6px ──────────────────────────────
402
456
  // pre 的行高本身就在首行文字上方留了约半行空白;补 6px 后实测间距约 17px
403
457
  // (原来给 0 时只有 11px,贴着看太挤)
404
- ".md-code-block pre{padding:6px 16px 16px;font-size:var(--md-plus-code-font-size,inherit)}",
405
- ".md-code-block pre{line-height:var(--md-plus-code-line-height,inherit)}",
458
+ // 未设字号时不能回退 inherit:pre 的父级是正文,会把宿主的代码字号
459
+ // (--dsh-code-font-size)盖成正文大小;回退到宿主变量才是真「跟随宿主」。
460
+ ".md-code-block pre{padding:6px 16px 16px;font-size:var(--md-plus-code-font-size,var(--dsh-code-font-size,11px))}",
461
+ ".md-code-block pre{line-height:var(--md-plus-code-line-height,calc(19px + var(--dsh-code-font-delta,0px)))}",
406
462
  // DSH 0.2.1 把内容容器设成 display:contents,折叠需要真实的盒子才能量高度和裁剪
407
463
  ".md-code-block > [data-code-block-content]{display:block}",
408
464
  // ── 头部按钮:文字换成图标 ──────────────────────────────────────
@@ -423,6 +479,8 @@ window.__ModuleLoader__.load({
423
479
  ".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}",
424
480
  ".md-code-block " + BANNER + " ." + EXPAND_CLASS + "::before{-webkit-mask:var(--md-plus-icon-expand) center/contain no-repeat;mask:var(--md-plus-icon-expand) center/contain no-repeat}",
425
481
  ".md-code-block " + BANNER + " ." + THEME_CLASS + "::before{-webkit-mask:var(--md-plus-icon-theme) center/contain no-repeat;mask:var(--md-plus-icon-theme) center/contain no-repeat}",
482
+ ".md-code-block " + BANNER + " ." + COPY_SOURCE_CLASS + "::before{-webkit-mask:var(--md-plus-icon-copy) center/contain no-repeat;mask:var(--md-plus-icon-copy) center/contain no-repeat}",
483
+ ".md-code-block " + BANNER + " ." + DOWNLOAD_CLASS + "::before{-webkit-mask:var(--md-plus-icon-download) center/contain no-repeat;mask:var(--md-plus-icon-download) center/contain no-repeat}",
426
484
  ".md-code-block " + BANNER + " ." + RETRY_CLASS + "::before{-webkit-mask:var(--md-plus-icon-retry) center/contain no-repeat;mask:var(--md-plus-icon-retry) center/contain no-repeat}",
427
485
  "[" + 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%))}",
428
486
  // 已经到最小 / 最大时,对应那颗按钮变淡且不可点
@@ -471,19 +529,29 @@ window.__ModuleLoader__.load({
471
529
  "." + SETTINGS_BTN_CLASS + "::before{content:'';width:16px;height:16px;background:currentColor;-webkit-mask:var(--md-plus-icon-gear) center/contain no-repeat;mask:var(--md-plus-icon-gear) center/contain no-repeat}",
472
530
  "." + SETTINGS_MODAL_CLASS + "{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;padding:calc(var(--dsh-windows-titlebar-height,0px) + 16px) 24px 24px;background:rgb(0 0 0 / 48%)}",
473
531
  "." + SETTINGS_MODAL_CLASS + "-panel{width:min(440px,100%);max-height:86vh;overflow:auto;padding:16px 18px 14px;border:1px solid var(--dsw-alias-border-secondary,rgb(127 127 127 / 18%));border-radius:12px;background:var(--dsw-alias-bg-base,#fff);box-shadow:0 24px 80px rgb(0 0 0 / 25%)}",
474
- "." + SETTINGS_MODAL_CLASS + "-title{margin:0 0 6px;font:600 14px/22px var(--dsw-font-family-sans,system-ui,sans-serif);color:var(--dsw-alias-label-primary)}",
475
- "." + SETTINGS_MODAL_CLASS + "-row{display:flex;align-items:center;gap:12px;margin:11px 0}",
532
+ "." + SETTINGS_MODAL_CLASS + "-title{margin:0 0 10px;font:600 14px/22px var(--dsw-font-family-sans,system-ui,sans-serif);color:var(--dsw-alias-label-primary)}",
533
+ "." + SETTINGS_MODAL_CLASS + "-row{display:flex;align-items:center;gap:12px;min-height:36px;margin:0 0 10px}",
476
534
  "." + SETTINGS_MODAL_CLASS + "-label{display:flex;flex-direction:column;gap:1px;flex:1;min-width:0;font:13px/20px var(--dsw-font-family-sans,system-ui,sans-serif);color:var(--dsw-alias-label-primary)}",
477
535
  "." + SETTINGS_MODAL_CLASS + "-label-title{display:block}",
478
536
  "." + SETTINGS_MODAL_CLASS + "-label-hint{display:block;font:11px/16px var(--dsw-font-family-sans,system-ui,sans-serif);color:var(--dsw-alias-label-tertiary)}",
479
537
  "." + SETTINGS_MODAL_CLASS + "-hint{margin:-4px 0 8px;font:11px/16px var(--dsw-font-family-sans,system-ui,sans-serif);color:var(--dsw-alias-label-tertiary)}",
538
+ "." + SETTINGS_MODAL_CLASS + "-section{margin:14px 0 8px;font:600 12px/18px var(--dsw-font-family-sans,system-ui,sans-serif);color:var(--dsw-alias-label-secondary)}",
480
539
  "." + SETTINGS_MODAL_CLASS + " input[type=number]{width:78px;padding:4px 6px;border:1px solid var(--dsw-alias-border-secondary,rgb(127 127 127 / 22%));border-radius:6px;background:var(--dsw-alias-bg-layer-2,rgb(127 127 127 / 6%));color:var(--dsw-alias-label-primary);font:13px/18px var(--dsw-font-family-sans,system-ui,sans-serif)}",
481
540
  "." + SETTINGS_MODAL_CLASS + " select{padding:4px 6px;border:1px solid var(--dsw-alias-border-secondary,rgb(127 127 127 / 22%));border-radius:6px;background:var(--dsw-alias-bg-layer-2,rgb(127 127 127 / 6%));color:var(--dsw-alias-label-primary);font:13px/18px var(--dsw-font-family-sans,system-ui,sans-serif)}",
482
- "." + SETTINGS_MODAL_CLASS + "-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}",
483
- "." + SETTINGS_MODAL_CLASS + "-actions button{appearance:none;border:0;border-radius:6px;padding:5px 12px;background:var(--dsw-alias-interactive-bg-hover,rgb(127 127 127 / 12%));color:var(--dsw-alias-label-primary);font:12px/18px var(--dsw-font-family-sans,system-ui,sans-serif);cursor:pointer}",
484
- "." + SETTINGS_MODAL_CLASS + "-actions button:not(." + SETTINGS_MODAL_CLASS + "-primary):hover{background:var(--dsw-alias-interactive-bg-active,rgb(76 110 245 / 12%))}",
541
+ "." + SETTINGS_MODAL_CLASS + "-footer{margin-top:18px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-secondary,rgb(127 127 127 / 18%))}",
542
+ "." + SETTINGS_MODAL_CLASS + "-utilities{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px 18px;margin:0 0 14px}",
543
+ "." + SETTINGS_MODAL_CLASS + "-fold-actions,." + SETTINGS_MODAL_CLASS + "-transfer-actions{display:flex;align-items:center;gap:8px}",
544
+ "." + SETTINGS_MODAL_CLASS + "-utility-label{font:11px/16px var(--dsw-font-family-sans,system-ui,sans-serif);color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
545
+ "." + SETTINGS_MODAL_CLASS + "-segment{display:inline-flex;align-items:center;gap:1px;padding:2px;border:1px solid var(--dsw-alias-border-secondary,rgb(127 127 127 / 18%));border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgb(127 127 127 / 5%))}",
546
+ "." + SETTINGS_MODAL_CLASS + "-segment button{appearance:none;border:0;border-radius:6px;padding:4px 8px;background:transparent;color:var(--dsw-alias-label-secondary);font:12px/18px var(--dsw-font-family-sans,system-ui,sans-serif);cursor:pointer}",
547
+ "." + SETTINGS_MODAL_CLASS + "-segment button:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(127 127 127 / 12%));color:var(--dsw-alias-label-primary)}",
548
+ "." + SETTINGS_MODAL_CLASS + "-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:0}",
549
+ "." + SETTINGS_MODAL_CLASS + "-actions button{appearance:none;border:0;border-radius:7px;min-height:30px;padding:5px 12px;background:transparent;color:var(--dsw-alias-label-secondary);font:12px/18px var(--dsw-font-family-sans,system-ui,sans-serif);cursor:pointer}",
550
+ "." + SETTINGS_MODAL_CLASS + "-actions button:first-child{margin-right:auto;color:var(--dsw-alias-label-tertiary)}",
551
+ "." + SETTINGS_MODAL_CLASS + "-actions button:not(." + SETTINGS_MODAL_CLASS + "-primary):hover{background:var(--dsw-alias-interactive-bg-hover,rgb(127 127 127 / 12%));color:var(--dsw-alias-label-primary)}",
485
552
  "." + SETTINGS_MODAL_CLASS + "-actions ." + SETTINGS_MODAL_CLASS + "-primary{background:var(--dsw-alias-button-primary-fill,#4c6ef5);color:var(--dsw-alias-label-primary-foreground,#fff)}",
486
553
  "." + SETTINGS_MODAL_CLASS + "-actions ." + SETTINGS_MODAL_CLASS + "-primary:hover{background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill,#4c6ef5))}",
554
+ "." + SETTINGS_MODAL_CLASS + "-fold-actions button:focus-visible,." + SETTINGS_MODAL_CLASS + "-transfer-actions button:focus-visible,." + SETTINGS_MODAL_CLASS + "-actions button:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,#4c6ef5);outline-offset:1px}",
487
555
  // 高亮宿主不参与布局,让里面的 <pre> 直接吃到卡片样式
488
556
  "." + HOST_CLASS + "{display:contents}",
489
557
  // 藏掉纯文本回退,只留着色结果;原节点仍在,复制读的还是它
@@ -519,8 +587,15 @@ window.__ModuleLoader__.load({
519
587
  TABLE + " > table tr:last-child td{border-bottom:0}",
520
588
  // 宽表横向扫读时给个落点
521
589
  TABLE + " > table tbody tr:hover td{background:var(--md-plus-tbl-hover)}",
522
- // 长表往下滚时表头留在视口顶部;底色不透明才不会透出下面的行
523
- TABLE + " > table thead th{position:sticky;top:0;z-index:1}",
590
+ // 冻结行 / 列:具体偏移由客户端按实际行列尺寸算好,支持设置多行多列
591
+ TABLE + " > table ." + FREEZE_ROW_CLASS + "{position:sticky;z-index:2;box-shadow:0 1px 0 var(--md-plus-tbl-line)}",
592
+ TABLE + " > table th." + FREEZE_ROW_CLASS + "{background:var(--md-plus-tbl-head)}",
593
+ TABLE + " > table td." + FREEZE_ROW_CLASS + "{background:var(--dsw-alias-bg-base,#fff)}",
594
+ TABLE + " > table ." + FREEZE_COL_CLASS + "{position:sticky;z-index:1;box-shadow:1px 0 0 var(--md-plus-tbl-line)}",
595
+ TABLE + " > table th." + FREEZE_COL_CLASS + "{background:var(--md-plus-tbl-head)}",
596
+ TABLE + " > table td." + FREEZE_COL_CLASS + "{background:var(--dsw-alias-bg-base,#fff)}",
597
+ TABLE + " > table ." + FREEZE_CROSS_CLASS + "{z-index:3}",
598
+ TABLE + " > table tbody tr:hover td." + FREEZE_ROW_CLASS + "," + TABLE + " > table tbody tr:hover td." + FREEZE_COL_CLASS + "{background:var(--md-plus-tbl-hover)}",
524
599
  // 复制按钮放在表尾右侧:不压住表头文字,表格折叠时也能随展开一起出现
525
600
  TABLE + " > table > caption." + TABLE_TOOLS_CLASS + "{caption-side:top;position:sticky;right:0;width:0;height:30px;margin-left:auto;padding:0;text-align:right;z-index:2}",
526
601
  TABLE + " > table > caption." + TABLE_TOOLS_CLASS + " > span{position:absolute;right:0;top:0;display:flex;gap:0;white-space:nowrap}",
@@ -537,7 +612,11 @@ window.__ModuleLoader__.load({
537
612
  // overscroll-behavior:auto 时才会把纵向滚轮交还给页面。
538
613
  // 这里同时写明 min/max-height:基础表格规则的 max-height 特异性更高,不补会压过设置值;
539
614
  // 最小高度保证极低阈值下「展开」按钮仍完整可见。
540
- TABLE + "." + FOLD_CLASS + "[" + FOLD_ATTR + '="on"]{min-height:48px;max-height:var(--md-plus-fold-max-height);overflow-x:auto;overflow-y:clip}'
615
+ TABLE + "." + FOLD_CLASS + "[" + FOLD_ATTR + '="on"]{min-height:48px;max-height:var(--md-plus-fold-max-height);overflow-x:auto;overflow-y:clip}',
616
+ // 展开态的「收起」按钮在表尾文档流里,纵向保持滚到底才出现;
617
+ // 横向用 sticky 钉在可视区中间,宽表左右滚动时不会跟着表内容跑掉。
618
+ // 折叠态按钮是绝对定位,横向跟随由 sync() 写 left 完成。
619
+ TABLE + " ." + FOLD_BTN_CLASS + "{position:sticky;left:50%;transform:translateX(-50%)}"
541
620
  ].join("\n");
542
621
  var scripts = /* @__PURE__ */ new Map();
543
622
  var pending = /* @__PURE__ */ new Set();
@@ -550,6 +629,9 @@ window.__ModuleLoader__.load({
550
629
  var timer = null;
551
630
  var rescanQueued = false;
552
631
  var injected = /* @__PURE__ */ new Set();
632
+ var tableFreezeObserver = null;
633
+ var tableFreezeTargets = /* @__PURE__ */ new Set();
634
+ var tableFreezeState = /* @__PURE__ */ new WeakMap();
553
635
  var diagramSeq = 0;
554
636
  var disposed = false;
555
637
  function injectStyle() {
@@ -717,6 +799,7 @@ window.__ModuleLoader__.load({
717
799
  if (!node.isConnected) injected.delete(node);
718
800
  }
719
801
  cleanupFoldTargets();
802
+ cleanupTableFreezeTargets();
720
803
  diagramKindCache = /* @__PURE__ */ new WeakMap();
721
804
  if (rescanQueued) {
722
805
  rescanQueued = false;
@@ -760,17 +843,22 @@ window.__ModuleLoader__.load({
760
843
  }
761
844
  schedule();
762
845
  }
763
- var PLAIN_BUTTONS = [
764
- {
765
- cls: TOOL_CLASS,
766
- titleKey: "wrapTitle",
767
- ariaKey: "wrapAria",
768
- run: (block) => {
769
- if (block.getAttribute(WRAP_ATTR) === "on") block.removeAttribute(WRAP_ATTR);
770
- else block.setAttribute(WRAP_ATTR, "on");
771
- }
846
+ var WRAP_BUTTON = {
847
+ cls: TOOL_CLASS,
848
+ titleKey: "wrapTitle",
849
+ ariaKey: "wrapAria",
850
+ run: (block) => {
851
+ if (block.getAttribute(WRAP_ATTR) === "on") block.removeAttribute(WRAP_ATTR);
852
+ else block.setAttribute(WRAP_ATTR, "on");
772
853
  }
773
- ];
854
+ };
855
+ var DOWNLOAD_BUTTON = {
856
+ cls: DOWNLOAD_CLASS,
857
+ titleKey: "downloadCode",
858
+ ariaKey: "downloadCodeAria",
859
+ run: (block) => downloadCode(block)
860
+ };
861
+ var PLAIN_BUTTONS = [WRAP_BUTTON, DOWNLOAD_BUTTON];
774
862
  var MMD_BUTTONS = [
775
863
  { cls: ZOOM_OUT_CLASS, titleKey: "zoomOut", ariaKey: "zoomOutAria", run: (block) => zoomBy(block, 1 / ZOOM_STEP) },
776
864
  { cls: ZOOM_IN_CLASS, titleKey: "zoomIn", ariaKey: "zoomInAria", run: (block) => zoomBy(block, ZOOM_STEP) },
@@ -799,6 +887,12 @@ window.__ModuleLoader__.load({
799
887
  titleKey: "theme",
800
888
  ariaKey: "themeAria",
801
889
  run: (block) => cycleMermaidTheme(block)
890
+ },
891
+ {
892
+ cls: COPY_SOURCE_CLASS,
893
+ titleKey: "copySource",
894
+ ariaKey: "copySourceAria",
895
+ run: (block, button) => copyDiagramSource(block, button)
802
896
  }
803
897
  ];
804
898
  var FAILED_BUTTONS = [
@@ -832,7 +926,8 @@ window.__ModuleLoader__.load({
832
926
  var label = readBannerLabel(block);
833
927
  var lang = readLang(block);
834
928
  if (lang !== "" && isPlaceholderLabel(label)) paintLanguageLabel(block, lang);
835
- var specs = block.getAttribute(MMD_ATTR) === "failed" ? FAILED_BUTTONS : diagramKind(block) !== null ? MMD_BUTTONS : findNativeWrap(nativeButtons) !== null ? [] : PLAIN_BUTTONS;
929
+ var hasNativeWrap = findNativeWrap(nativeButtons) !== null;
930
+ var specs = block.getAttribute(MMD_ATTR) === "failed" ? FAILED_BUTTONS : diagramKind(block) !== null ? MMD_BUTTONS : hasNativeWrap ? [DOWNLOAD_BUTTON] : PLAIN_BUTTONS;
836
931
  for (const existing of action.querySelectorAll("." + BTN_CLASS)) {
837
932
  if (specs.some((spec) => existing.classList.contains(spec.cls))) continue;
838
933
  existing.remove();
@@ -850,7 +945,7 @@ window.__ModuleLoader__.load({
850
945
  created.title = themeText;
851
946
  created.setAttribute("aria-label", themeText);
852
947
  }
853
- created.addEventListener("click", () => spec.run(block));
948
+ created.addEventListener("click", () => spec.run(block, created));
854
949
  action.insertBefore(created, copy);
855
950
  injected.add(created);
856
951
  }
@@ -1195,7 +1290,11 @@ window.__ModuleLoader__.load({
1195
1290
  button.textContent = t("foldOpen");
1196
1291
  button.setAttribute("aria-expanded", "false");
1197
1292
  var sync = target.hasAttribute(TABLE_DONE_ATTR) ? () => {
1198
- button.style.left = target.scrollLeft + target.clientWidth / 2 + "px";
1293
+ if (target.getAttribute(FOLD_ATTR) === "on") {
1294
+ button.style.left = target.scrollLeft + target.clientWidth / 2 + "px";
1295
+ } else {
1296
+ button.style.removeProperty("left");
1297
+ }
1199
1298
  } : null;
1200
1299
  if (sync !== null) {
1201
1300
  target.addEventListener("scroll", sync, { passive: true });
@@ -1238,6 +1337,22 @@ window.__ModuleLoader__.load({
1238
1337
  (row) => row.map((value) => /[",\n]/.test(value) ? '"' + value.replace(/"/g, '""') + '"' : value).join(",")
1239
1338
  ).join("\n");
1240
1339
  }
1340
+ function tableToTsv(rows) {
1341
+ return rows.map((row) => row.map((value) => value.replace(/\t/g, " ")).join(" ")).join("\n");
1342
+ }
1343
+ function escapeHtml(text) {
1344
+ return text.replace(
1345
+ /[&<>"']/g,
1346
+ (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]
1347
+ );
1348
+ }
1349
+ function tableToHtml(rows) {
1350
+ if (rows.length === 0) return "";
1351
+ var cell = (tag, value) => "<" + tag + ">" + escapeHtml(value) + "</" + tag + ">";
1352
+ var header = "<thead><tr>" + rows[0].map((value) => cell("th", value)).join("") + "</tr></thead>";
1353
+ var body = rows.slice(1).map((row) => "<tr>" + row.map((value) => cell("td", value)).join("") + "</tr>");
1354
+ return "<table>" + header + (body.length === 0 ? "" : "<tbody>" + body.join("") + "</tbody>") + "</table>";
1355
+ }
1241
1356
  function tableToMarkdown(rows) {
1242
1357
  if (rows.length === 0) return "";
1243
1358
  var width = Math.max(...rows.map((row) => row.length));
@@ -1246,6 +1361,36 @@ window.__ModuleLoader__.load({
1246
1361
  for (var index = 1; index < rows.length; index += 1) output.push(line(rows[index]));
1247
1362
  return output.join("\n");
1248
1363
  }
1364
+ var CODE_EXTENSIONS = {
1365
+ bash: "sh",
1366
+ batch: "bat",
1367
+ c: "c",
1368
+ cpp: "cpp",
1369
+ csharp: "cs",
1370
+ css: "css",
1371
+ go: "go",
1372
+ html: "html",
1373
+ java: "java",
1374
+ javascript: "js",
1375
+ json: "json",
1376
+ jsx: "jsx",
1377
+ kotlin: "kt",
1378
+ markdown: "md",
1379
+ mermaid: "mmd",
1380
+ php: "php",
1381
+ python: "py",
1382
+ ruby: "rb",
1383
+ rust: "rs",
1384
+ shell: "sh",
1385
+ shellscript: "sh",
1386
+ sql: "sql",
1387
+ swift: "swift",
1388
+ tsx: "tsx",
1389
+ typescript: "ts",
1390
+ xml: "xml",
1391
+ yaml: "yml",
1392
+ yml: "yml"
1393
+ };
1249
1394
  async function copyText(text) {
1250
1395
  if (window.navigator.clipboard !== void 0 && typeof window.navigator.clipboard.writeText === "function") {
1251
1396
  try {
@@ -1269,6 +1414,55 @@ window.__ModuleLoader__.load({
1269
1414
  area.remove();
1270
1415
  return ok;
1271
1416
  }
1417
+ async function copyRichText(text, html) {
1418
+ if (window.navigator.clipboard !== void 0 && typeof window.navigator.clipboard.write === "function" && typeof window.ClipboardItem === "function") {
1419
+ try {
1420
+ await window.navigator.clipboard.write([
1421
+ new window.ClipboardItem({
1422
+ "text/plain": new Blob([text], { type: "text/plain" }),
1423
+ "text/html": new Blob([html], { type: "text/html" })
1424
+ })
1425
+ ]);
1426
+ return true;
1427
+ } catch {
1428
+ }
1429
+ }
1430
+ return copyText(text);
1431
+ }
1432
+ async function copyDiagramSource(block, button) {
1433
+ var pre = block.querySelector(CONTENT + " pre");
1434
+ if (pre === null) return;
1435
+ var ok = false;
1436
+ try {
1437
+ ok = await copyText(pre.textContent ?? "");
1438
+ } catch {
1439
+ ok = false;
1440
+ }
1441
+ if (button !== void 0) {
1442
+ var original = button.title;
1443
+ var label = ok ? t("copied") : t("copyFailed");
1444
+ button.title = label;
1445
+ button.setAttribute("aria-label", label);
1446
+ window.setTimeout(() => {
1447
+ button.title = original;
1448
+ button.setAttribute("aria-label", original);
1449
+ }, 1200);
1450
+ }
1451
+ }
1452
+ function downloadCode(block) {
1453
+ var pre = block.querySelector(CONTENT + " pre");
1454
+ if (pre === null) return;
1455
+ var lang = readLang(block);
1456
+ var ext = CODE_EXTENSIONS[lang] ?? "txt";
1457
+ try {
1458
+ downloadBlob(
1459
+ new Blob([pre.textContent ?? ""], { type: "text/plain;charset=utf-8" }),
1460
+ "code-snippet." + ext
1461
+ );
1462
+ } catch (error) {
1463
+ console.warn("[dsh-md-plus] \u4EE3\u7801\u4E0B\u8F7D\u5931\u8D25", error);
1464
+ }
1465
+ }
1272
1466
  function flashButton(button, text) {
1273
1467
  var original = button.getAttribute("data-md-plus-label") ?? button.textContent;
1274
1468
  button.setAttribute("data-md-plus-label", original);
@@ -1277,9 +1471,88 @@ window.__ModuleLoader__.load({
1277
1471
  button.textContent = original;
1278
1472
  }, 1200);
1279
1473
  }
1474
+ function clearTableFreeze(table) {
1475
+ for (const cell of table.querySelectorAll(
1476
+ "." + FREEZE_ROW_CLASS + ",." + FREEZE_COL_CLASS + ",." + FREEZE_CROSS_CLASS
1477
+ )) {
1478
+ cell.classList.remove(FREEZE_ROW_CLASS, FREEZE_COL_CLASS, FREEZE_CROSS_CLASS);
1479
+ cell.style.removeProperty("top");
1480
+ cell.style.removeProperty("left");
1481
+ }
1482
+ }
1483
+ function observeTableFreeze(table) {
1484
+ if (tableFreezeTargets.has(table)) return;
1485
+ tableFreezeTargets.add(table);
1486
+ if (typeof window.ResizeObserver !== "function") return;
1487
+ if (tableFreezeObserver === null) {
1488
+ tableFreezeObserver = new window.ResizeObserver((entries) => {
1489
+ for (const entry of entries) {
1490
+ if (!entry.target.isConnected) {
1491
+ cleanupTableFreezeTargets();
1492
+ continue;
1493
+ }
1494
+ applyTableFreeze(entry.target);
1495
+ }
1496
+ });
1497
+ }
1498
+ tableFreezeObserver.observe(table);
1499
+ }
1500
+ function applyTableFreeze(table) {
1501
+ var dataTable = table.querySelector("table");
1502
+ if (dataTable === null) return;
1503
+ var settings = readSettings();
1504
+ var freezeRows = Math.max(0, Math.min(5, Number(settings.freezeRows) || 0));
1505
+ var freezeCols = Math.max(0, Math.min(5, Number(settings.freezeCols) || 0));
1506
+ var width = dataTable.scrollWidth;
1507
+ var height = dataTable.scrollHeight;
1508
+ var hasFrozenCells = dataTable.querySelector("." + FREEZE_ROW_CLASS + ",." + FREEZE_COL_CLASS) !== null;
1509
+ var state = tableFreezeState.get(table);
1510
+ if (state !== void 0 && state.rows === freezeRows && state.cols === freezeCols && state.width === width && state.height === height && (freezeRows === 0 && freezeCols === 0 || hasFrozenCells)) {
1511
+ return;
1512
+ }
1513
+ clearTableFreeze(table);
1514
+ tableFreezeState.set(table, { rows: freezeRows, cols: freezeCols, width, height });
1515
+ if (freezeRows === 0 && freezeCols === 0) return;
1516
+ var rows = Array.from(dataTable.rows);
1517
+ var top = 0;
1518
+ for (var rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
1519
+ var row = rows[rowIndex];
1520
+ var cells = Array.from(row.cells);
1521
+ var frozenRow = rowIndex < freezeRows;
1522
+ if (frozenRow) {
1523
+ for (const cell2 of cells) cell2.classList.add(FREEZE_ROW_CLASS);
1524
+ }
1525
+ var left = 0;
1526
+ for (var cellIndex = 0; cellIndex < cells.length; cellIndex += 1) {
1527
+ var cell = cells[cellIndex];
1528
+ if (frozenRow) cell.style.top = top + "px";
1529
+ if (cellIndex < freezeCols) {
1530
+ cell.classList.add(FREEZE_COL_CLASS);
1531
+ if (frozenRow) cell.classList.add(FREEZE_CROSS_CLASS);
1532
+ cell.style.left = left + "px";
1533
+ left += cell.getBoundingClientRect().width;
1534
+ }
1535
+ }
1536
+ if (frozenRow) top += row.getBoundingClientRect().height;
1537
+ }
1538
+ observeTableFreeze(table);
1539
+ }
1540
+ function cleanupTableFreezeTargets() {
1541
+ for (const table of tableFreezeTargets) {
1542
+ if (table.isConnected) continue;
1543
+ if (tableFreezeObserver !== null) tableFreezeObserver.unobserve(table);
1544
+ tableFreezeTargets.delete(table);
1545
+ tableFreezeState.delete(table);
1546
+ }
1547
+ }
1548
+ function applyTableFreezeSettings() {
1549
+ tableFreezeState = /* @__PURE__ */ new WeakMap();
1550
+ for (const table of document.querySelectorAll(TABLE)) applyTableFreeze(table);
1551
+ }
1280
1552
  function decorateTable(table) {
1281
1553
  var dataTable = table.querySelector("table");
1282
1554
  if (dataTable === null) return;
1555
+ applyTableFreeze(table);
1283
1556
  var first = dataTable.firstElementChild;
1284
1557
  if (first !== null && first.classList.contains(TABLE_TOOLS_CLASS)) return;
1285
1558
  table.setAttribute(TABLE_DONE_ATTR, "1");
@@ -1287,9 +1560,11 @@ window.__ModuleLoader__.load({
1287
1560
  tools.className = TABLE_TOOLS_CLASS;
1288
1561
  var group = document.createElement("span");
1289
1562
  group.className = TABLE_TOOLS_CLASS + "-group";
1290
- for (const [kind, label, titleKey, convert] of [
1291
- ["csv", t("tableCsv"), "copyCsvTitle", tableToCsv],
1292
- ["md", t("tableMd"), "copyMdTitle", tableToMarkdown]
1563
+ for (const [kind, label, titleKey, convert, rich] of [
1564
+ ["csv", t("tableCsv"), "copyCsvTitle", tableToCsv, false],
1565
+ ["tsv", t("tableTsv"), "copyTsvTitle", tableToTsv, false],
1566
+ ["md", t("tableMd"), "copyMdTitle", tableToMarkdown, false],
1567
+ ["html", t("tableHtml"), "copyHtmlTitle", tableToHtml, true]
1293
1568
  ]) {
1294
1569
  const button = document.createElement("button");
1295
1570
  button.type = "button";
@@ -1299,7 +1574,8 @@ window.__ModuleLoader__.load({
1299
1574
  button.addEventListener("click", async () => {
1300
1575
  var ok = false;
1301
1576
  try {
1302
- ok = await copyText(convert(tableRows(dataTable)));
1577
+ var rows = tableRows(dataTable);
1578
+ ok = rich ? await copyRichText(tableToTsv(rows), convert(rows)) : await copyText(convert(rows));
1303
1579
  } catch {
1304
1580
  ok = false;
1305
1581
  }
@@ -1460,6 +1736,34 @@ window.__ModuleLoader__.load({
1460
1736
  }
1461
1737
  scanFolds();
1462
1738
  }
1739
+ function setFoldExpanded(target, expanded) {
1740
+ var state = target.getAttribute(FOLD_ATTR);
1741
+ if (state !== "on" && state !== "off") return;
1742
+ target.setAttribute(FOLD_ATTR, expanded ? "off" : "on");
1743
+ if (target.hasAttribute(TABLE_DONE_ATTR)) target.scrollTop = 0;
1744
+ var button = target.querySelector("." + FOLD_BTN_CLASS);
1745
+ if (button !== null) {
1746
+ button.textContent = expanded ? t("foldClose") : t("foldOpen");
1747
+ button.setAttribute("aria-expanded", expanded ? "true" : "false");
1748
+ }
1749
+ foldSyncs.get(target)?.();
1750
+ }
1751
+ function setAllFolds(expanded) {
1752
+ for (const target of document.querySelectorAll("[" + FOLD_ATTR + "]")) {
1753
+ setFoldExpanded(target, expanded);
1754
+ }
1755
+ }
1756
+ function exportSettings() {
1757
+ try {
1758
+ var payload = { settings: readSettings(), mermaidTheme: getMermaidTheme() };
1759
+ downloadBlob(
1760
+ new Blob([JSON.stringify(payload, null, 2)], { type: "application/json;charset=utf-8" }),
1761
+ "dsh-md-plus-settings.json"
1762
+ );
1763
+ } catch (error) {
1764
+ console.warn("[dsh-md-plus] \u8BBE\u7F6E\u5BFC\u51FA\u5931\u8D25", error);
1765
+ }
1766
+ }
1463
1767
  function openSettings() {
1464
1768
  if (document.querySelector("." + SETTINGS_MODAL_CLASS) !== null) return;
1465
1769
  var current = readSettings();
@@ -1484,11 +1788,9 @@ window.__ModuleLoader__.load({
1484
1788
  themeSelect.value = getMermaidTheme();
1485
1789
  panel.appendChild(settingRow(t("settingsTheme"), themeSelect));
1486
1790
  var fontInput = numberInput(current.codeFontSize, 10, 20);
1487
- panel.appendChild(settingRow(t("settingsFontSize"), fontInput));
1488
- var fontHint = document.createElement("p");
1489
- fontHint.className = SETTINGS_MODAL_CLASS + "-hint";
1490
- fontHint.textContent = t("settingsFontSizeHint");
1491
- panel.appendChild(fontHint);
1791
+ panel.appendChild(
1792
+ settingRow(t("settingsFontSize"), fontInput, t("settingsFontSizeHint"))
1793
+ );
1492
1794
  var viewSelect = document.createElement("select");
1493
1795
  for (const [value, label] of [
1494
1796
  ["diagram", t("settingsViewDiagram")],
@@ -1502,7 +1804,7 @@ window.__ModuleLoader__.load({
1502
1804
  viewSelect.value = current.diagramView;
1503
1805
  panel.appendChild(settingRow(t("settingsView"), viewSelect));
1504
1806
  var foldHint = document.createElement("p");
1505
- foldHint.className = SETTINGS_MODAL_CLASS + "-hint";
1807
+ foldHint.className = SETTINGS_MODAL_CLASS + "-section";
1506
1808
  foldHint.textContent = t("settingsFold") + " \xB7 " + t("settingsFoldHint");
1507
1809
  panel.appendChild(foldHint);
1508
1810
  var foldCode = numberInput(current.foldCode, 0, 4e3);
@@ -1511,6 +1813,56 @@ window.__ModuleLoader__.load({
1511
1813
  panel.appendChild(settingRow(t("settingsFoldCode"), foldCode, t("settingsFoldCodeHint")));
1512
1814
  panel.appendChild(settingRow(t("settingsFoldTable"), foldTable, t("settingsFoldTableHint")));
1513
1815
  panel.appendChild(settingRow(t("settingsFoldDiagram"), foldDiagram, t("settingsFoldDiagramHint")));
1816
+ var freezeRowsInput = numberInput(current.freezeRows, 0, 5);
1817
+ var freezeColsInput = numberInput(current.freezeCols, 0, 5);
1818
+ panel.appendChild(
1819
+ settingRow(t("settingsFreezeRows"), freezeRowsInput, t("settingsFreezeRowsHint"))
1820
+ );
1821
+ panel.appendChild(
1822
+ settingRow(t("settingsFreezeCols"), freezeColsInput, t("settingsFreezeColsHint"))
1823
+ );
1824
+ var foldActions = document.createElement("div");
1825
+ foldActions.className = SETTINGS_MODAL_CLASS + "-fold-actions";
1826
+ foldActions.setAttribute("role", "group");
1827
+ foldActions.setAttribute("aria-label", t("settingsFoldGroup"));
1828
+ var foldLabel = document.createElement("span");
1829
+ foldLabel.className = SETTINGS_MODAL_CLASS + "-utility-label";
1830
+ foldLabel.textContent = t("settingsFoldGroup");
1831
+ var foldSegment = document.createElement("span");
1832
+ foldSegment.className = SETTINGS_MODAL_CLASS + "-segment";
1833
+ var expandAllButton = document.createElement("button");
1834
+ expandAllButton.type = "button";
1835
+ expandAllButton.textContent = t("foldExpandAll");
1836
+ var collapseAllButton = document.createElement("button");
1837
+ collapseAllButton.type = "button";
1838
+ collapseAllButton.textContent = t("foldCollapseAll");
1839
+ foldSegment.appendChild(expandAllButton);
1840
+ foldSegment.appendChild(collapseAllButton);
1841
+ foldActions.appendChild(foldLabel);
1842
+ foldActions.appendChild(foldSegment);
1843
+ var transferActions = document.createElement("div");
1844
+ transferActions.className = SETTINGS_MODAL_CLASS + "-transfer-actions";
1845
+ transferActions.setAttribute("role", "group");
1846
+ transferActions.setAttribute("aria-label", t("settingsFileGroup"));
1847
+ var transferLabel = document.createElement("span");
1848
+ transferLabel.className = SETTINGS_MODAL_CLASS + "-utility-label";
1849
+ transferLabel.textContent = t("settingsFileGroup");
1850
+ var transferSegment = document.createElement("span");
1851
+ transferSegment.className = SETTINGS_MODAL_CLASS + "-segment";
1852
+ var exportButton = document.createElement("button");
1853
+ exportButton.type = "button";
1854
+ exportButton.textContent = t("settingsExport");
1855
+ var importButton = document.createElement("button");
1856
+ importButton.type = "button";
1857
+ importButton.textContent = t("settingsImport");
1858
+ var fileInput = document.createElement("input");
1859
+ fileInput.type = "file";
1860
+ fileInput.accept = ".json,application/json";
1861
+ fileInput.style.display = "none";
1862
+ transferSegment.appendChild(exportButton);
1863
+ transferSegment.appendChild(importButton);
1864
+ transferActions.appendChild(transferLabel);
1865
+ transferActions.appendChild(transferSegment);
1514
1866
  var actions = document.createElement("div");
1515
1867
  actions.className = SETTINGS_MODAL_CLASS + "-actions";
1516
1868
  var resetButton = document.createElement("button");
@@ -1526,7 +1878,16 @@ window.__ModuleLoader__.load({
1526
1878
  actions.appendChild(resetButton);
1527
1879
  actions.appendChild(closeButton);
1528
1880
  actions.appendChild(saveButton);
1529
- panel.appendChild(actions);
1881
+ var utilities = document.createElement("div");
1882
+ utilities.className = SETTINGS_MODAL_CLASS + "-utilities";
1883
+ utilities.appendChild(foldActions);
1884
+ utilities.appendChild(transferActions);
1885
+ var footer = document.createElement("div");
1886
+ footer.className = SETTINGS_MODAL_CLASS + "-footer";
1887
+ footer.appendChild(utilities);
1888
+ footer.appendChild(actions);
1889
+ panel.appendChild(footer);
1890
+ panel.appendChild(fileInput);
1530
1891
  overlay.appendChild(panel);
1531
1892
  var previousFocus = document.activeElement;
1532
1893
  var releaseFocus = null;
@@ -1555,6 +1916,48 @@ window.__ModuleLoader__.load({
1555
1916
  if (!Number.isFinite(value) || value <= 0) return 0;
1556
1917
  return Math.min(20, Math.max(10, Math.round(value)));
1557
1918
  }
1919
+ function importSettingsFile(file) {
1920
+ file.text().then((text) => {
1921
+ var parsed = JSON.parse(text);
1922
+ var source = parsed !== null && typeof parsed === "object" && parsed.settings !== null && typeof parsed.settings === "object" ? parsed.settings : parsed;
1923
+ if (source === null || typeof source !== "object") throw new Error("\u8BBE\u7F6E\u683C\u5F0F\u65E0\u6548");
1924
+ var font = Number(source.codeFontSize);
1925
+ if (Number.isFinite(font)) {
1926
+ fontInput.value = font > 0 ? String(Math.min(20, Math.max(10, Math.round(font)))) : "";
1927
+ }
1928
+ for (const [key, input, max] of [
1929
+ ["foldCode", foldCode, 4e3],
1930
+ ["foldTable", foldTable, 4e3],
1931
+ ["foldDiagram", foldDiagram, 4e3],
1932
+ ["freezeRows", freezeRowsInput, 5],
1933
+ ["freezeCols", freezeColsInput, 5]
1934
+ ]) {
1935
+ var value = Number(source[key]);
1936
+ if (Number.isFinite(value) && value >= 0) {
1937
+ input.value = String(Math.min(max, Math.round(value)));
1938
+ }
1939
+ }
1940
+ if (source.diagramView === "code" || source.diagramView === "diagram") {
1941
+ viewSelect.value = source.diagramView;
1942
+ }
1943
+ var theme = parsed !== null && typeof parsed === "object" ? parsed.mermaidTheme : null;
1944
+ if (typeof theme === "string" && THEME_ORDER.indexOf(theme) !== -1) {
1945
+ themeSelect.value = theme;
1946
+ }
1947
+ saveButton.click();
1948
+ }).catch(() => {
1949
+ flashButton(importButton, t("settingsImportFailed"));
1950
+ });
1951
+ }
1952
+ exportButton.addEventListener("click", exportSettings);
1953
+ importButton.addEventListener("click", () => fileInput.click());
1954
+ fileInput.addEventListener("change", () => {
1955
+ var file = fileInput.files?.[0];
1956
+ if (file !== null && file !== void 0) importSettingsFile(file);
1957
+ fileInput.value = "";
1958
+ });
1959
+ expandAllButton.addEventListener("click", () => setAllFolds(true));
1960
+ collapseAllButton.addEventListener("click", () => setAllFolds(false));
1558
1961
  resetButton.addEventListener("click", () => {
1559
1962
  themeSelect.value = "blue";
1560
1963
  fontInput.value = "";
@@ -1562,6 +1965,8 @@ window.__ModuleLoader__.load({
1562
1965
  foldCode.value = String(DEFAULT_SETTINGS.foldCode);
1563
1966
  foldTable.value = String(DEFAULT_SETTINGS.foldTable);
1564
1967
  foldDiagram.value = String(DEFAULT_SETTINGS.foldDiagram);
1968
+ freezeRowsInput.value = String(DEFAULT_SETTINGS.freezeRows);
1969
+ freezeColsInput.value = String(DEFAULT_SETTINGS.freezeCols);
1565
1970
  });
1566
1971
  saveButton.addEventListener("click", () => {
1567
1972
  var previous = readSettings();
@@ -1572,7 +1977,9 @@ window.__ModuleLoader__.load({
1572
1977
  diagramView: viewSelect.value === "code" ? "code" : "diagram",
1573
1978
  foldCode: readNumber(foldCode, DEFAULT_SETTINGS.foldCode),
1574
1979
  foldTable: readNumber(foldTable, DEFAULT_SETTINGS.foldTable),
1575
- foldDiagram: readNumber(foldDiagram, DEFAULT_SETTINGS.foldDiagram)
1980
+ foldDiagram: readNumber(foldDiagram, DEFAULT_SETTINGS.foldDiagram),
1981
+ freezeRows: readNumber(freezeRowsInput, DEFAULT_SETTINGS.freezeRows, 5),
1982
+ freezeCols: readNumber(freezeColsInput, DEFAULT_SETTINGS.freezeCols, 5)
1576
1983
  };
1577
1984
  saveSettings(next);
1578
1985
  memoryTheme = nextTheme;
@@ -1582,6 +1989,7 @@ window.__ModuleLoader__.load({
1582
1989
  } catch {
1583
1990
  }
1584
1991
  applyCodeFontSize();
1992
+ applyTableFreezeSettings();
1585
1993
  refreshThemeButtons();
1586
1994
  if (previousTheme !== nextTheme) redrawDiagrams();
1587
1995
  if (previous.codeFontSize !== next.codeFontSize || previous.foldCode !== next.foldCode || previous.foldTable !== next.foldTable || previous.foldDiagram !== next.foldDiagram) {
@@ -1832,6 +2240,7 @@ window.__ModuleLoader__.load({
1832
2240
  themeCache = null;
1833
2241
  injectStyle();
1834
2242
  applyCodeFontSize();
2243
+ applyTableFreezeSettings();
1835
2244
  ensureSettingsButton();
1836
2245
  var root = document.body ?? document.documentElement;
1837
2246
  document.addEventListener("click", onDocumentClick, true);
@@ -1853,6 +2262,14 @@ window.__ModuleLoader__.load({
1853
2262
  pending.clear();
1854
2263
  rescanQueued = false;
1855
2264
  for (const target of [...foldTargets]) resetFold(target);
2265
+ for (const table of [...tableFreezeTargets]) clearTableFreeze(table);
2266
+ tableFreezeTargets.clear();
2267
+ if (tableFreezeObserver !== null) {
2268
+ tableFreezeObserver.disconnect();
2269
+ tableFreezeObserver = null;
2270
+ }
2271
+ tableFreezeState = /* @__PURE__ */ new WeakMap();
2272
+ for (const table of document.querySelectorAll(TABLE)) clearTableFreeze(table);
1856
2273
  observer.disconnect();
1857
2274
  document.removeEventListener("click", onDocumentClick, true);
1858
2275
  for (const node of injected) node.remove();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-md-plus",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "Markdown display enhancements for DeepSeek Harness Web: code block restyle, syntax highlighting for languages DSH does not ship, and built-in mermaid diagrams.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",