pi-quiver 6.5.0 → 6.7.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 +20 -0
- package/README.md +11 -6
- package/dist/bin/pi-quiver.js +481 -150
- package/extensions/doc_to_md.ts +2 -2
- package/lib/doc-to-md-bundle.ts +94 -23
- package/lib/doc-to-md-core.ts +163 -42
- package/lib/doc-to-md-handle.ts +49 -7
- package/lib/doc-to-md-options.ts +22 -10
- package/lib/fetch-core.ts +5 -0
- package/package.json +2 -1
- package/scripts/doc_to_md.py +509 -63
- package/scripts/docx_numbering.py +171 -0
package/dist/bin/pi-quiver.js
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
8
16
|
|
|
9
17
|
// lib/fetch-core.ts
|
|
18
|
+
var fetch_core_exports = {};
|
|
19
|
+
__export(fetch_core_exports, {
|
|
20
|
+
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
21
|
+
applyGate: () => applyGate,
|
|
22
|
+
binaryExtension: () => binaryExtension,
|
|
23
|
+
buildGhArgs: () => buildGhArgs,
|
|
24
|
+
buildGhLogArgs: () => buildGhLogArgs,
|
|
25
|
+
categorize: () => categorize,
|
|
26
|
+
classifyGitHubTarget: () => classifyGitHubTarget,
|
|
27
|
+
collectBody: () => collectBody,
|
|
28
|
+
executeGhRouting: () => executeGhRouting,
|
|
29
|
+
fetchUrl: () => fetchUrl,
|
|
30
|
+
formatSize: () => formatSize,
|
|
31
|
+
htmlToMarkdown: () => htmlToMarkdown,
|
|
32
|
+
htmlToMarkdownRaw: () => htmlToMarkdownRaw,
|
|
33
|
+
planGhRouting: () => planGhRouting,
|
|
34
|
+
prettyJson: () => prettyJson,
|
|
35
|
+
runGh: () => runGh
|
|
36
|
+
});
|
|
10
37
|
import { mkdirSync, writeFileSync, createWriteStream } from "node:fs";
|
|
11
38
|
import { rm } from "node:fs/promises";
|
|
12
39
|
import { tmpdir } from "node:os";
|
|
@@ -27,24 +54,6 @@ function formatSize(bytes) {
|
|
|
27
54
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
28
55
|
}
|
|
29
56
|
}
|
|
30
|
-
var RESERVED_OWNERS = /* @__PURE__ */ new Set([
|
|
31
|
-
"orgs",
|
|
32
|
-
"users",
|
|
33
|
-
"sponsors",
|
|
34
|
-
"topics",
|
|
35
|
-
"marketplace",
|
|
36
|
-
"apps",
|
|
37
|
-
"collections",
|
|
38
|
-
"stars",
|
|
39
|
-
"settings",
|
|
40
|
-
"notifications",
|
|
41
|
-
"codespaces",
|
|
42
|
-
"features",
|
|
43
|
-
"trending",
|
|
44
|
-
"security",
|
|
45
|
-
"customer-stories"
|
|
46
|
-
]);
|
|
47
|
-
var GH_NAME = /^[A-Za-z0-9._-]+$/;
|
|
48
57
|
function classifyGitHubTarget(url) {
|
|
49
58
|
const host = url.hostname.toLowerCase();
|
|
50
59
|
if (host !== "github.com" && host !== "www.github.com") return null;
|
|
@@ -82,22 +91,6 @@ function buildGhLogArgs(target) {
|
|
|
82
91
|
if (target.kind === "job") return ["run", "view", "--job", target.jobId, "--log-failed", "--repo", target.slug];
|
|
83
92
|
return null;
|
|
84
93
|
}
|
|
85
|
-
var GH_MAX_BUFFER = 1e7;
|
|
86
|
-
var execFileAsync = promisify(execFile);
|
|
87
|
-
var runGh = async (args, timeoutMs, signal) => {
|
|
88
|
-
try {
|
|
89
|
-
const { stdout } = await execFileAsync("gh", args, {
|
|
90
|
-
timeout: timeoutMs,
|
|
91
|
-
signal,
|
|
92
|
-
maxBuffer: GH_MAX_BUFFER,
|
|
93
|
-
encoding: "utf8"
|
|
94
|
-
});
|
|
95
|
-
if (!stdout.trim()) return { ok: false };
|
|
96
|
-
return { ok: true, stdout };
|
|
97
|
-
} catch {
|
|
98
|
-
return { ok: false };
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
94
|
function planGhRouting(params, url) {
|
|
102
95
|
if (params.raw) return null;
|
|
103
96
|
if ((params.method ?? "GET") !== "GET") return null;
|
|
@@ -176,16 +169,6 @@ async function executeGhRouting(params, url, signal, runner = runGh) {
|
|
|
176
169
|
}
|
|
177
170
|
return renderGhResult(target, gh.stdout, failedLogs);
|
|
178
171
|
}
|
|
179
|
-
var PARSABLE_MAX_BYTES = 1e6;
|
|
180
|
-
var BINARY_MAX_BYTES = 5e7;
|
|
181
|
-
var SNIFF_MAX_BYTES = 64e3;
|
|
182
|
-
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
183
|
-
var INLINE_MAX_BYTES = 32e3;
|
|
184
|
-
var INLINE_MAX_LINES = 1e3;
|
|
185
|
-
var PREVIEW_LINES = 60;
|
|
186
|
-
var PREVIEW_MAX_BYTES = 4e3;
|
|
187
|
-
var FIREFOX_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.7; rv:135.0) Gecko/20100101 Firefox/135.0";
|
|
188
|
-
var DEFAULT_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
|
|
189
172
|
function parseCharset(contentType) {
|
|
190
173
|
const m = /charset\s*=\s*"?([^";\s]+)"?/i.exec(contentType);
|
|
191
174
|
return (m?.[1] ?? "utf-8").trim().toLowerCase();
|
|
@@ -205,27 +188,9 @@ function buildPreview(body) {
|
|
|
205
188
|
}
|
|
206
189
|
return preview;
|
|
207
190
|
}
|
|
208
|
-
var turndownService = new TurndownService({
|
|
209
|
-
headingStyle: "atx",
|
|
210
|
-
codeBlockStyle: "fenced",
|
|
211
|
-
bulletListMarker: "-"
|
|
212
|
-
});
|
|
213
|
-
turndownService.use(gfm);
|
|
214
191
|
function mimeType(contentType) {
|
|
215
192
|
return contentType.split(";")[0].trim().toLowerCase();
|
|
216
193
|
}
|
|
217
|
-
var TEXT_ALLOWLIST = [
|
|
218
|
-
/^text\//,
|
|
219
|
-
/^application\/(json|xml|xhtml\+xml|javascript)$/,
|
|
220
|
-
/\+json$/,
|
|
221
|
-
/\+xml$/
|
|
222
|
-
];
|
|
223
|
-
var KNOWN_BINARY = [
|
|
224
|
-
/^audio\//,
|
|
225
|
-
/^video\//,
|
|
226
|
-
/^font\//,
|
|
227
|
-
/^application\/(pdf|zip|gzip|x-tar|x-7z-compressed|x-rar-compressed|wasm)$/
|
|
228
|
-
];
|
|
229
194
|
function categorize(contentType, sniff, raw) {
|
|
230
195
|
const mime = mimeType(contentType);
|
|
231
196
|
if (/^image\//.test(mime)) return "binary";
|
|
@@ -265,6 +230,9 @@ function htmlToMarkdown(html, url) {
|
|
|
265
230
|
${md}`;
|
|
266
231
|
return md;
|
|
267
232
|
}
|
|
233
|
+
function htmlToMarkdownRaw(html) {
|
|
234
|
+
return turndownService.turndown(html).trim();
|
|
235
|
+
}
|
|
268
236
|
function prettyJson(text) {
|
|
269
237
|
try {
|
|
270
238
|
return JSON.stringify(JSON.parse(text), null, 2);
|
|
@@ -300,18 +268,6 @@ function textExtension(category, contentType) {
|
|
|
300
268
|
if (category === "json") return "json";
|
|
301
269
|
return mimeType(contentType).includes("xml") ? "xml" : "txt";
|
|
302
270
|
}
|
|
303
|
-
var BINARY_EXT = {
|
|
304
|
-
"application/pdf": "pdf",
|
|
305
|
-
"application/zip": "zip",
|
|
306
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
307
|
-
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
|
308
|
-
"application/gzip": "gz",
|
|
309
|
-
"image/png": "png",
|
|
310
|
-
"image/jpeg": "jpg",
|
|
311
|
-
"image/gif": "gif",
|
|
312
|
-
"image/webp": "webp",
|
|
313
|
-
"image/svg+xml": "svg"
|
|
314
|
-
};
|
|
315
271
|
function binaryExtension(contentType) {
|
|
316
272
|
const mime = mimeType(contentType);
|
|
317
273
|
if (BINARY_EXT[mime]) return BINARY_EXT[mime];
|
|
@@ -515,16 +471,103 @@ async function fetchUrl(opts) {
|
|
|
515
471
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
516
472
|
}
|
|
517
473
|
}
|
|
474
|
+
var RESERVED_OWNERS, GH_NAME, GH_MAX_BUFFER, execFileAsync, runGh, PARSABLE_MAX_BYTES, BINARY_MAX_BYTES, SNIFF_MAX_BYTES, DEFAULT_TIMEOUT_MS, INLINE_MAX_BYTES, INLINE_MAX_LINES, PREVIEW_LINES, PREVIEW_MAX_BYTES, FIREFOX_UA, DEFAULT_ACCEPT, turndownService, TEXT_ALLOWLIST, KNOWN_BINARY, BINARY_EXT;
|
|
475
|
+
var init_fetch_core = __esm({
|
|
476
|
+
"lib/fetch-core.ts"() {
|
|
477
|
+
"use strict";
|
|
478
|
+
RESERVED_OWNERS = /* @__PURE__ */ new Set([
|
|
479
|
+
"orgs",
|
|
480
|
+
"users",
|
|
481
|
+
"sponsors",
|
|
482
|
+
"topics",
|
|
483
|
+
"marketplace",
|
|
484
|
+
"apps",
|
|
485
|
+
"collections",
|
|
486
|
+
"stars",
|
|
487
|
+
"settings",
|
|
488
|
+
"notifications",
|
|
489
|
+
"codespaces",
|
|
490
|
+
"features",
|
|
491
|
+
"trending",
|
|
492
|
+
"security",
|
|
493
|
+
"customer-stories"
|
|
494
|
+
]);
|
|
495
|
+
GH_NAME = /^[A-Za-z0-9._-]+$/;
|
|
496
|
+
GH_MAX_BUFFER = 1e7;
|
|
497
|
+
execFileAsync = promisify(execFile);
|
|
498
|
+
runGh = async (args, timeoutMs, signal) => {
|
|
499
|
+
try {
|
|
500
|
+
const { stdout } = await execFileAsync("gh", args, {
|
|
501
|
+
timeout: timeoutMs,
|
|
502
|
+
signal,
|
|
503
|
+
maxBuffer: GH_MAX_BUFFER,
|
|
504
|
+
encoding: "utf8"
|
|
505
|
+
});
|
|
506
|
+
if (!stdout.trim()) return { ok: false };
|
|
507
|
+
return { ok: true, stdout };
|
|
508
|
+
} catch {
|
|
509
|
+
return { ok: false };
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
PARSABLE_MAX_BYTES = 1e6;
|
|
513
|
+
BINARY_MAX_BYTES = 5e7;
|
|
514
|
+
SNIFF_MAX_BYTES = 64e3;
|
|
515
|
+
DEFAULT_TIMEOUT_MS = 2e4;
|
|
516
|
+
INLINE_MAX_BYTES = 32e3;
|
|
517
|
+
INLINE_MAX_LINES = 1e3;
|
|
518
|
+
PREVIEW_LINES = 60;
|
|
519
|
+
PREVIEW_MAX_BYTES = 4e3;
|
|
520
|
+
FIREFOX_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.7; rv:135.0) Gecko/20100101 Firefox/135.0";
|
|
521
|
+
DEFAULT_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
|
|
522
|
+
turndownService = new TurndownService({
|
|
523
|
+
headingStyle: "atx",
|
|
524
|
+
codeBlockStyle: "fenced",
|
|
525
|
+
bulletListMarker: "-"
|
|
526
|
+
});
|
|
527
|
+
turndownService.use(gfm);
|
|
528
|
+
TEXT_ALLOWLIST = [
|
|
529
|
+
/^text\//,
|
|
530
|
+
/^application\/(json|xml|xhtml\+xml|javascript)$/,
|
|
531
|
+
/\+json$/,
|
|
532
|
+
/\+xml$/
|
|
533
|
+
];
|
|
534
|
+
KNOWN_BINARY = [
|
|
535
|
+
/^audio\//,
|
|
536
|
+
/^video\//,
|
|
537
|
+
/^font\//,
|
|
538
|
+
/^application\/(pdf|zip|gzip|x-tar|x-7z-compressed|x-rar-compressed|wasm)$/
|
|
539
|
+
];
|
|
540
|
+
BINARY_EXT = {
|
|
541
|
+
"application/pdf": "pdf",
|
|
542
|
+
"application/zip": "zip",
|
|
543
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
544
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
|
545
|
+
"application/gzip": "gz",
|
|
546
|
+
"image/png": "png",
|
|
547
|
+
"image/jpeg": "jpg",
|
|
548
|
+
"image/gif": "gif",
|
|
549
|
+
"image/webp": "webp",
|
|
550
|
+
"image/svg+xml": "svg"
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// bin/pi-quiver.ts
|
|
556
|
+
init_fetch_core();
|
|
557
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
558
|
+
import { homedir as homedir2 } from "node:os";
|
|
559
|
+
import { join as join4, resolve as resolve3 } from "node:path";
|
|
560
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
518
561
|
|
|
519
562
|
// lib/doc-to-md-core.ts
|
|
520
|
-
import { existsSync as existsSync2, mkdtempSync, readdirSync as readdirSync2, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
|
|
563
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync2, readdirSync as readdirSync2, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
521
564
|
import { spawn } from "node:child_process";
|
|
522
565
|
import { homedir, tmpdir as tmpdir3 } from "node:os";
|
|
523
|
-
import { basename, dirname, extname as extname3, join as join3, resolve as resolve2 } from "node:path";
|
|
566
|
+
import { basename, dirname, extname as extname3, isAbsolute, join as join3, resolve as resolve2 } from "node:path";
|
|
524
567
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
525
568
|
|
|
526
569
|
// lib/doc-to-md-bundle.ts
|
|
527
|
-
import fs, { closeSync, existsSync, mkdirSync as mkdirSync2, openSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
570
|
+
import fs, { closeSync, existsSync, mkdirSync as mkdirSync2, openSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
528
571
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
529
572
|
import { extname, join as join2, resolve } from "node:path";
|
|
530
573
|
import { randomBytes } from "node:crypto";
|
|
@@ -535,31 +578,59 @@ function ownedPattern(stem) {
|
|
|
535
578
|
function ownedCsvPattern(stem) {
|
|
536
579
|
return new RegExp(`^${escRe(stem)}-s\\d+-[a-z0-9-]+\\.csv$`);
|
|
537
580
|
}
|
|
538
|
-
|
|
581
|
+
function ownedPagePattern(stem) {
|
|
582
|
+
return new RegExp(`^${escRe(stem)}-p\\d+\\.[a-z0-9]+$`);
|
|
583
|
+
}
|
|
584
|
+
var FILE_LINK_RE = /\[[^\]]*\]\(\s*((?:sheets|attachments)\/[^)\s]+)\s*\)/g;
|
|
539
585
|
var IMG_LINK_RE = /!\[[^\]]*\]\(\s*(?:<([^>]*)>|([^)]*?))\s*\)|<img\b[^>]*\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>"']+))/gi;
|
|
540
586
|
function imageTarget(m) {
|
|
541
587
|
const target = (m[1] ?? m[2] ?? m[3] ?? m[4] ?? m[5] ?? "").trim();
|
|
542
588
|
const destination = target.replace(/^<([\s\S]*)>$/, "$1").trim();
|
|
543
589
|
return m[2] === void 0 ? destination : destination.replace(/\s+(?:"[^"]*"|'[^']*'|\([^)]*\))$/, "");
|
|
544
590
|
}
|
|
545
|
-
function openBundle(root,
|
|
591
|
+
function openBundle(root, requested, overwrite) {
|
|
546
592
|
mkdirSync2(root, { recursive: true });
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
593
|
+
let stem, mdPath, lockPath;
|
|
594
|
+
let renameReason = null;
|
|
595
|
+
const exists = `${requested}.md exists`;
|
|
596
|
+
const held = `${requested}.md.lock held; delete it if no conversion is running`;
|
|
597
|
+
for (let i = 1; ; i++) {
|
|
598
|
+
stem = i === 1 ? requested : `${requested}-${i}`;
|
|
599
|
+
mdPath = join2(root, `${stem}.md`);
|
|
600
|
+
lockPath = `${mdPath}.lock`;
|
|
601
|
+
if (!overwrite) {
|
|
602
|
+
const mdExists = existsSync(mdPath);
|
|
603
|
+
if (mdExists || existsSync(lockPath)) {
|
|
604
|
+
if (i === 1) renameReason = mdExists ? exists : held;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
let fd;
|
|
609
|
+
try {
|
|
610
|
+
fd = openSync(lockPath, "wx");
|
|
611
|
+
} catch (e) {
|
|
612
|
+
if (e.code !== "EEXIST") throw new Error(`Output directory not writable: ${root} (${e.message})`);
|
|
613
|
+
if (overwrite) throw new Error(`Another conversion owns ${mdPath} (lock: ${lockPath}); if no conversion is running, delete the lock`);
|
|
614
|
+
if (i === 1) renameReason = held;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
closeSync(fd);
|
|
618
|
+
if (!overwrite && existsSync(mdPath)) {
|
|
619
|
+
unlinkSync(lockPath);
|
|
620
|
+
if (i === 1) renameReason = exists;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
break;
|
|
555
624
|
}
|
|
556
|
-
|
|
625
|
+
const renamedFrom = stem === requested ? null : requested;
|
|
557
626
|
const imagesDir = join2(root, "images");
|
|
558
627
|
const sheetsDir = join2(root, "sheets");
|
|
628
|
+
const pagesDir = join2(root, "pages");
|
|
629
|
+
const attachmentsDir = join2(root, "attachments");
|
|
559
630
|
try {
|
|
560
|
-
if (existsSync(mdPath)) {
|
|
561
|
-
|
|
562
|
-
const
|
|
631
|
+
if (existsSync(mdPath) && overwrite) {
|
|
632
|
+
const owned = ownedPattern(stem), ownedCsv = ownedCsvPattern(stem), ownedPage = ownedPagePattern(stem);
|
|
633
|
+
const attachments = new Set([...readFileSync(mdPath, "utf8").matchAll(FILE_LINK_RE)].filter((m) => m[1].startsWith("attachments/")).map((m) => m[1].slice("attachments/".length)).filter((f) => f.startsWith(`${stem}-`)));
|
|
563
634
|
unlinkSync(mdPath);
|
|
564
635
|
if (existsSync(imagesDir)) {
|
|
565
636
|
for (const f of readdirSync(imagesDir)) if (owned.test(f)) rmSync(join2(imagesDir, f), { force: true });
|
|
@@ -567,12 +638,20 @@ function openBundle(root, stem, overwrite) {
|
|
|
567
638
|
if (existsSync(sheetsDir)) {
|
|
568
639
|
for (const f of readdirSync(sheetsDir)) if (ownedCsv.test(f)) rmSync(join2(sheetsDir, f), { force: true });
|
|
569
640
|
}
|
|
641
|
+
if (existsSync(pagesDir)) {
|
|
642
|
+
for (const f of readdirSync(pagesDir)) if (ownedPage.test(f)) rmSync(join2(pagesDir, f), { force: true });
|
|
643
|
+
}
|
|
644
|
+
if (existsSync(attachmentsDir)) {
|
|
645
|
+
for (const f of readdirSync(attachmentsDir)) if (attachments.has(f)) rmSync(join2(attachmentsDir, f), { force: true });
|
|
646
|
+
}
|
|
570
647
|
}
|
|
571
648
|
const lockId = randomBytes(6).toString("hex");
|
|
572
649
|
const stagingDir = join2(imagesDir, `.stage-${lockId}`);
|
|
573
650
|
const sheetsStagingDir = join2(sheetsDir, `.stage-${lockId}`);
|
|
574
651
|
mkdirSync2(stagingDir, { recursive: true });
|
|
575
|
-
|
|
652
|
+
const pagesStagingDir = join2(pagesDir, `.stage-${lockId}`);
|
|
653
|
+
const attachmentsStagingDir = join2(attachmentsDir, `.stage-${lockId}`);
|
|
654
|
+
return { root, stem, renamedFrom, renameReason, mdPath, lockPath, imagesDir, stagingDir, lockId, sheetsDir, sheetsStagingDir, pagesDir, pagesStagingDir, attachmentsDir, attachmentsStagingDir, manifest: /* @__PURE__ */ new Set(), csvManifest: /* @__PURE__ */ new Set(), pageManifest: /* @__PURE__ */ new Set(), attachmentManifest: /* @__PURE__ */ new Set(), sourceMap: /* @__PURE__ */ new Map() };
|
|
576
655
|
} catch (e) {
|
|
577
656
|
rmSync(lockPath, { force: true });
|
|
578
657
|
throw e;
|
|
@@ -623,24 +702,54 @@ function publishSheetCsvs(b) {
|
|
|
623
702
|
b.sourceMap.set(`sheets/${f}`, `sheets/${name}`);
|
|
624
703
|
}
|
|
625
704
|
}
|
|
705
|
+
function publishPageImages(b, pageCount) {
|
|
706
|
+
if (!existsSync(b.pagesStagingDir)) return;
|
|
707
|
+
for (const f of readdirSync(b.pagesStagingDir).sort()) {
|
|
708
|
+
const m = f.match(/^p(\d+)(\.[a-z0-9]+)$/i);
|
|
709
|
+
if (!m || !statSync(join2(b.pagesStagingDir, f)).isFile()) continue;
|
|
710
|
+
const name = `${b.stem}-p${m[1].padStart(String(pageCount).length, "0")}${m[2]}`;
|
|
711
|
+
mkdirSync2(b.pagesDir, { recursive: true });
|
|
712
|
+
renameSync(join2(b.pagesStagingDir, f), join2(b.pagesDir, name));
|
|
713
|
+
b.pageManifest.add(name);
|
|
714
|
+
b.sourceMap.set(`pages/${f}`, `pages/${name}`);
|
|
715
|
+
}
|
|
716
|
+
rmSync(b.pagesStagingDir, { recursive: true, force: true });
|
|
717
|
+
}
|
|
718
|
+
function publishAttachments(b) {
|
|
719
|
+
if (!existsSync(b.attachmentsStagingDir)) return;
|
|
720
|
+
for (const f of readdirSync(b.attachmentsStagingDir).sort()) {
|
|
721
|
+
if (!statSync(join2(b.attachmentsStagingDir, f)).isFile()) continue;
|
|
722
|
+
const name = `${b.stem}-${f}`;
|
|
723
|
+
mkdirSync2(b.attachmentsDir, { recursive: true });
|
|
724
|
+
renameSync(join2(b.attachmentsStagingDir, f), join2(b.attachmentsDir, name));
|
|
725
|
+
b.attachmentManifest.add(name);
|
|
726
|
+
b.sourceMap.set(`attachments/${f}`, `attachments/${name}`);
|
|
727
|
+
}
|
|
728
|
+
rmSync(b.attachmentsStagingDir, { recursive: true, force: true });
|
|
729
|
+
}
|
|
626
730
|
function rewriteLinks(md, sourceMap) {
|
|
627
731
|
const images = md.replace(IMG_LINK_RE, (whole, mdAngle, mdPlain, htmlDouble, htmlSingle, htmlUnquoted) => {
|
|
628
732
|
const target = imageTarget([whole, mdAngle, mdPlain, htmlDouble, htmlSingle, htmlUnquoted]);
|
|
629
733
|
const dest = sourceMap.get(target) ?? sourceMap.get(target.replace(/^\.\//, ""));
|
|
630
734
|
return dest ? whole.replace(mdAngle === void 0 ? target : `<${mdAngle}>`, dest) : whole;
|
|
631
735
|
});
|
|
632
|
-
return images.replace(
|
|
736
|
+
return images.replace(FILE_LINK_RE, (whole, target) => {
|
|
633
737
|
const dest = sourceMap.get(target);
|
|
634
738
|
return dest ? whole.split(target).join(dest) : whole;
|
|
635
739
|
});
|
|
636
740
|
}
|
|
637
|
-
function validateImageLinks(md, manifest, csvManifest = /* @__PURE__ */ new Set()) {
|
|
741
|
+
function validateImageLinks(md, manifest, csvManifest = /* @__PURE__ */ new Set(), html = false, pageManifest = /* @__PURE__ */ new Set(), attachmentManifest = /* @__PURE__ */ new Set()) {
|
|
638
742
|
for (const m of md.matchAll(IMG_LINK_RE)) {
|
|
639
743
|
const target = imageTarget(m);
|
|
640
|
-
if (!target.
|
|
744
|
+
if (html && !target.replace(/^\.\//, "").startsWith("p1/")) continue;
|
|
745
|
+
if (target.startsWith("pages/")) {
|
|
746
|
+
if (!pageManifest.has(target.slice("pages/".length))) throw new Error(`unexpected image reference in output: ${target}`);
|
|
747
|
+
} else if (!target.startsWith("images/") || !manifest.has(target.slice("images/".length))) throw new Error(`unexpected image reference in output: ${target}`);
|
|
641
748
|
}
|
|
642
|
-
for (const m of md.matchAll(
|
|
643
|
-
if (
|
|
749
|
+
for (const m of md.matchAll(FILE_LINK_RE)) {
|
|
750
|
+
if (m[1].startsWith("attachments/")) {
|
|
751
|
+
if (!attachmentManifest.has(m[1].slice("attachments/".length))) throw new Error(`unexpected attachment reference in output: ${m[1]}`);
|
|
752
|
+
} else if (!csvManifest.has(m[1].slice("sheets/".length))) throw new Error(`unexpected sheet reference in output: ${m[1]}`);
|
|
644
753
|
}
|
|
645
754
|
}
|
|
646
755
|
function commitBundle(b, markdown) {
|
|
@@ -655,6 +764,14 @@ function commitBundle(b, markdown) {
|
|
|
655
764
|
fs.rmSync(b.sheetsStagingDir, { recursive: true, force: true });
|
|
656
765
|
} catch {
|
|
657
766
|
}
|
|
767
|
+
try {
|
|
768
|
+
fs.rmSync(b.pagesStagingDir, { recursive: true, force: true });
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
fs.rmSync(b.attachmentsStagingDir, { recursive: true, force: true });
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
658
775
|
try {
|
|
659
776
|
fs.rmSync(b.lockPath, { force: true });
|
|
660
777
|
} catch {
|
|
@@ -663,9 +780,13 @@ function commitBundle(b, markdown) {
|
|
|
663
780
|
function abortBundle(b) {
|
|
664
781
|
for (const f of b.manifest) rmSync(join2(b.imagesDir, f), { force: true });
|
|
665
782
|
for (const f of b.csvManifest) rmSync(join2(b.sheetsDir, f), { force: true });
|
|
783
|
+
for (const f of b.pageManifest) rmSync(join2(b.pagesDir, f), { force: true });
|
|
784
|
+
for (const f of b.attachmentManifest) rmSync(join2(b.attachmentsDir, f), { force: true });
|
|
666
785
|
rmSync(`${b.mdPath}.tmp`, { force: true });
|
|
667
786
|
rmSync(b.stagingDir, { recursive: true, force: true });
|
|
668
787
|
rmSync(b.sheetsStagingDir, { recursive: true, force: true });
|
|
788
|
+
rmSync(b.pagesStagingDir, { recursive: true, force: true });
|
|
789
|
+
rmSync(b.attachmentsStagingDir, { recursive: true, force: true });
|
|
669
790
|
rmSync(b.lockPath, { force: true });
|
|
670
791
|
}
|
|
671
792
|
function tempBundleRoot() {
|
|
@@ -697,6 +818,33 @@ function compactRanges(nums, maxEntries = 20) {
|
|
|
697
818
|
if (parts.length <= maxEntries) return parts.join(", ");
|
|
698
819
|
return `${parts.slice(0, maxEntries).join(", ")} (+${parts.length - maxEntries} more)`;
|
|
699
820
|
}
|
|
821
|
+
var INSTALL_HINT = "install Tesseract (see doc/doc-to-md.md), then rerun with ocr=true";
|
|
822
|
+
var BARE_REASONS = ["fallback tier", "no Python backend"];
|
|
823
|
+
function ocrLine(ocr, type) {
|
|
824
|
+
const image = type === "image";
|
|
825
|
+
const rerun = ocr.tesseract ? "rerun with ocr=true" : INSTALL_HINT;
|
|
826
|
+
switch (ocr.status) {
|
|
827
|
+
case "off":
|
|
828
|
+
return image ? `OCR: off; ${rerun}` : `OCR: off - ${ocr.textless.length} page(s) without a text layer; ${rerun}`;
|
|
829
|
+
case "unavailable": {
|
|
830
|
+
const reason = ocr.reason ?? "";
|
|
831
|
+
const bare = BARE_REASONS.includes(reason) || reason.startsWith("OCR child failed: ");
|
|
832
|
+
return `OCR: unavailable - ${reason}${bare ? "" : " (install Tesseract; see doc/doc-to-md.md)"}`;
|
|
833
|
+
}
|
|
834
|
+
case "skipped":
|
|
835
|
+
return "OCR: skipped - image too small";
|
|
836
|
+
case "ran": {
|
|
837
|
+
const clauses = [`OCR: ${ocr.pages.length} ${image ? "image" : "page(s)"} (${ocr.lang})`];
|
|
838
|
+
if (ocr.noText.length) clauses.push(`no text on pages ${compactRanges(ocr.noText)}`);
|
|
839
|
+
if (ocr.ocrFailed.length) clauses.push(`${ocr.ocrFailed.length} failed and were converted without OCR`);
|
|
840
|
+
if (ocr.budgetStopped.length) {
|
|
841
|
+
const r = compactRanges(ocr.budgetStopped, Number.POSITIVE_INFINITY).replaceAll(", ", ",");
|
|
842
|
+
clauses.push(`time budget reached for pages=${r}; rerun with pages=${r} or raise primaryTimeoutMs`);
|
|
843
|
+
}
|
|
844
|
+
return clauses.join("; ");
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
700
848
|
var PAGE_MARKER_RE = /^--- end of page\.page_number=(\d+) ---$/;
|
|
701
849
|
function scanOutline(md, max) {
|
|
702
850
|
const entries = [];
|
|
@@ -739,13 +887,15 @@ function outlineLines(entries, total) {
|
|
|
739
887
|
function pageCountLabel(h) {
|
|
740
888
|
const n = h.pageCount ?? "?";
|
|
741
889
|
if (h.type !== "docx") return String(n);
|
|
742
|
-
if (h.tier === "docx") return
|
|
890
|
+
if (h.tier === "docx") return (h.explicitBreaks ?? 0) > 0 ? `${n} (explicit page breaks, not printed pages)` : `${n} (no explicit page breaks) - no page markers; cite by Outline line`;
|
|
743
891
|
return `${n} (LibreOffice pagination)`;
|
|
744
892
|
}
|
|
745
893
|
function formatHandle(h) {
|
|
746
894
|
const lines = [`Saved-To: ${h.savedTo}`];
|
|
747
895
|
if (h.imagesDir && h.imageCount > 0) lines.push(`Images-Dir: ${h.imagesDir}`);
|
|
748
896
|
if (h.sheetsDir) lines.push(`Sheets-Dir: ${h.sheetsDir}`);
|
|
897
|
+
if (h.pagesDir && h.pageImageCount > 0) lines.push(`Pages-Dir: ${h.pagesDir} (${h.pageImageCount} pages)`);
|
|
898
|
+
else if (h.pageImagesReason) lines.push(`Pages-Dir: none - ${h.pageImagesReason}`);
|
|
749
899
|
lines.push(`Type: ${h.type} Engine: ${h.engine} Tier: ${h.tier}`);
|
|
750
900
|
lines.push(`Page-Count: ${pageCountLabel(h)} Pages: ${h.pages ? compactRanges(h.pages) : "all"} Images: ${h.imageCount} Size: ${formatSize2(h.bytes)} / ${h.lines} lines`);
|
|
751
901
|
if (h.degraded) lines.push(`Degraded: ${h.degraded}`);
|
|
@@ -754,7 +904,8 @@ function formatHandle(h) {
|
|
|
754
904
|
if (h.failedPages.length) fe.push(`Failed-Pages: ${compactRanges(h.failedPages)}`);
|
|
755
905
|
if (h.emptyPages.length) fe.push(`Empty-Pages: ${compactRanges(h.emptyPages)}`);
|
|
756
906
|
if (fe.length) lines.push(fe.join(" "));
|
|
757
|
-
h.
|
|
907
|
+
if (h.ocr) lines.push(ocrLine(h.ocr, h.type));
|
|
908
|
+
h.notes.slice(0, NOTE_MAX_LINES).forEach((n, i) => lines.push(`${i === 0 ? "Notes: " : " "}${n.startsWith("preview truncated:") ? n : trunc(n, NOTE_MAX_CHARS)}`));
|
|
758
909
|
lines.push(...outlineLines(h.outline, h.outlineTotal));
|
|
759
910
|
return lines.join("\n");
|
|
760
911
|
}
|
|
@@ -785,12 +936,15 @@ var UsageError = class extends Error {
|
|
|
785
936
|
};
|
|
786
937
|
var MIN_PYMUPDF4LLM = "1.27.0";
|
|
787
938
|
var VERSION_RE = /^\d+(\.\d+)*$/;
|
|
939
|
+
var OCR_LANGUAGE_RE = /^[a-z][a-z0-9_]*(\+[a-z][a-z0-9_]*)*$/;
|
|
940
|
+
var IMAGE_EXTS = [".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp", ".gif"];
|
|
788
941
|
var DOC_TO_MD_OPTIONS = [
|
|
789
|
-
{ key: "path", flag: null, type: "string", default: null, settable: false, help: "Local .pdf .docx .pptx .xlsx .xls file" },
|
|
942
|
+
{ key: "path", flag: null, type: "string", default: null, settable: false, help: "Local .pdf .docx .doc .pptx .xlsx .xlsm .xls .msg .eml .html .htm .png .jpg .jpeg .tif .tiff .bmp .gif file" },
|
|
790
943
|
{ key: "info", flag: "--info", type: "bool", default: false, settable: false, help: "Inspect only (page count, metadata, TOC or sheet inventory); no bundle" },
|
|
791
|
-
{ key: "pages", flag: "--pages", type: "pages", default: null, settable: false, help: 'Inclusive 1-based pages, e.g. "12-15" or "3,7,10-12" (PDF/DOCX/PPTX only); default all. DOCX: selects explicit-page-break segments; rejected when the file has none' },
|
|
792
|
-
{ key: "outputDir", flag: "--output-dir", type: "string", default: null, settable: false, help: "Bundle root for <stem>.md + images/; default a per-call temp dir" },
|
|
944
|
+
{ key: "pages", flag: "--pages", type: "pages", default: null, settable: false, help: 'Inclusive 1-based pages, e.g. "12-15" or "3,7,10-12" (PDF/DOCX/DOC/PPTX only); default all; "" means all pages. DOCX: selects explicit-page-break segments; rejected when the file has none' },
|
|
945
|
+
{ key: "outputDir", flag: "--output-dir", type: "string", default: null, settable: false, help: "Bundle root for <stem>.md + images/; default a per-call temp dir. <stem> = basename without extension with [^A-Za-z0-9._-]+ -> _ (empty -> document); a second call on the same stem writes <stem>-2.md" },
|
|
793
946
|
{ key: "overwrite", flag: "--overwrite", type: "bool", default: false, settable: false, help: "Replace an existing completed <stem>.md bundle" },
|
|
947
|
+
{ key: "pageImages", flag: "--page-images", type: "bool", default: false, settable: false, help: "Also render every selected page to pages/<stem>-pNNN.<imageFormat> at imageDpi (PDF, PPTX, .doc, DOCX via LibreOffice); off by default" },
|
|
794
948
|
{ key: "primaryTimeoutMs", flag: "--primary-timeout", type: "int", default: 6e4, settable: true, env: "PI_DOC_TO_MD_CONVERT_TIMEOUT_MS", help: "pymupdf4llm tier and DOCX child (docx mode); also the unpdf tier" },
|
|
795
949
|
{ key: "fallbackTimeoutMs", flag: "--fallback-timeout", type: "int", default: 3e4, settable: true, help: "PyMuPDF get_text tier (including DOCX LibreOffice fallback); also PDF and DOCX info and Excel rendered views" },
|
|
796
950
|
{ key: "sofficeTimeoutMs", flag: "--soffice-timeout", type: "int", default: 12e4, settable: true, env: "PI_DOC_TO_MD_SOFFICE_TIMEOUT_MS", help: "DOCX/PPTX -> PDF via LibreOffice; also Excel rendered views" },
|
|
@@ -800,7 +954,9 @@ var DOC_TO_MD_OPTIONS = [
|
|
|
800
954
|
{ key: "imageDpi", flag: "--image-dpi", type: "int", default: 150, settable: true, help: "Render DPI for page images and Excel rendered views" },
|
|
801
955
|
{ key: "imageFormat", flag: "--image-format", type: "enum", default: "png", settable: true, enumValues: ["png", "jpg"], help: "Rendered image format (embedded images keep their native extension)" },
|
|
802
956
|
{ key: "maxOutputBytes", flag: "--max-output-bytes", type: "int", default: 2e7, settable: true, help: "Child stdout cap in bytes" },
|
|
803
|
-
{ key: "outlineMaxEntries", flag: "--outline-max-entries", type: "int", default: 40, settable: true, help: "Heading outline / TOC / sheet inventory cap in the handle" }
|
|
957
|
+
{ key: "outlineMaxEntries", flag: "--outline-max-entries", type: "int", default: 40, settable: true, help: "Heading outline / TOC / sheet inventory cap in the handle" },
|
|
958
|
+
{ key: "ocr", flag: "--ocr", type: "bool", default: false, settable: true, help: "Run OCR on pages without a text layer and on image inputs when Tesseract language data is installed; off by default (--no-ocr turns a settings-level true off)" },
|
|
959
|
+
{ key: "ocrLanguage", flag: "--ocr-language", type: "lang", default: "eng", settable: true, help: "Tesseract language code(s), +-joined, e.g. deu+eng" }
|
|
804
960
|
];
|
|
805
961
|
var TUNABLE_DEFAULTS = Object.fromEntries(
|
|
806
962
|
DOC_TO_MD_OPTIONS.filter((d) => d.settable).map((d) => [d.key, d.default])
|
|
@@ -827,6 +983,8 @@ function coerceValue(d, raw, fromString = false) {
|
|
|
827
983
|
if (typeof raw !== "string" || !VERSION_RE.test(raw)) return { ok: false, reason: "must be digits and dots" };
|
|
828
984
|
if (!versionAtLeast(raw, MIN_PYMUPDF4LLM)) return { ok: false, reason: `must be >= ${MIN_PYMUPDF4LLM}` };
|
|
829
985
|
return { ok: true, value: raw };
|
|
986
|
+
case "lang":
|
|
987
|
+
return typeof raw === "string" && OCR_LANGUAGE_RE.test(raw) ? { ok: true, value: raw } : { ok: false, reason: "must be Tesseract language codes joined by + (e.g. eng, deu+eng)" };
|
|
830
988
|
case "string":
|
|
831
989
|
return typeof raw === "string" && raw.length > 0 ? { ok: true, value: raw } : { ok: false, reason: "must be a non-empty string" };
|
|
832
990
|
case "pages":
|
|
@@ -849,6 +1007,7 @@ function coerceDocToMdSettings(raw, warn = console.warn) {
|
|
|
849
1007
|
return out;
|
|
850
1008
|
}
|
|
851
1009
|
function parsePages(spec) {
|
|
1010
|
+
if (spec.trim() === "") return null;
|
|
852
1011
|
const out = /* @__PURE__ */ new Set();
|
|
853
1012
|
const parts = spec.split(",").map((s) => s.trim());
|
|
854
1013
|
if (parts.length === 0 || parts.some((p) => p === "")) throw new UsageError(`invalid --pages "${spec}": expected e.g. "12-15" or "3,7,10-12"`);
|
|
@@ -865,10 +1024,11 @@ function sanitizeStem(base) {
|
|
|
865
1024
|
const s = base.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
866
1025
|
return s.length ? s : "document";
|
|
867
1026
|
}
|
|
868
|
-
var SUPPORTED = { ".pdf": "pdf", ".docx": "docx", ".pptx": "pptx", ".xlsx": "xlsx", ".xls": "xls" };
|
|
1027
|
+
var SUPPORTED = { ".pdf": "pdf", ".docx": "docx", ".pptx": "pptx", ".xlsx": "xlsx", ".xls": "xls", ".xlsm": "xlsm", ".doc": "doc", ".msg": "email", ".eml": "email", ".html": "html", ".htm": "html", ...Object.fromEntries(IMAGE_EXTS.map((ext) => [ext, "image"])) };
|
|
1028
|
+
var SUPPORTED_EXTENSIONS = Object.keys(SUPPORTED);
|
|
869
1029
|
function classifyInput(filePath) {
|
|
870
1030
|
const t = SUPPORTED[extname2(filePath).toLowerCase()];
|
|
871
|
-
if (!t) throw new Error(`Unsupported file type "${extname2(filePath) || "(none)"}"; supported: .
|
|
1031
|
+
if (!t) throw new Error(`Unsupported file type "${extname2(filePath) || "(none)"}"; supported: ${Object.keys(SUPPORTED).join(", ")}`);
|
|
872
1032
|
return t;
|
|
873
1033
|
}
|
|
874
1034
|
function resolveOptions(perCall, settings, env) {
|
|
@@ -891,8 +1051,8 @@ function resolveOptions(perCall, settings, env) {
|
|
|
891
1051
|
out[d.key] = value;
|
|
892
1052
|
}
|
|
893
1053
|
const o = out;
|
|
894
|
-
if (o.info && (o.pages !== null || o.outputDir !== null || o.overwrite)) {
|
|
895
|
-
throw new UsageError("--info cannot be combined with --pages, --output-dir or --
|
|
1054
|
+
if (o.info && (o.pages !== null || o.outputDir !== null || o.overwrite || o.pageImages)) {
|
|
1055
|
+
throw new UsageError("--info cannot be combined with --pages, --output-dir, --overwrite or --page-images");
|
|
896
1056
|
}
|
|
897
1057
|
return o;
|
|
898
1058
|
}
|
|
@@ -913,14 +1073,14 @@ function renderHelp() {
|
|
|
913
1073
|
}
|
|
914
1074
|
|
|
915
1075
|
// lib/doc-to-md-core.ts
|
|
916
|
-
var PACKAGE_PINS = { pymupdf4llm: TUNABLE_DEFAULTS.pymupdfVersion, openpyxl: "3.1.5", xlrd: "2.0.2", pillow: "12.3.0", mammoth: "1.13.0", markdownify: "1.2.3", "python-docx": "1.2.0" };
|
|
1076
|
+
var PACKAGE_PINS = { pymupdf4llm: TUNABLE_DEFAULTS.pymupdfVersion, openpyxl: "3.1.5", xlrd: "2.0.2", pillow: "12.3.0", mammoth: "1.13.0", markdownify: "1.2.3", "python-docx": "1.2.0", "extract-msg": "0.56.1" };
|
|
917
1077
|
var KILL_GRACE_MS = 2e3;
|
|
918
|
-
var VENV_DIR_NAME = "doc-to-md-venv-
|
|
919
|
-
var LEGACY_VENV_DIR_NAMES = ["pymupdf-venv", "doc-to-md-venv-v2"];
|
|
1078
|
+
var VENV_DIR_NAME = "doc-to-md-venv-v4";
|
|
1079
|
+
var LEGACY_VENV_DIR_NAMES = ["pymupdf-venv", "doc-to-md-venv-v2", "doc-to-md-venv-v3"];
|
|
920
1080
|
var STDERR_CAP = 1e6;
|
|
921
1081
|
var OUTPUT_MAX_BYTES = 2e7;
|
|
922
1082
|
var EXCEL_PDF_FILTER = 'pdf:calc_pdf_Export:{"SinglePageSheets":{"type":"boolean","value":"true"}}';
|
|
923
|
-
var pinSpecs = (cfg) => [`pymupdf4llm==${cfg.pymupdfVersion}`, `openpyxl==${PACKAGE_PINS.openpyxl}`, `xlrd==${PACKAGE_PINS.xlrd}`, `pillow==${PACKAGE_PINS.pillow}`, `mammoth==${PACKAGE_PINS.mammoth}`, `markdownify==${PACKAGE_PINS.markdownify}`, `python-docx==${PACKAGE_PINS["python-docx"]}`];
|
|
1083
|
+
var pinSpecs = (cfg) => [`pymupdf4llm==${cfg.pymupdfVersion}`, `openpyxl==${PACKAGE_PINS.openpyxl}`, `xlrd==${PACKAGE_PINS.xlrd}`, `pillow==${PACKAGE_PINS.pillow}`, `mammoth==${PACKAGE_PINS.mammoth}`, `markdownify==${PACKAGE_PINS.markdownify}`, `python-docx==${PACKAGE_PINS["python-docx"]}`, `extract-msg==${PACKAGE_PINS["extract-msg"]}`];
|
|
924
1084
|
function withArgs(cfg) {
|
|
925
1085
|
return pinSpecs(cfg).flatMap((spec) => ["--with", spec]);
|
|
926
1086
|
}
|
|
@@ -928,7 +1088,7 @@ function pipInstallArgs(cfg) {
|
|
|
928
1088
|
return ["-m", "pip", "install", ...pinSpecs(cfg)];
|
|
929
1089
|
}
|
|
930
1090
|
function warmArgs(cfg) {
|
|
931
|
-
return ["run", ...withArgs(cfg), "--python", "3.14", "python", "-c", "import pymupdf4llm, openpyxl, xlrd, PIL, mammoth, markdownify, docx"];
|
|
1091
|
+
return ["run", ...withArgs(cfg), "--python", "3.14", "python", "-c", "import pymupdf4llm, openpyxl, xlrd, PIL, mammoth, markdownify, docx, extract_msg"];
|
|
932
1092
|
}
|
|
933
1093
|
function uvChildArgs(cfg, script, mode) {
|
|
934
1094
|
return ["run", ...withArgs(cfg), "--python", "3.14", "python", script, mode];
|
|
@@ -1085,6 +1245,11 @@ try:
|
|
|
1085
1245
|
print("DOCX", "yes")
|
|
1086
1246
|
except Exception:
|
|
1087
1247
|
print("DOCX", "no")
|
|
1248
|
+
try:
|
|
1249
|
+
import extract_msg, markdownify
|
|
1250
|
+
print("EMAIL", "yes")
|
|
1251
|
+
except Exception:
|
|
1252
|
+
print("EMAIL", "no")
|
|
1088
1253
|
`;
|
|
1089
1254
|
var PROBE_TIMEOUT_MS = 5e3;
|
|
1090
1255
|
function probeArgs() {
|
|
@@ -1092,8 +1257,8 @@ function probeArgs() {
|
|
|
1092
1257
|
}
|
|
1093
1258
|
var PYTHON_CANDIDATES = ["python3", "python"];
|
|
1094
1259
|
function parseProbeOutput(stdout) {
|
|
1095
|
-
const m = stdout.match(/^PY (\d+) (\d+)\r?\nPDF (yes|no)\r?\nXLSX (yes|no)\r?\nDOCX (yes|no)\s*$/);
|
|
1096
|
-
return m ? { major: Number(m[1]), minor: Number(m[2]), pdf: m[3] === "yes", xlsx: m[4] === "yes", docx: m[5] === "yes" } : null;
|
|
1260
|
+
const m = stdout.match(/^PY (\d+) (\d+)\r?\nPDF (yes|no)\r?\nXLSX (yes|no)\r?\nDOCX (yes|no)\r?\nEMAIL (yes|no)\s*$/);
|
|
1261
|
+
return m ? { major: Number(m[1]), minor: Number(m[2]), pdf: m[3] === "yes", xlsx: m[4] === "yes", docx: m[5] === "yes", email: m[6] === "yes" } : null;
|
|
1097
1262
|
}
|
|
1098
1263
|
function meetsFloor(p) {
|
|
1099
1264
|
return p.major > 3 || p.major === 3 && p.minor >= 12;
|
|
@@ -1118,7 +1283,7 @@ async function resolveBackend(cfg, deps, signal) {
|
|
|
1118
1283
|
};
|
|
1119
1284
|
const warm = await deps.run("uv", warmArgs(cfg), { timeoutMs: left(), capBytes: OUTPUT_MAX_BYTES, env: deps.env, signal });
|
|
1120
1285
|
if (signal?.aborted) throw new Error("aborted");
|
|
1121
|
-
if (warm.code === 0 && !warm.timedOut) return { kind: "uv", pdf: true, xlsx: true, docx: true };
|
|
1286
|
+
if (warm.code === 0 && !warm.timedOut) return { kind: "uv", pdf: true, xlsx: true, docx: true, email: true };
|
|
1122
1287
|
const uvAbsent = warm.code === null && !warm.timedOut;
|
|
1123
1288
|
const isDeadline = (result) => result !== null && "kind" in result;
|
|
1124
1289
|
const probe = async (exe, tmp) => {
|
|
@@ -1134,19 +1299,19 @@ async function resolveBackend(cfg, deps, signal) {
|
|
|
1134
1299
|
const p = await probe(exe);
|
|
1135
1300
|
if (isDeadline(p)) return p;
|
|
1136
1301
|
if (!p || !meetsFloor(p)) continue;
|
|
1137
|
-
if (p.pdf) return { kind: "python", exe, pdf: true, xlsx: p.xlsx, docx: p.docx };
|
|
1302
|
+
if (p.pdf) return { kind: "python", exe, pdf: true, xlsx: p.xlsx, docx: p.docx, email: p.email };
|
|
1138
1303
|
eligible ??= { exe, version: `${p.major}.${p.minor}` };
|
|
1139
1304
|
}
|
|
1140
|
-
const healthy = (p) => meetsFloor(p) && p.pdf && p.xlsx && p.docx;
|
|
1305
|
+
const healthy = (p) => meetsFloor(p) && p.pdf && p.xlsx && p.docx && p.email;
|
|
1141
1306
|
const venvDir = join3(deps.cacheRoot, VENV_DIR_NAME);
|
|
1142
1307
|
const venvExe = venvPython(venvDir, deps.platform);
|
|
1143
1308
|
const cached = await probe(venvExe);
|
|
1144
1309
|
if (isDeadline(cached)) return cached;
|
|
1145
|
-
if (cached && healthy(cached)) return { kind: "venv", exe: venvExe, pdf: true, xlsx: true, docx: true };
|
|
1310
|
+
if (cached && healthy(cached)) return { kind: "venv", exe: venvExe, pdf: true, xlsx: true, docx: true, email: true };
|
|
1146
1311
|
if (eligible) {
|
|
1147
1312
|
const recheck = await probe(venvExe);
|
|
1148
1313
|
if (isDeadline(recheck)) return recheck;
|
|
1149
|
-
if (recheck && healthy(recheck)) return { kind: "venv", exe: venvExe, pdf: true, xlsx: true, docx: true };
|
|
1314
|
+
if (recheck && healthy(recheck)) return { kind: "venv", exe: venvExe, pdf: true, xlsx: true, docx: true, email: true };
|
|
1150
1315
|
const tmp = `${venvDir}.tmp-${deps.pid}`;
|
|
1151
1316
|
const bootFail = (stderr) => {
|
|
1152
1317
|
deps.rmrf(tmp);
|
|
@@ -1172,18 +1337,18 @@ async function resolveBackend(cfg, deps, signal) {
|
|
|
1172
1337
|
};
|
|
1173
1338
|
if (publish()) {
|
|
1174
1339
|
for (const legacy of LEGACY_VENV_DIR_NAMES) deps.rmrf(join3(deps.cacheRoot, legacy));
|
|
1175
|
-
return { kind: "venv", exe: venvExe, pdf: true, xlsx: true, docx: true };
|
|
1340
|
+
return { kind: "venv", exe: venvExe, pdf: true, xlsx: true, docx: true, email: true };
|
|
1176
1341
|
}
|
|
1177
1342
|
const winner = await probe(venvExe, tmp);
|
|
1178
1343
|
if (isDeadline(winner)) return winner;
|
|
1179
1344
|
if (winner && healthy(winner)) {
|
|
1180
1345
|
deps.rmrf(tmp);
|
|
1181
|
-
return { kind: "venv", exe: venvExe, pdf: true, xlsx: true, docx: true };
|
|
1346
|
+
return { kind: "venv", exe: venvExe, pdf: true, xlsx: true, docx: true, email: true };
|
|
1182
1347
|
}
|
|
1183
1348
|
deps.rmrf(venvDir);
|
|
1184
1349
|
if (publish()) {
|
|
1185
1350
|
for (const legacy of LEGACY_VENV_DIR_NAMES) deps.rmrf(join3(deps.cacheRoot, legacy));
|
|
1186
|
-
return { kind: "venv", exe: venvExe, pdf: true, xlsx: true, docx: true };
|
|
1351
|
+
return { kind: "venv", exe: venvExe, pdf: true, xlsx: true, docx: true, email: true };
|
|
1187
1352
|
}
|
|
1188
1353
|
return bootFail("rename after competing bootstrap");
|
|
1189
1354
|
}
|
|
@@ -1247,6 +1412,76 @@ function officeFailure(r) {
|
|
|
1247
1412
|
return new Error(`soffice failed (code=${r.code} timedOut=${r.timedOut}): ${r.stderr}`);
|
|
1248
1413
|
}
|
|
1249
1414
|
var DEGRADED_TEXT = "PyMuPDF text extraction - layout/tables not preserved";
|
|
1415
|
+
var DEGRADED_HTML_TURNDOWN = "Turndown HTML conversion - definition lists and headerless tables not preserved";
|
|
1416
|
+
var DATA_IMAGE_RE = /^data:image\/(png|jpeg|gif|bmp|tiff);base64,([A-Za-z0-9+/=\s]+)$/i;
|
|
1417
|
+
var DATA_EXT = { png: ".png", jpeg: ".jpg", gif: ".gif", bmp: ".bmp", tiff: ".tif" };
|
|
1418
|
+
var DATA_SIGNATURES = {
|
|
1419
|
+
png: [[137, 80, 78, 71]],
|
|
1420
|
+
jpeg: [[255, 216, 255]],
|
|
1421
|
+
gif: [[71, 73, 70, 56]],
|
|
1422
|
+
bmp: [[66, 77]],
|
|
1423
|
+
tiff: [[73, 73, 42, 0], [77, 77, 0, 42]]
|
|
1424
|
+
};
|
|
1425
|
+
async function prepareHtml(inputPath, stagingDir) {
|
|
1426
|
+
const { JSDOM: JSDOM2 } = await import("jsdom");
|
|
1427
|
+
const bytes = readFileSync2(inputPath);
|
|
1428
|
+
let source = bytes;
|
|
1429
|
+
try {
|
|
1430
|
+
source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1431
|
+
} catch {
|
|
1432
|
+
}
|
|
1433
|
+
const dom = new JSDOM2(source);
|
|
1434
|
+
const doc = dom.window.document;
|
|
1435
|
+
const title = doc.querySelector("title")?.textContent?.trim();
|
|
1436
|
+
if (title && !doc.body.querySelector("h1")) {
|
|
1437
|
+
const h1 = doc.createElement("h1");
|
|
1438
|
+
h1.textContent = title;
|
|
1439
|
+
doc.body.prepend(h1);
|
|
1440
|
+
}
|
|
1441
|
+
for (const el of [...doc.querySelectorAll("head, script, style, noscript, template")]) el.remove();
|
|
1442
|
+
const dir = join3(stagingDir, "p1");
|
|
1443
|
+
mkdirSync3(dir, { recursive: true });
|
|
1444
|
+
let k = 0, missing = 0;
|
|
1445
|
+
for (const img of [...doc.querySelectorAll("img")]) {
|
|
1446
|
+
const src = (img.getAttribute("src") ?? "").trim();
|
|
1447
|
+
const alt = img.getAttribute("alt") ?? "";
|
|
1448
|
+
if (/^(?:https?:)?\/\//i.test(src)) {
|
|
1449
|
+
const a = doc.createElement("a");
|
|
1450
|
+
const url = src.startsWith("//") ? `https:${src}` : src;
|
|
1451
|
+
a.setAttribute("href", url);
|
|
1452
|
+
a.textContent = alt || url;
|
|
1453
|
+
img.replaceWith(a);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
let target = null;
|
|
1457
|
+
const data = src.match(DATA_IMAGE_RE);
|
|
1458
|
+
if (data) {
|
|
1459
|
+
const buf = Buffer.from(data[2].replace(/\s+/g, ""), "base64");
|
|
1460
|
+
if (DATA_SIGNATURES[data[1].toLowerCase()].some((signature) => signature.every((byte, i) => buf[i] === byte))) {
|
|
1461
|
+
target = `p1/${++k}${DATA_EXT[data[1].toLowerCase()]}`;
|
|
1462
|
+
writeFileSync3(join3(stagingDir, target), buf);
|
|
1463
|
+
}
|
|
1464
|
+
} else if (src && !/^[a-z][a-z0-9+.-]*:/i.test(src) && !isAbsolute(src)) {
|
|
1465
|
+
let file = null;
|
|
1466
|
+
try {
|
|
1467
|
+
file = resolve2(dirname(inputPath), decodeURIComponent(src.split(/[?#]/)[0]));
|
|
1468
|
+
} catch {
|
|
1469
|
+
}
|
|
1470
|
+
const ext = file ? extname3(file).toLowerCase() : "";
|
|
1471
|
+
if (file && IMAGE_EXTS.includes(ext) && statSync2(file, { throwIfNoEntry: false })?.isFile()) {
|
|
1472
|
+
target = `p1/${++k}${ext}`;
|
|
1473
|
+
copyFileSync(file, join3(stagingDir, target));
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (target) img.setAttribute("src", target);
|
|
1477
|
+
else {
|
|
1478
|
+
missing++;
|
|
1479
|
+
img.replaceWith(doc.createTextNode(alt));
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
writeFileSync3(join3(dir, ".done"), "");
|
|
1483
|
+
return { html: dom.serialize(), missing };
|
|
1484
|
+
}
|
|
1250
1485
|
var DEGRADED_UNPDF = "unpdf text extraction - structure not preserved";
|
|
1251
1486
|
var DEGRADED_DOCX_TEXT = "python-docx text extraction - footnotes, hyperlinks, images not preserved";
|
|
1252
1487
|
var DEGRADED_DOCX_OFFICE = "LibreOffice PDF route - heading styles and explicit page breaks not preserved; page numbers are LibreOffice pagination";
|
|
@@ -1312,26 +1547,96 @@ function reconcileRenderMarkers(md, renderPages, fmt, sourceMap, reason) {
|
|
|
1312
1547
|
if (/<!--rvs?:\d+-->/.test(md)) throw new Error("internal: unresolved render marker");
|
|
1313
1548
|
return md;
|
|
1314
1549
|
}
|
|
1550
|
+
var emptyOcr = (lang) => ({ status: "off", lang, textless: [], pages: [], noText: [], ocrFailed: [], budgetStopped: [], reason: null, tesseract: null });
|
|
1551
|
+
var OCR_SENTINEL_RE = /\x00OCR ([^\x00]*)\x00/g;
|
|
1552
|
+
function resolveOcrLabels(md, sourceMap) {
|
|
1553
|
+
return md.replace(OCR_SENTINEL_RE, (_, key) => {
|
|
1554
|
+
const dest = sourceMap.get(key);
|
|
1555
|
+
return dest ? `> Text recognized in ${dest} (OCR, may contain recognition errors):` : "> Text recognized by OCR (source image missing):";
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
function handleOcr(tier, type, o, json) {
|
|
1559
|
+
if (tier === "unpdf") return o.ocr ? { ...emptyOcr(o.ocrLanguage), status: "unavailable", reason: "no Python backend" } : null;
|
|
1560
|
+
const x = json.ocr;
|
|
1561
|
+
if (!x || type !== "image" && !x.textless.length && !x.pages.length && !x.ocrFailed.length) return null;
|
|
1562
|
+
return x;
|
|
1563
|
+
}
|
|
1315
1564
|
async function convertDocument(o, signal, seams) {
|
|
1316
1565
|
const s = { backend: (c) => getBackend(c, void 0, signal), runTier: runTierReal, office: tryConvertOffice, ...seams };
|
|
1317
1566
|
const inputPath = resolve2(o.path);
|
|
1318
1567
|
const st = statSync2(inputPath, { throwIfNoEntry: false });
|
|
1319
1568
|
if (!st || !st.isFile()) throw new Error(`Not a readable file: ${o.path}`);
|
|
1569
|
+
if (st.size === 0) throw new Error(`empty file: ${o.path}`);
|
|
1320
1570
|
const type = classifyInput(inputPath);
|
|
1321
|
-
|
|
1571
|
+
if (o.pages && (type === "html" || type === "image" || type === "email")) throw new Error(`--pages does not apply to ${type === "html" ? "HTML files" : type === "image" ? "images" : "email"}`);
|
|
1572
|
+
const isExcel = type === "xlsx" || type === "xlsm" || type === "xls";
|
|
1322
1573
|
if (isExcel && o.pages) throw new Error("--pages does not apply to spreadsheets: worksheets have no stable page numbering");
|
|
1323
1574
|
const backend = await s.backend({ pymupdfVersion: o.pymupdfVersion, warmTimeoutMs: o.warmTimeoutMs });
|
|
1324
1575
|
if (isExcel && (backend.kind === "none" || !backend.xlsx)) throw new Error(`Excel conversion needs a Python backend with openpyxl, xlrd and pillow (${backend.kind === "none" ? backend.reason : `${backend.kind} lacks the Excel packages`}). ${EXCEL_REMEDY}`);
|
|
1576
|
+
if (type === "email" && extname3(inputPath).toLowerCase() === ".msg" && (backend.kind === "none" || !backend.email)) throw new Error(`MSG conversion needs the extract-msg package. Python backend: ${backendState(backend, "found without extract-msg")}. Remedy: install uv, or pip install extract-msg markdownify into that Python`);
|
|
1577
|
+
if (type === "email" && extname3(inputPath).toLowerCase() !== ".msg" && lacksDocx(backend)) throw new Error(`EML conversion needs the Python DOCX/HTML packages (mammoth, markdownify, python-docx). Python backend: ${backendState(backend, "found without mammoth/markdownify/python-docx")}. ${docxRemedy}`);
|
|
1325
1578
|
const stem = sanitizeStem(basename(inputPath, extname3(inputPath)));
|
|
1326
1579
|
const b = openBundle(o.outputDir ? resolve2(o.outputDir) : tempBundleRoot(), stem, o.overwrite);
|
|
1327
1580
|
let office = null;
|
|
1328
1581
|
try {
|
|
1329
1582
|
let pdfPath = inputPath;
|
|
1330
|
-
const base = { path: inputPath, pages: o.pages, stagingDir: b.stagingDir, sheetsStagingDir: b.sheetsStagingDir, imageDpi: o.imageDpi, imageFormat: o.imageFormat, maxOutputBytes: o.maxOutputBytes, pymupdfVersion: o.pymupdfVersion };
|
|
1583
|
+
const base = { path: inputPath, pages: o.pages, stagingDir: b.stagingDir, sheetsStagingDir: b.sheetsStagingDir, pageImages: o.pageImages, pagesStagingDir: b.pagesStagingDir, imageDpi: o.imageDpi, imageFormat: o.imageFormat, maxOutputBytes: o.maxOutputBytes, pymupdfVersion: o.pymupdfVersion, ocr: o.ocr, ocrLanguage: o.ocrLanguage, ocrBudgetMs: o.primaryTimeoutMs };
|
|
1331
1584
|
let tier, engine, json, degraded = null, fallbackReason = null;
|
|
1332
1585
|
let explicitBreaks = null;
|
|
1333
1586
|
let notes = [];
|
|
1334
1587
|
let officeRoute = null;
|
|
1588
|
+
if (type === "html") {
|
|
1589
|
+
const prepared = await prepareHtml(inputPath, b.stagingDir);
|
|
1590
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
1591
|
+
if (prepared.missing) notes.push(`${prepared.missing} image(s) not found; replaced with alt text`);
|
|
1592
|
+
if (!lacksDocx(backend)) {
|
|
1593
|
+
const r = await s.runTier("html", { ...base, html: prepared.html }, b, signal, o.primaryTimeoutMs, backend);
|
|
1594
|
+
if (r.ok) {
|
|
1595
|
+
tier = "html";
|
|
1596
|
+
engine = "markdownify";
|
|
1597
|
+
json = r.json;
|
|
1598
|
+
} else if ("userError" in r) throw new Error(r.userError);
|
|
1599
|
+
else if (signal?.aborted || r.reason === "aborted") throw new Error("aborted");
|
|
1600
|
+
else fallbackReason = `html ${r.reason}${detailSuffix(r)}`;
|
|
1601
|
+
}
|
|
1602
|
+
if (json === void 0) {
|
|
1603
|
+
const { htmlToMarkdownRaw: htmlToMarkdownRaw2 } = await Promise.resolve().then(() => (init_fetch_core(), fetch_core_exports));
|
|
1604
|
+
tier = "html";
|
|
1605
|
+
engine = "turndown";
|
|
1606
|
+
degraded = DEGRADED_HTML_TURNDOWN;
|
|
1607
|
+
json = { markdown: `${htmlToMarkdownRaw2(prepared.html)}
|
|
1608
|
+
`, notes: [] };
|
|
1609
|
+
}
|
|
1610
|
+
publishStaged(b);
|
|
1611
|
+
}
|
|
1612
|
+
if (type === "image") {
|
|
1613
|
+
let reason = "no Python backend";
|
|
1614
|
+
if (backend.kind !== "none") {
|
|
1615
|
+
const r = await s.runTier("image", { ...base, stem: b.stem }, b, signal, o.primaryTimeoutMs, backend);
|
|
1616
|
+
if (r.ok) {
|
|
1617
|
+
publishStaged(b);
|
|
1618
|
+
tier = "image";
|
|
1619
|
+
engine = "pymupdf4llm";
|
|
1620
|
+
json = r.json;
|
|
1621
|
+
} else if ("userError" in r) throw new Error(r.userError);
|
|
1622
|
+
else if (signal?.aborted || r.reason === "aborted") throw new Error("aborted");
|
|
1623
|
+
else reason = `OCR child failed: ${r.reason}`;
|
|
1624
|
+
}
|
|
1625
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
1626
|
+
if (json === void 0) {
|
|
1627
|
+
clearStaging(b);
|
|
1628
|
+
const file = `original${extname3(inputPath).toLowerCase()}`;
|
|
1629
|
+
const dir = join3(b.stagingDir, "p1");
|
|
1630
|
+
mkdirSync3(dir, { recursive: true });
|
|
1631
|
+
copyFileSync(inputPath, join3(dir, file));
|
|
1632
|
+
writeFileSync3(join3(dir, ".done"), "");
|
|
1633
|
+
publishStaged(b);
|
|
1634
|
+
tier = "image";
|
|
1635
|
+
engine = "copy";
|
|
1636
|
+
json = { markdown: `
|
|
1637
|
+
`, pageCount: 1, notes: [], ocr: { ...emptyOcr(o.ocrLanguage), status: "unavailable", reason } };
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1335
1640
|
if (type === "docx" && !lacksDocx(backend)) {
|
|
1336
1641
|
const d = await s.runTier("docx", base, b, signal, o.primaryTimeoutMs, backend);
|
|
1337
1642
|
if (d.ok) {
|
|
@@ -1361,13 +1666,23 @@ async function convertDocument(o, signal, seams) {
|
|
|
1361
1666
|
office = r;
|
|
1362
1667
|
pdfPath = r.pdfPath;
|
|
1363
1668
|
}
|
|
1364
|
-
if (type === "pptx") {
|
|
1669
|
+
if (type === "doc" || type === "pptx") {
|
|
1365
1670
|
const r = await s.office(o.sofficeTimeoutMs, inputPath, signal);
|
|
1366
|
-
if (!r.ok && r.kind === "missing") throw new Error(
|
|
1671
|
+
if (!r.ok && r.kind === "missing") throw new Error(`${type.toUpperCase()} conversion needs LibreOffice (soffice); direct conversion is not available. Python backend: ${backendState(backend, "available")}. Remedy: install LibreOffice`);
|
|
1367
1672
|
if (!r.ok) throw officeFailure(r);
|
|
1368
1673
|
office = r;
|
|
1369
1674
|
pdfPath = r.pdfPath;
|
|
1370
1675
|
}
|
|
1676
|
+
if (type === "email") {
|
|
1677
|
+
const isMsg = extname3(inputPath).toLowerCase() === ".msg";
|
|
1678
|
+
const r = await s.runTier("email", { ...base, stem: b.stem, attachmentsStagingDir: b.attachmentsStagingDir }, b, signal, o.primaryTimeoutMs, backend);
|
|
1679
|
+
if (!r.ok) throw new Error("userError" in r ? r.userError : `Conversion failed: email ${r.reason}${detailSuffix(r)}`);
|
|
1680
|
+
publishStaged(b);
|
|
1681
|
+
publishAttachments(b);
|
|
1682
|
+
tier = "email";
|
|
1683
|
+
engine = isMsg ? "extract-msg" : "email";
|
|
1684
|
+
json = r.json;
|
|
1685
|
+
}
|
|
1371
1686
|
const pdfBase = { ...base, path: pdfPath };
|
|
1372
1687
|
if (json === void 0) {
|
|
1373
1688
|
if (isExcel) {
|
|
@@ -1382,7 +1697,7 @@ async function convertDocument(o, signal, seams) {
|
|
|
1382
1697
|
tier = "excel";
|
|
1383
1698
|
engine = type === "xls" ? "xlrd" : "openpyxl";
|
|
1384
1699
|
json = r.json;
|
|
1385
|
-
notes =
|
|
1700
|
+
notes = (json.notes ?? []).map((n) => n.replace(/sheets\/[^\s,;]+/g, (m) => b.sourceMap.get(m) ?? m));
|
|
1386
1701
|
const renderPages = json.renderPages ?? [];
|
|
1387
1702
|
let skip = null;
|
|
1388
1703
|
const perSheet = /* @__PURE__ */ new Map();
|
|
@@ -1421,12 +1736,15 @@ async function convertDocument(o, signal, seams) {
|
|
|
1421
1736
|
tier = "primary";
|
|
1422
1737
|
engine = "pymupdf4llm";
|
|
1423
1738
|
json = p.json;
|
|
1739
|
+
if (json.pageImages?.length) publishPageImages(b, json.pageCount ?? 0);
|
|
1424
1740
|
} else if ("userError" in p) throw new Error(p.userError);
|
|
1425
1741
|
else {
|
|
1426
1742
|
if (signal?.aborted) throw new Error("aborted");
|
|
1427
1743
|
const keepPages = Object.fromEntries([...kept.entries()].map(([k, v]) => [String(k), v]));
|
|
1744
|
+
rmSync2(b.pagesStagingDir, { recursive: true, force: true });
|
|
1428
1745
|
const f = await s.runTier("pdf-fallback", { ...pdfBase, keepPages }, b, signal, o.fallbackTimeoutMs, backend);
|
|
1429
1746
|
publishStaged(b);
|
|
1747
|
+
if (f.ok && f.json.pageImages?.length) publishPageImages(b, f.json.pageCount ?? 0);
|
|
1430
1748
|
if (!f.ok) throw new Error("userError" in f ? f.userError : `Conversion failed: primary ${p.reason}; fallback ${f.reason}${detailSuffix(f)}`);
|
|
1431
1749
|
tier = "fallback";
|
|
1432
1750
|
engine = "pymupdf-text";
|
|
@@ -1436,14 +1754,14 @@ async function convertDocument(o, signal, seams) {
|
|
|
1436
1754
|
}
|
|
1437
1755
|
}
|
|
1438
1756
|
}
|
|
1439
|
-
if (officeRoute !== null)
|
|
1440
|
-
|
|
1441
|
-
fallbackReason = fallbackReason ? `${officeRoute}; ${fallbackReason}` : officeRoute;
|
|
1442
|
-
}
|
|
1757
|
+
if (type === "doc" || officeRoute !== null) degraded = DEGRADED_DOCX_OFFICE;
|
|
1758
|
+
if (officeRoute !== null) fallbackReason = fallbackReason ? `${officeRoute}; ${fallbackReason}` : officeRoute;
|
|
1443
1759
|
if (tier === void 0 || engine === void 0 || json === void 0) throw new Error("internal: no tier produced output");
|
|
1444
|
-
if (!isExcel) notes = json.notes ?? [];
|
|
1445
|
-
|
|
1446
|
-
|
|
1760
|
+
if (!isExcel) notes = [...notes, ...json.notes ?? []];
|
|
1761
|
+
if (b.renamedFrom) notes.splice(notes[0]?.startsWith("preview truncated:") ? 1 : 0, 0, `renamed to ${b.stem} (${b.renameReason})`);
|
|
1762
|
+
const pageImagesReason = !o.pageImages || b.pageManifest.size || tier === "primary" || tier === "fallback" ? null : tier === "unpdf" ? "page images need the Python backend" : `${type} has no page geometry`;
|
|
1763
|
+
const body = resolveOcrLabels(rewriteLinks(json.markdown ?? "", b.sourceMap), b.sourceMap);
|
|
1764
|
+
validateImageLinks(body, b.manifest, b.csvManifest, type === "html" || type === "email", b.pageManifest, b.attachmentManifest);
|
|
1447
1765
|
const head = [];
|
|
1448
1766
|
if (degraded) head.push(`Degraded: ${degraded}`);
|
|
1449
1767
|
if (fallbackReason) head.push(`Fallback-Reason: ${fallbackReason}`);
|
|
@@ -1455,7 +1773,7 @@ async function convertDocument(o, signal, seams) {
|
|
|
1455
1773
|
` : "") + body;
|
|
1456
1774
|
commitBundle(b, markdown);
|
|
1457
1775
|
const outline = scanOutline(markdown, o.outlineMaxEntries);
|
|
1458
|
-
const details = { path: inputPath, backend: backend.kind, pymupdfVersion: o.pymupdfVersion, inputType: type, file: b.mdPath, outputDir: b.root, savedTo: b.mdPath, imagesDir: b.imagesDir, sheetsDir: b.csvManifest.size ? b.sheetsDir : null, type, engine, tier, pageCount: json.pageCount ?? null, pages: o.pages, explicitBreaks, imageCount: b.manifest.size, bytes: Buffer.byteLength(markdown, "utf8"), lines: markdown.split("\n").length, degraded, fallbackReason, failedPages: (json.failedPages ?? []).map((f) => f.page), emptyPages: json.emptyPages ?? [], notes, outline: outline.entries, outlineTotal: outline.total };
|
|
1776
|
+
const details = { path: inputPath, backend: backend.kind, pymupdfVersion: o.pymupdfVersion, inputType: type, file: b.mdPath, outputDir: b.root, savedTo: b.mdPath, imagesDir: b.imagesDir, sheetsDir: b.csvManifest.size ? b.sheetsDir : null, pagesDir: b.pageManifest.size ? b.pagesDir : null, pageImageCount: b.pageManifest.size, pageImagesReason, type, engine, tier, pageCount: json.pageCount ?? null, pages: o.pages, explicitBreaks, imageCount: b.manifest.size, bytes: Buffer.byteLength(markdown, "utf8"), lines: markdown.split("\n").length, degraded, fallbackReason, failedPages: (json.failedPages ?? []).map((f) => f.page), emptyPages: json.emptyPages ?? [], notes, outline: outline.entries, outlineTotal: outline.total, ocr: handleOcr(tier, type, o, json) };
|
|
1459
1777
|
return { output: formatHandle(details), details };
|
|
1460
1778
|
} catch (e) {
|
|
1461
1779
|
abortBundle(b);
|
|
@@ -1469,8 +1787,10 @@ async function inspectDocument(o, signal, seams) {
|
|
|
1469
1787
|
const inputPath = resolve2(o.path);
|
|
1470
1788
|
const st = statSync2(inputPath, { throwIfNoEntry: false });
|
|
1471
1789
|
if (!st || !st.isFile()) throw new Error(`Not a readable file: ${o.path}`);
|
|
1790
|
+
if (st.size === 0) throw new Error(`empty file: ${o.path}`);
|
|
1472
1791
|
const type = classifyInput(inputPath);
|
|
1473
|
-
|
|
1792
|
+
if (type === "html" || type === "image" || type === "email") throw new Error(`info does not apply to ${type === "html" ? "HTML files" : type === "image" ? "images" : "email"}; convert directly`);
|
|
1793
|
+
const isExcel = type === "xlsx" || type === "xlsm" || type === "xls";
|
|
1474
1794
|
const backend = await s.backend({ pymupdfVersion: o.pymupdfVersion, warmTimeoutMs: o.warmTimeoutMs });
|
|
1475
1795
|
if (isExcel && (backend.kind === "none" || !backend.xlsx)) throw new Error(`Excel inspection needs a Python backend with openpyxl, xlrd and pillow. ${EXCEL_REMEDY}`);
|
|
1476
1796
|
let office = null;
|
|
@@ -1478,7 +1798,7 @@ async function inspectDocument(o, signal, seams) {
|
|
|
1478
1798
|
let path = inputPath;
|
|
1479
1799
|
if (type === "docx") {
|
|
1480
1800
|
if (lacksDocx(backend)) throw new Error(`DOCX inspection needs the Python DOCX packages. Python backend: ${backendState(backend, "found without mammoth/markdownify/python-docx")}. ${docxRemedy}`);
|
|
1481
|
-
} else if (type === "pptx") {
|
|
1801
|
+
} else if (type === "pptx" || type === "doc") {
|
|
1482
1802
|
const r2 = await s.office(o.sofficeTimeoutMs, inputPath, signal);
|
|
1483
1803
|
if (!r2.ok) throw officeFailure(r2);
|
|
1484
1804
|
office = r2;
|
|
@@ -1499,25 +1819,32 @@ async function inspectDocument(o, signal, seams) {
|
|
|
1499
1819
|
}
|
|
1500
1820
|
|
|
1501
1821
|
// bin/pi-quiver.ts
|
|
1502
|
-
var USAGE = 'Usage: pi-quiver fetch <url> [--method GET|HEAD|POST] [--header "K: V"]... [--body <str>] [--raw] [--timeout-ms <n>]\n pi-quiver doc-to-md [--info] [--pages <spec>] [--output-dir <dir>] [--overwrite] [tunable flags] <path> (--help for all flags)';
|
|
1822
|
+
var USAGE = 'Usage: pi-quiver fetch <url> [--method GET|HEAD|POST] [--header "K: V"]... [--body <str>] [--raw] [--timeout-ms <n>]\n pi-quiver doc-to-md [--json] [--info] [--page-images] [--pages <spec>] [--output-dir <dir>] [--overwrite] [tunable flags] <path> (--help for all flags)';
|
|
1503
1823
|
function parseDocToMd(rest) {
|
|
1504
1824
|
if (rest.includes("--help") || rest.includes("-h")) return { ok: true, cmd: "doc-to-md-help" };
|
|
1825
|
+
const json = rest.includes("--json");
|
|
1826
|
+
const args = rest.filter((a) => a !== "--json");
|
|
1505
1827
|
const perCall = {};
|
|
1506
1828
|
let path;
|
|
1507
|
-
for (let i = 0; i <
|
|
1508
|
-
const arg =
|
|
1829
|
+
for (let i = 0; i < args.length; i++) {
|
|
1830
|
+
const arg = args[i];
|
|
1509
1831
|
if (!arg.startsWith("--")) {
|
|
1510
1832
|
if (path !== void 0) return { ok: false, error: `unexpected argument: ${arg}` };
|
|
1511
1833
|
path = arg;
|
|
1512
1834
|
continue;
|
|
1513
1835
|
}
|
|
1836
|
+
const negated = arg.startsWith("--no-") ? DOC_TO_MD_OPTIONS.find((o) => o.type === "bool" && o.settable && o.flag === `--${arg.slice(5)}`) : void 0;
|
|
1837
|
+
if (negated) {
|
|
1838
|
+
perCall[negated.key] = false;
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1514
1841
|
const d = DOC_TO_MD_OPTIONS.find((o) => o.flag === arg);
|
|
1515
1842
|
if (!d) return { ok: false, error: `unknown flag: ${arg}` };
|
|
1516
1843
|
if (d.type === "bool") {
|
|
1517
1844
|
perCall[d.key] = true;
|
|
1518
1845
|
continue;
|
|
1519
1846
|
}
|
|
1520
|
-
const value =
|
|
1847
|
+
const value = args[++i];
|
|
1521
1848
|
if (value === void 0) return { ok: false, error: `${arg} requires a value` };
|
|
1522
1849
|
if (d.type === "int") {
|
|
1523
1850
|
const n = Number(value);
|
|
@@ -1526,7 +1853,7 @@ function parseDocToMd(rest) {
|
|
|
1526
1853
|
} else perCall[d.key] = value;
|
|
1527
1854
|
}
|
|
1528
1855
|
if (!path) return { ok: false, error: "missing <path>" };
|
|
1529
|
-
return { ok: true, cmd: "doc-to-md", perCall: { ...perCall, path } };
|
|
1856
|
+
return { ok: true, cmd: "doc-to-md", perCall: { ...perCall, path }, json };
|
|
1530
1857
|
}
|
|
1531
1858
|
function cliAgentDir(env) {
|
|
1532
1859
|
const raw = env.PI_CODING_AGENT_DIR;
|
|
@@ -1544,7 +1871,7 @@ function readCliSettings(cwd, env, warn) {
|
|
|
1544
1871
|
if (!existsSync3(file)) continue;
|
|
1545
1872
|
let raw;
|
|
1546
1873
|
try {
|
|
1547
|
-
raw = JSON.parse(
|
|
1874
|
+
raw = JSON.parse(readFileSync3(file, "utf8")).quiver?.docToMd;
|
|
1548
1875
|
} catch {
|
|
1549
1876
|
warn(`pi-quiver: ${file} is not valid JSON; ignored.`);
|
|
1550
1877
|
continue;
|
|
@@ -1639,7 +1966,11 @@ ${USAGE}
|
|
|
1639
1966
|
}
|
|
1640
1967
|
try {
|
|
1641
1968
|
const r = o.info ? await inspectDocument(o) : await convertDocument(o);
|
|
1642
|
-
|
|
1969
|
+
if (parsed.json) {
|
|
1970
|
+
const payload = o.info ? r.details : (({ path, backend, pymupdfVersion, inputType, file, outputDir, ...handle }) => handle)(r.details);
|
|
1971
|
+
process.stdout.write(`${JSON.stringify(payload)}
|
|
1972
|
+
`);
|
|
1973
|
+
} else process.stdout.write(`${r.output}
|
|
1643
1974
|
`);
|
|
1644
1975
|
return 0;
|
|
1645
1976
|
} catch (err) {
|