dsh-long-plugins 2.4.5 → 2.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/client/client.js +0 -4
- package/dsh.plugin.json +1 -1
- package/lib/index.js +6 -66
- package/package.json +2 -3
- package/skill/dsh-upgrade/SKILL.md +23 -0
- package/lib/md2docx.py +0 -210
package/client/client.js
CHANGED
|
@@ -3445,7 +3445,6 @@ window.__ModuleLoader__.load({
|
|
|
3445
3445
|
// 「dsh-long」设置区:各模块开关(独立、移动端干净),避免把开关塞进 RA-Span
|
|
3446
3446
|
const DSH_LONG_MODULES = [
|
|
3447
3447
|
['glass', 'RA-Span'],
|
|
3448
|
-
['turnRuler', '会话导航(轮次刻尺)'],
|
|
3449
3448
|
['uploadAttach', '附件上传(回形针)'],
|
|
3450
3449
|
['uploadDragDrop', '附件拖放上传'],
|
|
3451
3450
|
['uploadPaste', '附件粘贴上传'],
|
|
@@ -3935,7 +3934,6 @@ window.__ModuleLoader__.load({
|
|
|
3935
3934
|
try {
|
|
3936
3935
|
const tk = dshDark() ? 'dark' : 'light'
|
|
3937
3936
|
root.setAttribute('data-dsh-theme', tk)
|
|
3938
|
-
root.setAttribute('data-dsh-turnruler', ((c.modules && c.modules.turnRuler) !== false) ? 'on' : 'off')
|
|
3939
3937
|
window.__dshLongModules = c.modules || {}
|
|
3940
3938
|
try { localStorage.setItem('dsh-long:modules', JSON.stringify(window.__dshLongModules)) } catch (e) {}
|
|
3941
3939
|
const bt = bgTint(tk)
|
|
@@ -4036,7 +4034,6 @@ window.__ModuleLoader__.load({
|
|
|
4036
4034
|
...tokenUsagePlugin.inject,
|
|
4037
4035
|
...mobilePlugin.inject,
|
|
4038
4036
|
...workspaceFilesPlugin.inject,
|
|
4039
|
-
...turnRulerPlugin.inject,
|
|
4040
4037
|
...glassPlugin.inject,
|
|
4041
4038
|
]))
|
|
4042
4039
|
|
|
@@ -4074,7 +4071,6 @@ window.__ModuleLoader__.load({
|
|
|
4074
4071
|
safeApply('token-usage', (c) => tokenUsagePlugin.apply(c)) // 余额 chip 开关在其内部; MOBILE_CSS 始终注入
|
|
4075
4072
|
if (modEnabled('mobile')) safeApply('mobile-hamburger', (c) => mobilePlugin.apply(c))
|
|
4076
4073
|
if (modEnabled('workspace')) safeApply('workspace-files', (c) => workspaceFilesPlugin.apply(c))
|
|
4077
|
-
if (modEnabled('turnRuler')) safeApply('turn-ruler', (c) => turnRulerPlugin.apply(c))
|
|
4078
4074
|
if (modEnabled('glass')) safeApply('glass', (c) => glassPlugin.apply(c))
|
|
4079
4075
|
}
|
|
4080
4076
|
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-long-plugins",
|
|
3
3
|
"description": "Merged DSH web plugins: upload manager (drag-and-drop attach any file to the message), workspace output files, skill docs, account balance, Markdown-to-Word (md2docx), and polished file preview (Word & PowerPoint real preview with zoom/pan, rendered Markdown), plus an auto-repair install for DSH core patches (reverse-proxy WebSocket heartbeat, upgrade relink). | 一个插件整合 DSH Web 的常用增强:上传管理(拖放上传:拖任意文件到会话框直接附加)、工作区「输出文件」面板、技能文档浏览、账户余额显示、Markdown 转 Word(md2docx)、Word/PowerPoint 真实预览(可缩放、翻页);并自带修复安装,自动重连 DSH 核心补丁(反代 WebSocket 心跳、升级重连)。",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.6",
|
|
5
5
|
"entry": {
|
|
6
6
|
"name": "dsh-long-plugins",
|
|
7
7
|
"inject": [
|
package/lib/index.js
CHANGED
|
@@ -1134,6 +1134,10 @@ export function createHandlers(options = {}) {
|
|
|
1134
1134
|
// Markdown → 直接渲染成 HTML(真实效果),失败回退源码文本。
|
|
1135
1135
|
editable = true; rawText = buffer.toString("utf8");
|
|
1136
1136
|
body = `<article class="md">${markdownToHtml(rawText)}</article>`;
|
|
1137
|
+
} else if (/\.(html?|xhtml)$/i.test(name)) {
|
|
1138
|
+
// HTML/HTM → 直接渲染成实际效果(iframe + srcdoc, 保留其样式/脚本)
|
|
1139
|
+
editable = true; rawText = buffer.toString("utf8");
|
|
1140
|
+
body = `<iframe class="html" srcDoc="${escapeHtml(rawText)}"></iframe>`;
|
|
1137
1141
|
} else if (/\.(json|ya?ml|py|js|mjs|cjs|ts|sh|css|html?|xml|csv|ini|conf|env|toml|sql|rs|go|c|h|cpp|java|kt|swift|rb|php|vue|jsx|tsx)$/i.test(name)) {
|
|
1138
1142
|
editable = true; rawText = buffer.toString("utf8");
|
|
1139
1143
|
body = `<pre class="text">${escapeHtml(rawText)}</pre>`;
|
|
@@ -2553,6 +2557,7 @@ function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "", e
|
|
|
2553
2557
|
body.maximized .md { max-width:none; }
|
|
2554
2558
|
.image { max-width:100%; border-radius:8px; }
|
|
2555
2559
|
.pdf { display:block; width:100%; height:86vh; border:1px solid var(--lp-border); border-radius:8px; background:#fff; }
|
|
2560
|
+
.html { display:block; width:100%; height:86vh; border:1px solid var(--lp-border); border-radius:8px; background:#fff; }
|
|
2556
2561
|
.office { background:#fff; color:#111; border-radius:8px; padding:16px; overflow:auto; }
|
|
2557
2562
|
.unsupported { color:#f59e0b; text-align:center; padding:40px 0; }
|
|
2558
2563
|
/* 放大模式:顶栏常驻(固定悬浮),内容占满视口、PDF 全高 */
|
|
@@ -2560,6 +2565,7 @@ function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "", e
|
|
|
2560
2565
|
body.maximized .content { max-width:none; padding:0; margin:0; height:100vh; padding-top:53px; }
|
|
2561
2566
|
body.maximized .md { padding:24px 32px 48px; max-width:820px; margin:0 auto; }
|
|
2562
2567
|
body.maximized .pdf { height:calc(100vh - 53px); border:none; border-radius:0; }
|
|
2568
|
+
body.maximized .html { height:calc(100vh - 53px); border:none; border-radius:0; }
|
|
2563
2569
|
body.maximized .text { height:calc(100vh - 53px); border:none; border-radius:0; overflow:auto; }
|
|
2564
2570
|
body.maximized .office { height:calc(100vh - 53px); overflow:auto; }
|
|
2565
2571
|
body.maximized .image { max-width:100vw; max-height:100vh; object-fit:contain; }
|
|
@@ -2644,72 +2650,6 @@ export async function apply(ctx, config = {}) {
|
|
|
2644
2650
|
|
|
2645
2651
|
const handlers = createHandlers({ trustedHosts, onError, excludedWorkspaceNames: config.excludedWorkspaceNames });
|
|
2646
2652
|
|
|
2647
|
-
// ---- md2docx 工具: Markdown → Word (.docx, 带页码) ----
|
|
2648
|
-
// 调用 md2docx.py (pandoc-free, python-docx + 页脚 PAGE 字段)。
|
|
2649
|
-
// 默认脚本随插件包分发(lib/md2docx.py),换机器也可靠;可用 config.md2docxScript 覆盖。
|
|
2650
|
-
const PACKAGE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
2651
|
-
const MD2DOCX_SCRIPT = resolve(config.md2docxScript ?? join(PACKAGE_DIR, "md2docx.py"));
|
|
2652
|
-
const runScript = (script, args) => new Promise((resolvePromise) => {
|
|
2653
|
-
const child = spawn("python3", [script, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
2654
|
-
let stdout = "", stderr = "";
|
|
2655
|
-
child.stdout.on("data", (d) => { stdout += d.toString(); });
|
|
2656
|
-
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
2657
|
-
child.on("error", (error) => resolvePromise({ ok: false, error: String(error), stdout, stderr }));
|
|
2658
|
-
child.on("close", (code) => resolvePromise({ ok: code === 0, code, stdout, stderr }));
|
|
2659
|
-
});
|
|
2660
|
-
ctx.tools.register(defineTool({
|
|
2661
|
-
name: "md2docx",
|
|
2662
|
-
description: "Convert a Markdown file to a styled Word (.docx) document with a page-number footer. Ships a bundled python-docx script (lib/md2docx.py) that renders headings, tables, bold/italic, and lists; the docx includes a footer '第 N 页' field that updates when opened in Word or exported to PDF. Requires python3 and python-docx installed on the host. Override the script path via config.md2docxScript if needed.",
|
|
2663
|
-
parameters: {
|
|
2664
|
-
input: {
|
|
2665
|
-
type: "string",
|
|
2666
|
-
required: true,
|
|
2667
|
-
description: "Absolute path to the input .md file."
|
|
2668
|
-
},
|
|
2669
|
-
output: {
|
|
2670
|
-
type: "string",
|
|
2671
|
-
description: "Optional absolute path for the output .docx. Defaults to the input path with a .docx extension."
|
|
2672
|
-
}
|
|
2673
|
-
},
|
|
2674
|
-
output: {
|
|
2675
|
-
schema: {
|
|
2676
|
-
type: "object",
|
|
2677
|
-
additionalProperties: false,
|
|
2678
|
-
properties: {
|
|
2679
|
-
ok: { type: "boolean", required: true },
|
|
2680
|
-
docxPath: { type: "string" },
|
|
2681
|
-
error: { type: "string" }
|
|
2682
|
-
}
|
|
2683
|
-
},
|
|
2684
|
-
render: (_args, value) => [{
|
|
2685
|
-
type: "text",
|
|
2686
|
-
text: value && value.ok === true
|
|
2687
|
-
? `已生成 Word 文档:${value.docxPath}\n(含页码页脚,Word/另存 PDF 时自动更新)`
|
|
2688
|
-
: `md2docx 失败:${value?.error ?? "未知错误"}`
|
|
2689
|
-
}]
|
|
2690
|
-
},
|
|
2691
|
-
// 声明交付物:DSH 从 presentCall 的 locations 识别本工具产出的文件,
|
|
2692
|
-
// 从而把 docx 渲染成可点击的交付物卡片(消息里的文件引用也能打开)。
|
|
2693
|
-
presentCall: (args) => {
|
|
2694
|
-
const inPath = resolve(String(args.input ?? ""));
|
|
2695
|
-
const outPath = args.output ? resolve(String(args.output)) : inPath.replace(/\.md$/i, ".docx");
|
|
2696
|
-
return {
|
|
2697
|
-
card: "generic",
|
|
2698
|
-
title: "md2docx",
|
|
2699
|
-
kind: "edit",
|
|
2700
|
-
locations: [{ path: outPath }]
|
|
2701
|
-
};
|
|
2702
|
-
},
|
|
2703
|
-
async execute(args) {
|
|
2704
|
-
const inPath = resolve(String(args.input ?? ""));
|
|
2705
|
-
const outPath = args.output ? resolve(String(args.output)) : inPath.replace(/\.md$/i, ".docx");
|
|
2706
|
-
const result = await runScript(MD2DOCX_SCRIPT, [inPath, outPath]);
|
|
2707
|
-
if (!result.ok) {
|
|
2708
|
-
return { ok: false, error: (result.stderr || result.stdout || String(result.error)).trim() || `md2docx failed (exit ${result.code})` };
|
|
2709
|
-
}
|
|
2710
|
-
return { ok: true, docxPath: outPath };
|
|
2711
|
-
}
|
|
2712
|
-
}));
|
|
2713
2653
|
|
|
2714
2654
|
await sweepUploadTemps(handlers.root);
|
|
2715
2655
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-long-plugins",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Merged DSH web plugins: upload manager (drag-and-drop attach any file to the message), workspace output files, skill docs, account balance,
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "Merged DSH web plugins: upload manager (drag-and-drop attach any file to the message), workspace output files, skill docs, account balance, and polished file preview (Word & PowerPoint real preview with zoom/pan, rendered Markdown), plus an auto-repair install for DSH core patches (reverse-proxy WebSocket heartbeat, upgrade relink). | 一个插件整合 DSH Web 的常用增强:上传管理(拖放上传:拖任意文件到会话框直接附加)、工作区「输出文件」面板、技能文档浏览、账户余额显示、Word/PowerPoint 真实预览(可缩放、翻页);并自带修复安装,自动重连 DSH 核心补丁(反代 WebSocket 心跳、升级重连)。Office 读取/生成已独立到 dsh-office-reader。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "lib/index.js",
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"lib",
|
|
15
|
-
"lib/md2docx.py",
|
|
16
15
|
"client",
|
|
17
16
|
"!client/*.bak",
|
|
18
17
|
"cordis.patch.yml",
|
|
@@ -91,6 +91,29 @@ fi
|
|
|
91
91
|
9. 设置面板「技能管理」「上传文件」「输出文件」的 🔍 搜索框:点击后弹出框紧贴按钮下方、滚动不脱离。
|
|
92
92
|
10. **「技能管理」区(全局技能 / 工作区技能两 tab)**:全局技能列 `<DSH_HOME>/skills`;工作区技能扫描工作区根下各子目录的 `.dsh/skills`(无 `.dsh/skills` 则该工作区组为空)。若该区不出现/工作区技能为空,可能 DSH `settings.section` 槽或工作区结构变化——复核。
|
|
93
93
|
|
|
94
|
+
## 插件依赖 DSH 内部接口(升级脆弱清单)⚠️
|
|
95
|
+
dsh-long-plugins 大量依赖 DSH 内部接口/结构,DSH 升级改动后这些会**静默失效**(不注册/不显示/报错)。升级后逐项复核:
|
|
96
|
+
|
|
97
|
+
| 依赖 | 用途 | 升级失效表现 |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `settings.section` 槽 | 上传文件/输出文件/技能管理/dsh-long/RA-Span 设置区 | 对应设置区不出现 |
|
|
100
|
+
| `conversation.composer.dock` 槽 | 余额 chip | 余额不显示 |
|
|
101
|
+
| `conversation.input.dock` 槽 | 待发送文件栏、拖放/粘贴上传 | 待发送栏/拖放失效 |
|
|
102
|
+
| `conversation.input.left` 槽 | 附件上传(回形针) | 回形针消失 |
|
|
103
|
+
| `conversation.session.header.actions` 槽 | 会话历史/工作区按钮 | 按钮消失 |
|
|
104
|
+
| `shell.overlay` 槽 | 文件浏览器浮层 | 文件浮层打不开 |
|
|
105
|
+
| `data-slot-conversation="..."` 属性 | 余额 chip / dock 样式定位 | 对应样式失效 |
|
|
106
|
+
| `ctx.webServer.register` | 所有路由(上传/工作区/技能/预览/glass/补丁状态) | 路由 404/无响应 |
|
|
107
|
+
| `ctx.inputTriggers.registerSource` | 文件引用 codec(`@`) | 附件引用失效 |
|
|
108
|
+
| `ctx.locale.register` | 多语言 | 文案不翻译 |
|
|
109
|
+
| `window.__ModuleLoader__.load` | 插件客户端加载 | 插件整个不加载 |
|
|
110
|
+
| `useSessions` / 会话 store | 工作区按钮拿 cwd / 当前工作区 | 工作区按钮/当前工作区失效 |
|
|
111
|
+
| 混淆类名 `wSkVaW_/uV2eYG_/o3BgMG_/CY-8Ka_/QWLzLg_/M8wy4a_/lXshSW_/7yHdaG_/VOzbGW_/p-xYUq_/osXY9a_` | RA-Span 玻璃观感/排版 | 观感/配色/排版错(见"RA-Span 升级后需重新校准") |
|
|
112
|
+
| DSH 设置面板 DOM(`VOzbGW` 面板) | 各设置区在其内渲染 | 设置区排版乱 |
|
|
113
|
+
| DSH 核心 `dsh-client-connection` | 心跳补丁(补丁3) | 补丁被覆盖→心跳失效(需重打) |
|
|
114
|
+
|
|
115
|
+
> 处置:某项失效 → 让用户右键抓新元素 class/槽名/接口,更新插件对应选择器/槽名/调用;或集成 DSH 新 API。**稳定项**:插件自己的路由表、`data-slot` 状态栏(部分)、注入 `<head>` 的 CSS(部分)。
|
|
116
|
+
|
|
94
117
|
## 硬性安全边界(必须遵守)
|
|
95
118
|
- **不主动 push / 打 tag / 发 Release**;需要发布版本时停下用 ask_user_question 等确认。
|
|
96
119
|
- 升级会重启 DSH 服务,可能中断当前会话——先向用户说明再执行。
|
package/lib/md2docx.py
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""Generic Markdown -> styled .docx converter with a page-number footer.
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
python3 md2docx.py <input.md> [output.docx]
|
|
7
|
-
|
|
8
|
-
If output.docx is omitted, it defaults to <input-name>.docx next to the input.
|
|
9
|
-
|
|
10
|
-
Renders headings (h1-h3), bold/italic inline, tables (pipe syntax),
|
|
11
|
-
ordered/unordered lists, blockquotes, and horizontal rules. Adds a centered
|
|
12
|
-
footer with an auto-updating PAGE field (updates when opened in Word or
|
|
13
|
-
exported to PDF).
|
|
14
|
-
|
|
15
|
-
Requires: python3 + python-docx (`pip install python-docx`).
|
|
16
|
-
"""
|
|
17
|
-
import re
|
|
18
|
-
import sys
|
|
19
|
-
import os
|
|
20
|
-
|
|
21
|
-
from docx import Document
|
|
22
|
-
from docx.shared import Pt, RGBColor
|
|
23
|
-
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
24
|
-
from docx.enum.table import WD_TABLE_ALIGNMENT
|
|
25
|
-
from docx.oxml.ns import qn
|
|
26
|
-
from docx.oxml import OxmlElement
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def die(msg):
|
|
30
|
-
print(f"md2docx: {msg}", file=sys.stderr)
|
|
31
|
-
sys.exit(1)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if len(sys.argv) < 2:
|
|
35
|
-
die("usage: md2docx.py <input.md> [output.docx]")
|
|
36
|
-
|
|
37
|
-
SRC = os.path.abspath(sys.argv[1])
|
|
38
|
-
if not os.path.isfile(SRC):
|
|
39
|
-
die(f"input file not found: {SRC}")
|
|
40
|
-
OUT = (
|
|
41
|
-
os.path.abspath(sys.argv[2])
|
|
42
|
-
if len(sys.argv) > 2
|
|
43
|
-
else os.path.splitext(SRC)[0] + ".docx"
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
with open(SRC, encoding="utf-8") as f:
|
|
47
|
-
lines = f.read().splitlines()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def add_page_number(paragraph):
|
|
51
|
-
"""Insert an auto-updating '第 N 页' page field into a footer paragraph."""
|
|
52
|
-
run = paragraph.add_run("第 ")
|
|
53
|
-
set_east_asia(run)
|
|
54
|
-
fld = OxmlElement("w:fldChar")
|
|
55
|
-
fld.set(qn("w:fldCharType"), "begin")
|
|
56
|
-
instr = OxmlElement("w:instrText")
|
|
57
|
-
instr.set(qn("xml:space"), "preserve")
|
|
58
|
-
instr.text = " PAGE "
|
|
59
|
-
sep = OxmlElement("w:fldChar")
|
|
60
|
-
sep.set(qn("w:fldCharType"), "separate")
|
|
61
|
-
t = OxmlElement("w:t")
|
|
62
|
-
t.text = "1"
|
|
63
|
-
end = OxmlElement("w:fldChar")
|
|
64
|
-
end.set(qn("w:fldCharType"), "end")
|
|
65
|
-
r = paragraph.add_run()
|
|
66
|
-
set_east_asia(r)
|
|
67
|
-
for el in (fld, instr, sep, t, end):
|
|
68
|
-
r._r.append(el)
|
|
69
|
-
run2 = paragraph.add_run(" 页")
|
|
70
|
-
set_east_asia(run2)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
doc = Document()
|
|
74
|
-
doc.add_heading(os.path.splitext(os.path.basename(SRC))[0], level=0)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# --- base styles ---
|
|
78
|
-
normal = doc.styles["Normal"]
|
|
79
|
-
normal.font.name = "Calibri"
|
|
80
|
-
normal.font.size = Pt(10.5)
|
|
81
|
-
normal._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def set_east_asia(run):
|
|
85
|
-
run.font.name = "Calibri"
|
|
86
|
-
r = run._element
|
|
87
|
-
rPr = r.get_or_add_rPr()
|
|
88
|
-
rf = rPr.find(qn("w:rFonts"))
|
|
89
|
-
if rf is None:
|
|
90
|
-
rf = OxmlElement("w:rFonts")
|
|
91
|
-
rPr.append(rf)
|
|
92
|
-
rf.set(qn("w:eastAsia"), "微软雅黑")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def add_runs_with_bold(par, text):
|
|
96
|
-
"""Add text to paragraph, honoring **bold** and *italic* markers."""
|
|
97
|
-
for tok in re.split(r"(\*\*.*?\*\*|\*.*?\*)", text):
|
|
98
|
-
if not tok:
|
|
99
|
-
continue
|
|
100
|
-
if tok.startswith("**") and tok.endswith("**") and len(tok) > 4:
|
|
101
|
-
r = par.add_run(tok[2:-2])
|
|
102
|
-
r.bold = True
|
|
103
|
-
elif tok.startswith("*") and tok.endswith("*") and len(tok) > 2:
|
|
104
|
-
r = par.add_run(tok[1:-1])
|
|
105
|
-
r.italic = True
|
|
106
|
-
else:
|
|
107
|
-
r = par.add_run(tok)
|
|
108
|
-
set_east_asia(r)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def add_body_paragraph(text, style=None):
|
|
112
|
-
p = doc.add_paragraph(style=style)
|
|
113
|
-
add_runs_with_bold(p, text)
|
|
114
|
-
return p
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def flush_table():
|
|
118
|
-
global table_rows, in_table
|
|
119
|
-
if not table_rows:
|
|
120
|
-
in_table = False
|
|
121
|
-
return
|
|
122
|
-
ncols = max(len(r) for r in table_rows)
|
|
123
|
-
tbl = doc.add_table(rows=len(table_rows), cols=ncols)
|
|
124
|
-
tbl.style = "Light Grid Accent 1"
|
|
125
|
-
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
|
126
|
-
for ri, row in enumerate(table_rows):
|
|
127
|
-
for ci in range(ncols):
|
|
128
|
-
cell = tbl.cell(ri, ci)
|
|
129
|
-
cell.text = ""
|
|
130
|
-
cp = cell.paragraphs[0]
|
|
131
|
-
text = row[ci] if ci < len(row) else ""
|
|
132
|
-
add_runs_with_bold(cp, text)
|
|
133
|
-
if ri == 0:
|
|
134
|
-
for run in cp.runs:
|
|
135
|
-
run.bold = True
|
|
136
|
-
tbl.autofit = True
|
|
137
|
-
table_rows = []
|
|
138
|
-
in_table = False
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
table_rows = []
|
|
142
|
-
in_table = False
|
|
143
|
-
|
|
144
|
-
i = 0
|
|
145
|
-
while i < len(lines):
|
|
146
|
-
stripped = lines[i].strip()
|
|
147
|
-
if not stripped:
|
|
148
|
-
i += 1
|
|
149
|
-
continue
|
|
150
|
-
if re.fullmatch(r"-{3,}", stripped):
|
|
151
|
-
flush_table()
|
|
152
|
-
doc.add_paragraph()
|
|
153
|
-
i += 1
|
|
154
|
-
continue
|
|
155
|
-
if in_table and "|" in stripped and re.fullmatch(r"\|?[\s:|-]+\|?", stripped):
|
|
156
|
-
i += 1
|
|
157
|
-
continue
|
|
158
|
-
if stripped.startswith("|"):
|
|
159
|
-
in_table = True
|
|
160
|
-
table_rows.append([c.strip() for c in stripped.strip("|").split("|")])
|
|
161
|
-
i += 1
|
|
162
|
-
continue
|
|
163
|
-
if in_table:
|
|
164
|
-
flush_table()
|
|
165
|
-
if stripped.startswith("### "):
|
|
166
|
-
h = doc.add_heading(level=3)
|
|
167
|
-
add_runs_with_bold(h, stripped[4:])
|
|
168
|
-
i += 1
|
|
169
|
-
continue
|
|
170
|
-
if stripped.startswith("## "):
|
|
171
|
-
h = doc.add_heading(level=2)
|
|
172
|
-
add_runs_with_bold(h, stripped[3:])
|
|
173
|
-
i += 1
|
|
174
|
-
continue
|
|
175
|
-
if stripped.startswith("# "):
|
|
176
|
-
h = doc.add_heading(level=1)
|
|
177
|
-
add_runs_with_bold(h, stripped[2:])
|
|
178
|
-
i += 1
|
|
179
|
-
continue
|
|
180
|
-
if stripped.startswith("> "):
|
|
181
|
-
p = doc.add_paragraph(style="Intense Quote")
|
|
182
|
-
add_runs_with_bold(p, stripped[2:] + " ")
|
|
183
|
-
i += 1
|
|
184
|
-
continue
|
|
185
|
-
m = re.match(r"^(\d+)\.\s+(.*)$", stripped)
|
|
186
|
-
if m:
|
|
187
|
-
p = doc.add_paragraph(style="List Number")
|
|
188
|
-
add_runs_with_bold(p, m.group(2))
|
|
189
|
-
i += 1
|
|
190
|
-
continue
|
|
191
|
-
if stripped.startswith("- "):
|
|
192
|
-
p = doc.add_paragraph(style="List Bullet")
|
|
193
|
-
add_runs_with_bold(p, stripped[2:])
|
|
194
|
-
i += 1
|
|
195
|
-
continue
|
|
196
|
-
p = doc.add_paragraph()
|
|
197
|
-
add_runs_with_bold(p, stripped)
|
|
198
|
-
i += 1
|
|
199
|
-
|
|
200
|
-
if in_table:
|
|
201
|
-
flush_table()
|
|
202
|
-
|
|
203
|
-
# --- footer with page number field ---
|
|
204
|
-
footer = doc.sections[0].footer
|
|
205
|
-
fp = footer.paragraphs[0]
|
|
206
|
-
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
207
|
-
add_page_number(fp)
|
|
208
|
-
|
|
209
|
-
doc.save(OUT)
|
|
210
|
-
print("Saved:", OUT)
|