dsh-office-kit 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CAPABILITY.md
CHANGED
|
@@ -138,10 +138,11 @@ never guessed.
|
|
|
138
138
|
| PDF create / read / render / `metadata.py` / `page_ops.py` / `redact_region.py` | ok — CJK font applied, metadata written, merge→remove→rotate, region redaction removed 170 chars and 11 drawings, `page-1.png` produced |
|
|
139
139
|
| Annotation geometry helpers | pymupdf rect conversion, python-pptx overlap scan, A1 text matching run against those files |
|
|
140
140
|
| Forbidden-vocabulary grep over the delivered docs | empty |
|
|
141
|
+
| macOS 26 arm64, DSH 0.2.1-rc.2, `DSH_OFFICE_PYTHON` = Homebrew 3.13 | full vitest suite with `DSH_OFFICE_REQUIRE_RENDERER=1` passes; docx/pptx/xlsx render and xlsx recalc report `engine: native`, also with the app's Electron binary as the recorded node; PDF create/stamp pick STHeiti |
|
|
141
142
|
|
|
142
143
|
Not claimed (no evidence here): installer or self-heal timings, live GUI
|
|
143
|
-
interaction (`/` menu, chip, sidebar preview, annotation overlay), and
|
|
144
|
-
|
|
144
|
+
interaction (`/` menu, chip, sidebar preview, annotation overlay), and automatic
|
|
145
|
+
Python discovery on macOS (see #23).
|
|
145
146
|
|
|
146
147
|
## Explicitly out of scope
|
|
147
148
|
|
|
@@ -152,6 +153,6 @@ renderer support.
|
|
|
152
153
|
so); OCR and scanned-PDF text extraction.
|
|
153
154
|
- PPTX animations, speaker notes, slide masters; XLSX pivots, macros, data
|
|
154
155
|
validation, conditional formatting — no spec keys or verbs in the helper.
|
|
155
|
-
- Formula evaluation without the DSH LibreOffice kit;
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
- Formula evaluation without the DSH LibreOffice kit; Linux installation was
|
|
157
|
+
exercised in CI (Linux runs the WASM engine); Windows and macOS were checked by
|
|
158
|
+
hand.
|
|
@@ -5,7 +5,9 @@ Word's TOC field needs a refresh before its links work. This script instead
|
|
|
5
5
|
writes static hyperlinks that any renderer follows: bookmarks on every heading,
|
|
6
6
|
a contents list at the top (with quick links and back links), and optional
|
|
7
7
|
Top/Bottom anchors. Because the entries are literal text there is nothing to
|
|
8
|
-
update before the file is read.
|
|
8
|
+
update before the file is read. Rerunning on a file this script already
|
|
9
|
+
processed strips the previous navigation first, so the output is rebuilt
|
|
10
|
+
cleanly instead of stacking a second contents block.
|
|
9
11
|
|
|
10
12
|
Exit codes: 0 success, 1 no headings, 2 bad input.
|
|
11
13
|
"""
|
|
@@ -59,6 +61,63 @@ def paragraph(style: str | None = None):
|
|
|
59
61
|
return node
|
|
60
62
|
|
|
61
63
|
|
|
64
|
+
def _remove_bookmark(node) -> None:
|
|
65
|
+
"""Drop one bookmarkStart and its matching bookmarkEnd wherever they live."""
|
|
66
|
+
ident = node.get(kit.q("id"))
|
|
67
|
+
parent = node.getparent()
|
|
68
|
+
if parent is not None:
|
|
69
|
+
parent.remove(node)
|
|
70
|
+
if ident is None:
|
|
71
|
+
return
|
|
72
|
+
root = parent if parent is not None else node
|
|
73
|
+
while root.getparent() is not None:
|
|
74
|
+
root = root.getparent()
|
|
75
|
+
for end in list(root.iter(kit.q("bookmarkEnd"))):
|
|
76
|
+
if end.get(kit.q("id")) == ident:
|
|
77
|
+
end.getparent().remove(end)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def strip_previous_nav(body) -> int:
|
|
81
|
+
"""Remove navigation inserted by an earlier run, so reruns rebuild cleanly.
|
|
82
|
+
|
|
83
|
+
Without this, a second run stacks a second contents block, re-collects the
|
|
84
|
+
first block's title as a chapter, and duplicates the nav_top/nav_toc/
|
|
85
|
+
nav_bottom bookmark names, which makes every back link resolve to the stale
|
|
86
|
+
block. A paragraph is treated as generated when it links to a nav_ anchor or
|
|
87
|
+
carries a structural nav bookmark; a real heading only loses its nav_h
|
|
88
|
+
bookmarks. Returns the number of paragraphs removed.
|
|
89
|
+
"""
|
|
90
|
+
removed = 0
|
|
91
|
+
for paragraph_node in list(body.iter(kit.q("p"))):
|
|
92
|
+
bookmarks = [
|
|
93
|
+
node
|
|
94
|
+
for node in paragraph_node.iter(kit.q("bookmarkStart"))
|
|
95
|
+
if (node.get(kit.q("name")) or "").startswith("nav_")
|
|
96
|
+
]
|
|
97
|
+
links = [
|
|
98
|
+
node
|
|
99
|
+
for node in paragraph_node.iter(kit.q("hyperlink"))
|
|
100
|
+
if (node.get(kit.q("anchor")) or "").startswith("nav_")
|
|
101
|
+
]
|
|
102
|
+
if not bookmarks and not links:
|
|
103
|
+
continue
|
|
104
|
+
generated = bool(links) or any(
|
|
105
|
+
node.get(kit.q("name")) in (TOP_BOOKMARK, BOTTOM_BOOKMARK, TOC_BOOKMARK)
|
|
106
|
+
for node in bookmarks
|
|
107
|
+
)
|
|
108
|
+
if generated:
|
|
109
|
+
for node in bookmarks:
|
|
110
|
+
_remove_bookmark(node)
|
|
111
|
+
parent = paragraph_node.getparent()
|
|
112
|
+
if parent is not None:
|
|
113
|
+
parent.remove(paragraph_node)
|
|
114
|
+
removed += 1
|
|
115
|
+
else:
|
|
116
|
+
for node in bookmarks:
|
|
117
|
+
_remove_bookmark(node)
|
|
118
|
+
return removed
|
|
119
|
+
|
|
120
|
+
|
|
62
121
|
def main(argv: list[str] | None = None) -> int:
|
|
63
122
|
parser = argparse.ArgumentParser(description="Add bookmark anchors and static internal links to headings.")
|
|
64
123
|
parser.add_argument("input_docx", help="Input .docx")
|
|
@@ -77,6 +136,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
77
136
|
return 2
|
|
78
137
|
|
|
79
138
|
body = doc.body()
|
|
139
|
+
stripped = strip_previous_nav(body)
|
|
80
140
|
headings = []
|
|
81
141
|
ident = doc.next_id()
|
|
82
142
|
for paragraph_node in list(body.iter(kit.q("p"))):
|
|
@@ -147,7 +207,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
147
207
|
body.append(last)
|
|
148
208
|
|
|
149
209
|
doc.save(args.out)
|
|
150
|
-
kit.emit({"ok": True, "output": str(Path(args.out)), "headings": headings, "entries": len(block)})
|
|
210
|
+
kit.emit({"ok": True, "output": str(Path(args.out)), "headings": headings, "entries": len(block), "stripped": stripped})
|
|
151
211
|
return 0
|
|
152
212
|
|
|
153
213
|
|
|
@@ -25,9 +25,9 @@ CJK_FONT_CANDIDATES = {
|
|
|
25
25
|
"/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc",
|
|
26
26
|
],
|
|
27
27
|
"darwin": [
|
|
28
|
-
"/System/Library/Fonts/PingFang.ttc",
|
|
29
28
|
"/System/Library/Fonts/STHeiti Medium.ttc",
|
|
30
|
-
"/System/Library/Fonts/
|
|
29
|
+
"/System/Library/Fonts/STHeiti Light.ttc",
|
|
30
|
+
"/System/Library/Fonts/Supplemental/Songti.ttc",
|
|
31
31
|
],
|
|
32
32
|
}
|
|
33
33
|
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { dirname, join, sep } from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { BUNDLED_SKILL_RANK } from "@deepseek-ai/dsh-skill";
|
|
@@ -55,6 +55,16 @@ const OFFICE_FEATURES = [
|
|
|
55
55
|
const PACKAGE_ROOT$1 = fileURLToPath(new URL("..", import.meta.url));
|
|
56
56
|
const KIT_CLI = "@deepseek-ai/libreoffice-kit/cli";
|
|
57
57
|
/**
|
|
58
|
+
* Electron reads `app.asar` as a directory but nothing else can, and the helper
|
|
59
|
+
* runs the recorded path from Python. The packaged app ships the kit unpacked.
|
|
60
|
+
*/
|
|
61
|
+
function outsideAsar(path) {
|
|
62
|
+
const packed = `${sep}app.asar${sep}`;
|
|
63
|
+
if (!path.includes(packed)) return path;
|
|
64
|
+
const unpacked = path.replace(packed, `${sep}app.asar.unpacked${sep}`);
|
|
65
|
+
return existsSync(unpacked) ? unpacked : path;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
58
68
|
* Locate the LibreOffice kit CLI that DeepSeek Harness ships.
|
|
59
69
|
*
|
|
60
70
|
* DSH installs third-party plugins into a profile (`~/.dsh/profiles/<name>/node_modules`)
|
|
@@ -71,7 +81,7 @@ function resolveKitCli(entry = process.argv[1]) {
|
|
|
71
81
|
if (anchor == null) continue;
|
|
72
82
|
try {
|
|
73
83
|
const cli = createRequire(anchor).resolve(KIT_CLI);
|
|
74
|
-
if (existsSync(cli)) return cli;
|
|
84
|
+
if (existsSync(cli)) return outsideAsar(cli);
|
|
75
85
|
} catch {}
|
|
76
86
|
}
|
|
77
87
|
return null;
|
package/icon.svg
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M10.5 7.5H20L26 13.5V28C26 28.83 25.33 29.5 24.5 29.5H10.5C9.67 29.5 9 28.83 9 28V9C9 8.17 9.67 7.5 10.5 7.5Z" fill="url(#office-page)" fill-opacity="0.85" />
|
|
3
|
+
<path d="M20 7.5V12.5C20 13.33 20.67 14 21.5 14H26L20 7.5Z" fill="#8CC5FD" fill-opacity="0.6" />
|
|
4
|
+
<path d="M12.75 17.5H22M12.75 22H18.75" stroke="#FFFFFF" stroke-width="1.9" stroke-linecap="round" />
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient id="office-page" x1="9.5" y1="9" x2="27" y2="29" gradientUnits="userSpaceOnUse">
|
|
7
|
+
<stop stop-color="#67C7FE" />
|
|
8
|
+
<stop offset="1" stop-color="#3F77D8" />
|
|
9
|
+
</linearGradient>
|
|
10
|
+
</defs>
|
|
11
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-office-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.7",
|
|
5
5
|
"description": "Office documents, PDF, presentations, and spreadsheets for DeepSeek Harness",
|
|
6
6
|
"author": "Hairy",
|
|
7
7
|
"license": "MIT",
|
|
@@ -29,12 +29,14 @@
|
|
|
29
29
|
"main": "./dist/index.js",
|
|
30
30
|
"module": "./dist/index.js",
|
|
31
31
|
"types": "./dist/index.d.ts",
|
|
32
|
+
"icon": "./icon.svg",
|
|
32
33
|
"files": [
|
|
33
34
|
"CAPABILITY.md",
|
|
34
35
|
"README.md",
|
|
35
36
|
"assets",
|
|
36
37
|
"cordis.patch.yml",
|
|
37
38
|
"dist",
|
|
39
|
+
"icon.svg",
|
|
38
40
|
"postinstall.mjs",
|
|
39
41
|
"python"
|
|
40
42
|
],
|
package/python/office_cli.py
CHANGED
|
@@ -184,6 +184,11 @@ def find_kit() -> tuple[str, str] | None:
|
|
|
184
184
|
return node, str(cli)
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
def _kit_env() -> dict[str, str]:
|
|
188
|
+
"""Environment for a kit run: the recorded node may be the harness's Electron binary."""
|
|
189
|
+
return {**os.environ, "ELECTRON_RUN_AS_NODE": "1"}
|
|
190
|
+
|
|
191
|
+
|
|
187
192
|
def _kit_call(arguments: list[str], timeout: int = 300) -> dict[str, Any]:
|
|
188
193
|
"""Run one kit command and return its JSON report, failing with the kit's own words."""
|
|
189
194
|
kit = find_kit()
|
|
@@ -195,6 +200,7 @@ def _kit_call(arguments: list[str], timeout: int = 300) -> dict[str, Any]:
|
|
|
195
200
|
[node, cli, *arguments],
|
|
196
201
|
capture_output=True,
|
|
197
202
|
text=True,
|
|
203
|
+
env=_kit_env(),
|
|
198
204
|
# The kit always speaks UTF-8; the locale code page would mangle CJK paths.
|
|
199
205
|
encoding="utf-8",
|
|
200
206
|
errors="replace",
|
|
@@ -231,6 +237,7 @@ def kit_report() -> dict[str, Any]:
|
|
|
231
237
|
[node, cli, "capabilities", "--json"],
|
|
232
238
|
capture_output=True,
|
|
233
239
|
text=True,
|
|
240
|
+
env=_kit_env(),
|
|
234
241
|
encoding="utf-8",
|
|
235
242
|
errors="replace",
|
|
236
243
|
timeout=120,
|
|
@@ -763,8 +770,9 @@ _PDF_CJK_CANDIDATES = [
|
|
|
763
770
|
r"C:\Windows\Fonts\msyh.ttf",
|
|
764
771
|
r"C:\Windows\Fonts\simhei.ttf",
|
|
765
772
|
r"C:\Windows\Fonts\simsun.ttc",
|
|
766
|
-
"/System/Library/Fonts/PingFang.ttc",
|
|
767
773
|
"/System/Library/Fonts/STHeiti Light.ttc",
|
|
774
|
+
"/System/Library/Fonts/STHeiti Medium.ttc",
|
|
775
|
+
"/System/Library/Fonts/Supplemental/Songti.ttc",
|
|
768
776
|
"/usr/share/fonts/truetype/winfonts/msyh.ttc",
|
|
769
777
|
"/usr/share/fonts/truetype/winfonts/msyhl.ttc",
|
|
770
778
|
"/usr/share/fonts/truetype/winfonts/simhei.ttf",
|