thincoder 0.12.59 → 0.12.60
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 +38 -3
- package/README.md +2 -2
- package/bin/thincoder.mjs +80 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +7 -4
- package/src/advisor/messages.mjs +24 -4
- package/src/advisor/run.mjs +35 -33
- package/src/advisor.mjs +25 -6
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +102 -19
- package/src/agent/helpers.mjs +36 -0
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +18 -2
- package/src/agent/spawn-child.mjs +29 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +110 -108
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +121 -102
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/read-history.mjs +155 -31
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +7 -17
- package/src/agent-tools/subagent-actions.mjs +168 -130
- package/src/agent-tools/subagent-async.mjs +129 -174
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +45 -21
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +107 -555
- package/src/agent-tools/verify.mjs +118 -270
- package/src/agent.mjs +57 -190
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +3 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +56 -8
- package/src/context.mjs +5 -147
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +11 -11
- package/src/explore-distill.mjs +155 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +58 -48
- package/src/memory.mjs +3 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +9 -76
- package/src/prompts/advisor-round1.md +9 -68
- package/src/prompts/advisor-round2.md +7 -54
- package/src/prompts/advisor-round3.md +7 -54
- package/src/prompts/coder.md +7 -50
- package/src/prompts/consult-base.md +4 -24
- package/src/prompts/discipline.md +26 -44
- package/src/prompts/eng-coder.md +7 -32
- package/src/prompts/engineering-sub.md +3 -23
- package/src/prompts/engineering.md +53 -306
- package/src/prompts/explore.md +3 -12
- package/src/prompts/main.md +10 -32
- package/src/prompts/methodology-template.md +28 -48
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +16 -35
- package/src/provider/core.mjs +6 -67
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +11 -6
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +5 -6
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +3 -0
- package/src/tools/read.md +0 -1
- package/src/tools/shared.mjs +14 -13
- package/src/tools/system.mjs +44 -9
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +17 -228
- package/src/tui/cmd-config.mjs +48 -7
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +3 -2
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/mouse.mjs +7 -1
- package/src/tui/pickers.mjs +40 -22
- package/src/tui/render-segments.mjs +27 -10
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +95 -263
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +61 -23
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +3 -3
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +37 -173
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +12 -6
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
|
@@ -1,497 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pdf-parse-text.mjs — read_pdf text-layer core: page tree, content-stream operator walk,
|
|
3
|
-
* glyph decoding (ToUnicode CMap → WinAnsi/Standard/MacRoman + /Differences), (x,y) layout
|
|
4
|
-
* with light x-cluster columns (TOOLS.md §11.2 stages 5/7/8/9, §11.3.3). Tables cross-checked vs Unicode.org + pdf.js.
|
|
5
|
-
*/
|
|
6
|
-
import { PdfError, nextToken, skipWs, decodeStreamBytes } from "./pdf-parse-xref.mjs";
|
|
7
|
-
|
|
8
|
-
const MAX_PAGE_NODES = 10_000;
|
|
9
|
-
const MAX_FORM_DEPTH = 16;
|
|
10
|
-
|
|
11
|
-
// ── page tree (stage 5) ─────────────────────────────
|
|
12
|
-
function collectPageRefs(doc, ref, out, seen) {
|
|
13
|
-
if (out.length > MAX_PAGE_NODES) throw new PdfError("page tree exceeds safety limit");
|
|
14
|
-
const key = ref.r.join(",");
|
|
15
|
-
if (seen.has(key)) throw new PdfError("cyclic page tree");
|
|
16
|
-
seen.add(key);
|
|
17
|
-
const node = doc.resolve(ref);
|
|
18
|
-
if (!node || typeof node !== "object") throw new PdfError(`page-tree node ${ref.r[0]} unreadable`);
|
|
19
|
-
if (node.Type === "Page" || (node.Type === undefined && node.Kids === undefined && node.Contents !== undefined)) out.push(ref);
|
|
20
|
-
else if (Array.isArray(node.Kids)) for (const k of node.Kids) collectPageRefs(doc, k, out, new Set([...seen]));
|
|
21
|
-
else throw new PdfError(`page-tree node ${ref.r[0]} (${node.Type || "?"}) has no /Kids`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Merge /Resources down the parent chain (page → root Pages), child wins per key. */
|
|
25
|
-
function pageResources(doc, pageRef) {
|
|
26
|
-
const merged = {};
|
|
27
|
-
const chain = [];
|
|
28
|
-
let ref = pageRef, depth = 0;
|
|
29
|
-
while (ref && depth++ < 64) {
|
|
30
|
-
const node = doc.resolve(ref);
|
|
31
|
-
if (!node || typeof node !== "object") break;
|
|
32
|
-
chain.unshift(node);
|
|
33
|
-
ref = node.Parent && node.Parent.r ? node.Parent : null;
|
|
34
|
-
}
|
|
35
|
-
for (const node of chain) {
|
|
36
|
-
if (node.Resources && typeof node.Resources === "object" && !node.Resources.r) {
|
|
37
|
-
const r = doc.resolve(node.Resources);
|
|
38
|
-
for (const k of Object.keys(r)) if (!(k in merged)) merged[k] = r[k];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return merged;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ── encoding tables (stage 8 fallbacks) ─────────────
|
|
45
|
-
const WINANSI_OVERRIDES = { 0x80: "\u20AC", 0x82: "\u201A", 0x83: "\u0192", 0x84: "\u201E", 0x85: "\u2026", 0x86: "\u2020", 0x87: "\u2021", 0x88: "\u02C6", 0x89: "\u2030", 0x8A: "\u0160", 0x8B: "\u2039", 0x8C: "\u0152", 0x8E: "\u017D", 0x91: "\u2018", 0x92: "\u2019", 0x93: "\u201C", 0x94: "\u201D", 0x95: "\u2022", 0x96: "\u2013", 0x97: "\u2014", 0x98: "\u02DC", 0x99: "\u2122", 0x9A: "\u0161", 0x9B: "\u203A", 0x9C: "\u0153", 0x9E: "\u017E", 0x9F: "\u0178" };
|
|
46
|
-
const UNDEFINED = new Set([0x81, 0x8D, 0x8F, 0x90, 0x9D]);
|
|
47
|
-
|
|
48
|
-
const MACROMAN_HIGH = "\u00C4\u00C5\u00C7\u00C9\u00D1\u00D6\u00DC\u00E1\u00E0\u00E2\u00E4\u00E3\u00E5\u00E7\u00E9\u00E8\u00EA\u00EB\u00ED\u00EC\u00EE\u00EF\u00F1\u00F3\u00F2\u00F4\u00F6\u00F5\u00FA\u00F9\u00FB\u00FC\u2020\u00B0\u00A2\u00A3\u00A7\u2022\u00B6\u00DF\u00AE\u00A9\u2122\u00B4\u00A8\u2260\u00C6\u00D8\u221E\u00B1\u2264\u2265\u00A5\u00B5\u2202\u2211\u220F\u03C0\u222B\u00AA\u00BA\u03A9\u00E6\u00F8\u00BF\u00A1\u00AC\u221A\u0192\u2248\u2206\u00AB\u00BB\u2026\u00A0\u00C0\u00C3\u00D5\u0152\u0153\u2013\u2014\u201C\u201D\u2018\u2019\u00F7\u25CA\u00FF\u0178\u2044\u00A4\u2039\u203A\uFB01\uFB02\u2021\u00B7\u201A\u201E\u2030\u00C2\u00CA\u00C1\u00CB\u00C8\u00CD\u00CE\u00CF\u00CC\u00D3\u00D4\uF8FF\u00D2\u00DA\u00DB\u00D9\u0131\u02C6\u02DC\u00AF\u02D8\u02D9\u02DA\u00B8\u02DD\u02DB\u02C7";
|
|
49
|
-
|
|
50
|
-
const STANDARD_HIGH = { 0xA1: "exclamdown", 0xA2: "cent", 0xA3: "sterling", 0xA4: "fraction", 0xA5: "yen", 0xA6: "florin", 0xA7: "section", 0xA8: "currency", 0xA9: "quotesingle", 0xAA: "quotedblleft", 0xAB: "guillemotleft", 0xAC: "guilsinglleft", 0xAD: "guilsinglright", 0xAE: "fi", 0xAF: "fl", 0xB1: "endash", 0xB2: "dagger", 0xB3: "daggerdbl", 0xB4: "periodcentered", 0xB6: "paragraph", 0xB7: "bullet", 0xB8: "quotesinglbase", 0xB9: "quotedblbase", 0xBA: "quotedblright", 0xBB: "guillemotright", 0xBC: "ellipsis", 0xBD: "perthousand", 0xBF: "questiondown", 0xC1: "grave", 0xC2: "acute", 0xC3: "circumflex", 0xC4: "tilde", 0xC5: "macron", 0xC6: "breve", 0xC7: "dotaccent", 0xC8: "dieresis", 0xCA: "ring", 0xCB: "cedilla", 0xCD: "hungarumlaut", 0xCE: "ogonek", 0xCF: "caron", 0xD0: "emdash", 0xE1: "AE", 0xE3: "ordfeminine", 0xE8: "Lslash", 0xE9: "Oslash", 0xEA: "OE", 0xEB: "ordmasculine", 0xF1: "ae", 0xF5: "dotlessi", 0xF8: "lslash", 0xF9: "oslash", 0xFA: "oe", 0xFB: "germandbls" };
|
|
51
|
-
const GLYPH_CHAR = { exclamdown: "\u00A1", cent: "\u00A2", sterling: "\u00A3", fraction: "\u2044", yen: "\u00A5", florin: "\u0192", section: "\u00A7", currency: "\u00A4", quotesingle: "'", quotedblleft: "\u201C", guillemotleft: "\u00AB", guilsinglleft: "\u2039", guilsinglright: "\u203A", fi: "\uFB01", fl: "\uFB02", endash: "\u2013", dagger: "\u2020", daggerdbl: "\u2021", periodcentered: "\u00B7", paragraph: "\u00B6", bullet: "\u2022", quotesinglbase: "\u201A", quotedblbase: "\u201E", quotedblright: "\u201D", guillemotright: "\u00BB", ellipsis: "\u2026", perthousand: "\u2030", questiondown: "\u00BF", grave: "`", acute: "\u00B4", circumflex: "\u02C6", tilde: "\u02DC", macron: "\u00AF", breve: "\u02D8", dotaccent: "\u02D9", dieresis: "\u00A8", ring: "\u02DA", cedilla: "\u00B8", hungarumlaut: "\u02DD", ogonek: "\u02DB", caron: "\u02C7", emdash: "\u2014", AE: "\u00C6", ordfeminine: "\u00AA", Lslash: "\u0141", Oslash: "\u00D8", OE: "\u0152", ordmasculine: "\u00BA", ae: "\u00E6", dotlessi: "\u0131", lslash: "\u0142", oslash: "\u00F8", oe: "\u0153", germandbls: "\u00DF", quoteright: "\u2019", quoteleft: "\u2018", space: " " };
|
|
52
|
-
const SYMBOLIC = /^([A-Z]{6}\+)?(Symbol|ZapfDingbats)$/;
|
|
53
|
-
|
|
54
|
-
function decodeUTF16BE(b) {
|
|
55
|
-
try { return new TextDecoder("utf-16be").decode(b); }
|
|
56
|
-
catch {
|
|
57
|
-
let s = "";
|
|
58
|
-
for (let i = 0; i + 1 < b.length; i += 2) s += String.fromCharCode((b[i] << 8) | b[i + 1]);
|
|
59
|
-
return s;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** ActualText / a PDF text string: UTF-16BE (BOM) or UTF-8 (BOM), else latin1. */
|
|
64
|
-
function decodeActual(b) {
|
|
65
|
-
if (b.length >= 2 && b[0] === 0xfe && b[1] === 0xff) return decodeUTF16BE(b.slice(2));
|
|
66
|
-
if (b.length >= 3 && b[0] === 0xef && b[1] === 0xbb && b[2] === 0xbf) return new TextDecoder("utf-8").decode(b.slice(3));
|
|
67
|
-
if (b.length >= 4 && b[0] === 0 && b[1] !== 0) return decodeUTF16BE(b);
|
|
68
|
-
return b.toString("latin1");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Content-stream property dict (BDC operands) — flat top level, nested values skipped. */
|
|
72
|
-
function parseDictInline(data, p) {
|
|
73
|
-
const dict = {};
|
|
74
|
-
for (;;) {
|
|
75
|
-
const t = nextToken(data, p);
|
|
76
|
-
if (!t.tok) throw new PdfError("unterminated inline dict");
|
|
77
|
-
p = t.p;
|
|
78
|
-
if (t.tok.t === ">>") return { dict, after: p };
|
|
79
|
-
if (t.tok.t !== "name") continue;
|
|
80
|
-
const v = nextToken(data, p);
|
|
81
|
-
if (!v.tok) throw new PdfError("unterminated inline dict value");
|
|
82
|
-
p = v.p;
|
|
83
|
-
if (v.tok.t === "str" || v.tok.t === "hex") dict[t.tok.s] = { b: v.tok.buf };
|
|
84
|
-
else if (v.tok.t === "num") dict[t.tok.s] = v.tok.n;
|
|
85
|
-
else if (v.tok.t === "name") dict[t.tok.s] = v.tok.s;
|
|
86
|
-
else if (v.tok.t === "true" || v.tok.t === "false") dict[t.tok.s] = v.tok.t === "true";
|
|
87
|
-
else if (v.tok.t === "[" || v.tok.t === "<<") { // nested — skip to matching closer
|
|
88
|
-
const close = v.tok.t === "[" ? "]" : ">>";
|
|
89
|
-
let depth = 1;
|
|
90
|
-
while (depth > 0) {
|
|
91
|
-
const t2 = nextToken(data, p);
|
|
92
|
-
if (!t2.tok) break;
|
|
93
|
-
p = t2.p;
|
|
94
|
-
if (t2.tok.t === v.tok.t) depth++;
|
|
95
|
-
else if (t2.tok.t === close) depth--;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ── CMap (ToUnicode: codespaces / bfchar / bfrange) ──
|
|
102
|
-
function parseCMap(data) {
|
|
103
|
-
const single = new Map();
|
|
104
|
-
const ranges = [];
|
|
105
|
-
let codeWidth = 0, p = 0;
|
|
106
|
-
const codeOf = (tok) => { if (!tok) throw new PdfError("CMap truncated"); return tok.buf.length === 1 ? tok.buf[0] : (tok.buf[0] << 8) | (tok.buf[1] || 0); };
|
|
107
|
-
const utf16 = (b) => b.length >= 2 ? decodeUTF16BE(b) : String.fromCharCode(b[0]);
|
|
108
|
-
while (p < data.length) {
|
|
109
|
-
const { tok, p: np } = nextToken(data, p);
|
|
110
|
-
if (!tok) throw new PdfError("CMap truncated");
|
|
111
|
-
p = np; // always advance — non-section keywords must not stall the loop
|
|
112
|
-
if (tok.t !== "kw") continue;
|
|
113
|
-
if (tok.s === "begincodespacerange") {
|
|
114
|
-
const t1 = nextToken(data, p);
|
|
115
|
-
const lo = t1.tok && t1.tok.buf ? t1.tok.buf.length : 0;
|
|
116
|
-
if (lo === 1) codeWidth = 1;
|
|
117
|
-
else if (lo === 2) codeWidth = 2;
|
|
118
|
-
}
|
|
119
|
-
if (tok.s !== "beginbfchar" && tok.s !== "beginbfrange") continue;
|
|
120
|
-
for (;;) {
|
|
121
|
-
const t1 = nextToken(data, skipWs(data, p));
|
|
122
|
-
if (!t1.tok) throw new PdfError("CMap unterminated");
|
|
123
|
-
if (t1.tok.t === "kw" && /^end/.test(t1.tok.s)) { p = t1.p; break; }
|
|
124
|
-
const t2 = nextToken(data, t1.p);
|
|
125
|
-
const t3 = nextToken(data, t2.p);
|
|
126
|
-
if (tok.s === "beginbfchar") { // <src> <dst>
|
|
127
|
-
single.set(codeOf(t1.tok), utf16(t2.tok.buf));
|
|
128
|
-
p = t2.p;
|
|
129
|
-
} else if (t3.tok && t3.tok.t === "[") { // <lo> <hi> [dst …]
|
|
130
|
-
const lo = codeOf(t1.tok), hi = codeOf(t2.tok);
|
|
131
|
-
let q = t3.p, code = lo;
|
|
132
|
-
for (;;) {
|
|
133
|
-
const dt = nextToken(data, q);
|
|
134
|
-
if (!dt.tok || dt.tok.t === "]") { q = dt.p; break; }
|
|
135
|
-
if (dt.tok.t === "str" || dt.tok.t === "hex") single.set(code, utf16(dt.tok.buf));
|
|
136
|
-
code++; q = dt.p;
|
|
137
|
-
if (code > hi) break;
|
|
138
|
-
}
|
|
139
|
-
p = q;
|
|
140
|
-
} else { // <lo> <hi> <dst>
|
|
141
|
-
const lo = codeOf(t1.tok), hi = codeOf(t2.tok), dst = t3.tok.buf;
|
|
142
|
-
if (hi < lo) throw new PdfError("CMap bfrange reversed");
|
|
143
|
-
if (dst.length <= 2) ranges.push({ lo, hi, base: dst.length === 1 ? dst[0] : (dst[0] << 8) | dst[1] });
|
|
144
|
-
else single.set(lo, utf16(dst)); // 4+ byte dst: single-char range only
|
|
145
|
-
p = t3.p;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return { single, ranges, codeWidth };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ── fonts: resource dict → decode plan ──────────────
|
|
153
|
-
function loadFont(doc, fontValue) {
|
|
154
|
-
const f = doc.resolve(fontValue);
|
|
155
|
-
if (!f || typeof f !== "object") return null;
|
|
156
|
-
const info = { type0: f.Subtype === "Type0", baseFont: f.BaseFont, cmap: null, enc: null, diffs: null, widths: null, firstChar: 0, wRanges: null, dw: 1000, symbolic: false, notes: new Set() };
|
|
157
|
-
const readTu = (ref) => {
|
|
158
|
-
try {
|
|
159
|
-
const tu = doc.resolve(ref);
|
|
160
|
-
if (tu && tu.stream) info.cmap = parseCMap(decodeStreamBytes(tu.stream, tu.dict));
|
|
161
|
-
} catch { info.cmap = info.cmap || null; } // unreadable ToUnicode → fall through to encodings
|
|
162
|
-
};
|
|
163
|
-
if (f.ToUnicode && f.ToUnicode.r) readTu(f.ToUnicode);
|
|
164
|
-
if (info.type0) {
|
|
165
|
-
const desc = f.DescendantFonts?.[0] ? doc.resolve(f.DescendantFonts[0]) : null;
|
|
166
|
-
if (desc && typeof desc === "object") {
|
|
167
|
-
if (Array.isArray(desc.W)) info.wRanges = parseWRanges(desc.W);
|
|
168
|
-
if (typeof desc.DW === "number") info.dw = desc.DW;
|
|
169
|
-
if (!info.cmap && desc.ToUnicode && desc.ToUnicode.r) readTu(desc.ToUnicode);
|
|
170
|
-
}
|
|
171
|
-
const encName = typeof f.Encoding === "string" ? f.Encoding : f.Encoding?.BaseEncoding;
|
|
172
|
-
info.identity = /Identity-[HV]/.test(encName || "");
|
|
173
|
-
} else {
|
|
174
|
-
if (Array.isArray(f.Widths)) { info.widths = f.Widths; info.firstChar = f.FirstChar || 0; }
|
|
175
|
-
const enc = f.Encoding;
|
|
176
|
-
if (typeof enc === "string") info.enc = enc === "MacRomanEncoding" ? "macRoman" : enc === "StandardEncoding" ? "standard" : "winAnsi";
|
|
177
|
-
else if (enc && typeof enc === "object" && !enc.r) {
|
|
178
|
-
info.enc = enc.BaseEncoding === "MacRomanEncoding" ? "macRoman" : enc.BaseEncoding === "StandardEncoding" ? "standard" : "winAnsi";
|
|
179
|
-
if (Array.isArray(enc.Differences)) {
|
|
180
|
-
info.diffs = new Map();
|
|
181
|
-
let code = null;
|
|
182
|
-
for (const d of enc.Differences) { if (typeof d === "number") code = d; else if (code !== null) info.diffs.set(code++, d); }
|
|
183
|
-
}
|
|
184
|
-
} else info.enc = f.Subtype === "Type1" ? "standard" : "winAnsi"; // no /Encoding: base-14 ≈ Standard, TrueType ≈ WinAnsi
|
|
185
|
-
info.symbolic = !info.cmap && SYMBOLIC.test(info.baseFont || "") && !info.diffs;
|
|
186
|
-
if (!info.cmap && f.Subtype === "Type3") info.notes.add("type3-no-tounicode"); // codes are charproc indices — best-effort only
|
|
187
|
-
}
|
|
188
|
-
return info;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function parseWRanges(W) {
|
|
192
|
-
const out = [];
|
|
193
|
-
for (let i = 0; i < W.length; i++) {
|
|
194
|
-
const c = W[i];
|
|
195
|
-
if (typeof c !== "number") continue;
|
|
196
|
-
const n2 = W[i + 1];
|
|
197
|
-
if (Array.isArray(n2)) { out.push({ from: c, width: n2 }); i++; }
|
|
198
|
-
else if (typeof n2 === "number") {
|
|
199
|
-
const n3 = W[i + 2];
|
|
200
|
-
if (typeof n3 === "number") { out.push({ from: c, to: n2, w: n3 }); i += 2; }
|
|
201
|
-
else if (Array.isArray(n3)) { out.push({ from: c, width: n3 }); i += 2; }
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return out;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function advOf(info, code, size) {
|
|
208
|
-
if (info.type0) {
|
|
209
|
-
for (const r of info.wRanges || []) {
|
|
210
|
-
if (code >= r.from && (r.to === undefined ? code - r.from < r.width.length : code <= r.to)) {
|
|
211
|
-
const w = r.to === undefined ? r.width[code - r.from] : r.w;
|
|
212
|
-
if (typeof w === "number") return (w / 1000) * size;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return (info.dw / 1000) * size;
|
|
216
|
-
}
|
|
217
|
-
if (info.widths) {
|
|
218
|
-
const w = info.widths[code - info.firstChar];
|
|
219
|
-
if (typeof w === "number") return (w / 1000) * size;
|
|
220
|
-
}
|
|
221
|
-
return size * 0.5;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** One code → string (ToUnicode first, then encoding tables); null = skip glyph. */
|
|
225
|
-
function decodeCode(info, code) {
|
|
226
|
-
if (info.cmap) {
|
|
227
|
-
if (info.cmap.single.has(code)) return info.cmap.single.get(code);
|
|
228
|
-
for (const r of info.cmap.ranges) if (code >= r.lo && code <= r.hi) return String.fromCharCode(r.base + (code - r.lo));
|
|
229
|
-
if (code <= 0x20) return " "; // space glyph sits at low codes (Chrome Type3: 0x01-0x03) but is often missing from the CMap
|
|
230
|
-
info.notes.add("unmapped-code");
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
if (info.type0) {
|
|
234
|
-
if (!info.identity) { info.notes.add("cmap-preset"); return "\uFFFD"; } // preset encodings unknowable without ToUnicode
|
|
235
|
-
info.notes.add("no-tounicode");
|
|
236
|
-
return String.fromCharCode(code); // Identity-H: code ≈ Unicode (尽力)
|
|
237
|
-
}
|
|
238
|
-
if (code < 0x20 || code === 0x7f) return null;
|
|
239
|
-
if (info.symbolic) { info.notes.add("symbolic"); return null; }
|
|
240
|
-
let c = null;
|
|
241
|
-
if (code >= 0x80) {
|
|
242
|
-
if (info.enc === "macRoman") c = MACROMAN_HIGH[code - 0x80];
|
|
243
|
-
else if (info.enc === "standard") { const name = STANDARD_HIGH[code]; if (name !== undefined) c = GLYPH_CHAR[name] ?? "\uFFFD"; }
|
|
244
|
-
else { if (UNDEFINED.has(code)) return null; c = WINANSI_OVERRIDES[code] ?? String.fromCharCode(code); }
|
|
245
|
-
} else {
|
|
246
|
-
if (info.enc === "standard") { if (code === 0x27) c = "\u2019"; else if (code === 0x60) c = "\u2018"; }
|
|
247
|
-
c = c ?? String.fromCharCode(code);
|
|
248
|
-
}
|
|
249
|
-
if (info.diffs && info.diffs.has(code)) c = info.diffs.get(code) === "space" ? " " : (GLYPH_CHAR[info.diffs.get(code)] ?? "\uFFFD");
|
|
250
|
-
if (c === "\uFFFD") info.notes.add("unmapped-code");
|
|
251
|
-
return c;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ── content-stream walk (stage 7) ───────────────────
|
|
255
|
-
function walkContent(doc, data, st) {
|
|
256
|
-
let p = 0;
|
|
257
|
-
let inText = false, tx = 0, lx = 0, ly = 0;
|
|
258
|
-
let fontInfo = null, fontName = null, fontSize = 0;
|
|
259
|
-
const spans = [];
|
|
260
|
-
const resolveFont = (name) => {
|
|
261
|
-
const fr = st.resources.Font?.[name];
|
|
262
|
-
if (!fr) return null;
|
|
263
|
-
if (!st.fontCache.has(name)) st.fontCache.set(name, loadFont(doc, fr));
|
|
264
|
-
return st.fontCache.get(name);
|
|
265
|
-
};
|
|
266
|
-
const show = (buf) => {
|
|
267
|
-
if (!inText) return;
|
|
268
|
-
if (!fontInfo) { st.missingFonts.add(fontName); return; }
|
|
269
|
-
if (fontInfo.type0 && !fontInfo.identity && !fontInfo.cmap) { fontInfo.notes.add("preset-skip"); return; } // CMap-preset, no ToUnicode
|
|
270
|
-
const step = fontInfo.cmap?.codeWidth || (fontInfo.type0 ? 2 : 1);
|
|
271
|
-
const text = [];
|
|
272
|
-
let adv = 0;
|
|
273
|
-
for (let i = 0; i < buf.length; i += step) {
|
|
274
|
-
const code = step === 2 && i + 1 < buf.length ? (buf[i] << 8) | buf[i + 1] : buf[i];
|
|
275
|
-
if (fontInfo.symbolic) { fontInfo.notes.add("symbolic"); break; }
|
|
276
|
-
adv += advOf(fontInfo, code, fontSize);
|
|
277
|
-
const ch = decodeCode(fontInfo, code);
|
|
278
|
-
if (ch !== null) text.push(ch);
|
|
279
|
-
}
|
|
280
|
-
const startX = tx;
|
|
281
|
-
tx += adv;
|
|
282
|
-
if (!text.length) return;
|
|
283
|
-
const joined = text.join("");
|
|
284
|
-
const run = { x: startX, y: ly, size: fontSize, adv, text: joined, font: fontName, fw: adv / Math.max(1, joined.length) / fontSize > 0.85, i: st.order++ };
|
|
285
|
-
if (spans.length && spans[spans.length - 1].actual !== null && spans[spans.length - 1].runsStart < 0) spans[spans.length - 1].runsStart = st.runs.length;
|
|
286
|
-
st.runs.push(run);
|
|
287
|
-
};
|
|
288
|
-
const pending = []; // PDF content is POSTFIX: operands precede their operator
|
|
289
|
-
const pushOp = (o) => { pending.push(o); if (pending.length > 10_000) throw new PdfError("content stream: operand flood — malformed/hostile stream"); };
|
|
290
|
-
while (p < data.length) {
|
|
291
|
-
const t = nextToken(data, p);
|
|
292
|
-
if (!t.tok) break;
|
|
293
|
-
p = t.p;
|
|
294
|
-
if (t.tok.t !== "kw") {
|
|
295
|
-
if (t.tok.t === "<<") { const { dict, after } = parseDictInline(data, p); pushOp({ t: "dict", v: dict }); p = after; }
|
|
296
|
-
else if (t.tok.t === "[") {
|
|
297
|
-
const items = [];
|
|
298
|
-
for (;;) {
|
|
299
|
-
const e = nextToken(data, p);
|
|
300
|
-
if (!e.tok) break;
|
|
301
|
-
p = e.p;
|
|
302
|
-
if (e.tok.t === "]") break;
|
|
303
|
-
if (e.tok.t === "num") items.push({ t: "num", n: e.tok.n });
|
|
304
|
-
else if (e.tok.t === "str" || e.tok.t === "hex") items.push({ t: "str", b: e.tok.buf });
|
|
305
|
-
}
|
|
306
|
-
pushOp({ t: "arr", items });
|
|
307
|
-
} else if (t.tok.t === "num") pushOp({ t: "num", n: t.tok.n });
|
|
308
|
-
else if (t.tok.t === "name") pushOp({ t: "name", s: t.tok.s });
|
|
309
|
-
else if (t.tok.t === "str" || t.tok.t === "hex") pushOp({ t: "str", b: t.tok.buf });
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
const nums = () => pending.filter((o) => o.t === "num").map((o) => o.n);
|
|
313
|
-
const lastStr = () => { for (let i = pending.length - 1; i >= 0; i--) if (pending[i].t === "str") return pending[i].b; return null; };
|
|
314
|
-
switch (t.tok.s) {
|
|
315
|
-
case "BT": inText = true; tx = 0; lx = 0; ly = 0; break;
|
|
316
|
-
case "ET": inText = false; break;
|
|
317
|
-
case "Tf": { const name = pending.find((o) => o.t === "name"), size = nums()[0]; if (name && size !== undefined) { fontName = name.s; fontInfo = resolveFont(fontName); fontSize = size; } break; }
|
|
318
|
-
case "Td": case "TD": {
|
|
319
|
-
const n = nums();
|
|
320
|
-
if (n.length >= 2) { lx += n[0]; ly += n[1]; if (t.tok.s === "TD") st.leading = -n[1]; tx = lx; }
|
|
321
|
-
break;
|
|
322
|
-
}
|
|
323
|
-
case "T*": ly -= st.leading; tx = lx; break;
|
|
324
|
-
case "Tm": { const n = nums(); if (n.length >= 6) { lx = n[4]; ly = n[5]; tx = lx; } break; }
|
|
325
|
-
case "TL": { const n = nums(); if (n.length) st.leading = n[0]; break; }
|
|
326
|
-
case "Tj": { const b = lastStr(); if (b) show(b); break; }
|
|
327
|
-
case "TJ": {
|
|
328
|
-
const arr = pending.find((o) => o.t === "arr");
|
|
329
|
-
if (arr) for (const item of arr.items) { if (item.t === "str") show(item.b); else if (item.t === "num") tx += (item.n / 1000) * fontSize; }
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
case "'": { if (inText) { ly -= st.leading; tx = lx; const b = lastStr(); if (b) show(b); } break; }
|
|
333
|
-
case '"': { const s = pending.find((o) => o.t === "str"); if (s && inText) { ly -= st.leading; tx = lx; show(s.b); } break; }
|
|
334
|
-
case "Do": {
|
|
335
|
-
const name = pending.find((o) => o.t === "name");
|
|
336
|
-
if (name && st.resources.XObject?.[name.s]) {
|
|
337
|
-
const xo = doc.resolve(st.resources.XObject[name.s]);
|
|
338
|
-
if (xo?.dict?.Subtype === "Image") st.images.push(xo); // full stream object — tool extracts payloads
|
|
339
|
-
else if (xo?.dict?.Subtype === "Form" && xo.stream && st.formDepth < MAX_FORM_DEPTH) {
|
|
340
|
-
st.formDepth++;
|
|
341
|
-
const saved = st.resources;
|
|
342
|
-
if (xo.dict.Resources) st.resources = { ...saved, ...doc.resolve(xo.dict.Resources) };
|
|
343
|
-
try { walkContent(doc, decodeStreamBytes(xo.stream, xo.dict), st); }
|
|
344
|
-
finally { st.resources = saved; st.formDepth--; }
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
case "BDC": {
|
|
350
|
-
const d = [...pending].reverse().find((o) => o.t === "dict");
|
|
351
|
-
const actual = d?.v.ActualText && d.v.ActualText.b ? decodeActual(d.v.ActualText.b) : null;
|
|
352
|
-
spans.push({ actual, runsStart: -1 });
|
|
353
|
-
break;
|
|
354
|
-
}
|
|
355
|
-
case "EMC": {
|
|
356
|
-
const top = spans.pop();
|
|
357
|
-
if (top && top.actual !== null && top.runsStart >= 0 && st.runs.length > top.runsStart) {
|
|
358
|
-
const first = st.runs[top.runsStart];
|
|
359
|
-
for (const r of st.runs.splice(top.runsStart + 1)) first.adv += r.adv;
|
|
360
|
-
first.text = top.actual;
|
|
361
|
-
}
|
|
362
|
-
break;
|
|
363
|
-
}
|
|
364
|
-
case "BI": p = skipInlineImage(data, p); break;
|
|
365
|
-
default: break; // unknown/graphics operator — pending (its operands) is dropped below
|
|
366
|
-
}
|
|
367
|
-
pending.length = 0;
|
|
368
|
-
}
|
|
369
|
-
for (const fi of st.fontCache.values()) {
|
|
370
|
-
if (!fi) continue;
|
|
371
|
-
const who = fi.baseFont || "?";
|
|
372
|
-
if (fi.notes.has("symbolic")) st.warn.push(`font ${who}: symbolic font without usable mapping — its glyphs were skipped (F-P3)`);
|
|
373
|
-
if (fi.notes.has("type3-no-tounicode")) st.warn.push(`font ${who}: Type3 font without ToUnicode — glyph codes decoded best-effort, may be wrong (F-P3)`);
|
|
374
|
-
if (fi.notes.has("no-tounicode")) st.warn.push(`font ${who}: CID/Type0 font without ToUnicode — codes mapped as Unicode, may be wrong (F-P3)`);
|
|
375
|
-
if (fi.notes.has("cmap-preset")) st.warn.push(`font ${who}: CMap-preset encoding without ToUnicode — glyphs shown as U+FFFD (F-P3)`);
|
|
376
|
-
if (fi.notes.has("preset-skip")) st.warn.push(`font ${who}: CMap-preset encoded text skipped (F-P3)`);
|
|
377
|
-
if (fi.notes.has("unmapped-code")) st.warn.push(`font ${who}: some glyph codes unmapped — output may be incomplete (F-P3)`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function skipInlineImage(data, p) {
|
|
382
|
-
for (;;) {
|
|
383
|
-
const t = nextToken(data, p);
|
|
384
|
-
if (!t.tok) return t.p;
|
|
385
|
-
p = t.p;
|
|
386
|
-
if (t.tok.t === "kw" && t.tok.s === "ID") break;
|
|
387
|
-
}
|
|
388
|
-
for (let i = p; i < data.length - 1; i++) { // EI must be whitespace-delimited (binary-safe)
|
|
389
|
-
if (data[i] === 0x45 && data[i + 1] === 0x49 && i > p && WS_BYTE(data[i - 1]) && (i + 2 >= data.length || WS_BYTE(data[i + 2]))) return i + 2;
|
|
390
|
-
}
|
|
391
|
-
return data.length;
|
|
392
|
-
}
|
|
393
|
-
const WS_BYTE = (c) => c === 10 || c === 13 || c === 32 || c === 0;
|
|
394
|
-
|
|
395
|
-
// ── layout (stage 9: rows / light columns / paragraphs) ──
|
|
396
|
-
function layout(runs) {
|
|
397
|
-
if (!runs.length) return [];
|
|
398
|
-
runs.sort((a, b) => b.y - a.y);
|
|
399
|
-
const rows = [];
|
|
400
|
-
for (const r of runs) {
|
|
401
|
-
let row = null;
|
|
402
|
-
for (const cand of rows) if (Math.abs(cand.y - r.y) <= Math.max(cand.size, r.size) * 0.62) { row = cand; break; }
|
|
403
|
-
if (row) { row.runs.push(r); row.y = (row.y + r.y) / 2; row.size = Math.max(row.size, r.size); }
|
|
404
|
-
else rows.push({ y: r.y, size: r.size, runs: [r] });
|
|
405
|
-
}
|
|
406
|
-
const byOrder = [...rows].sort((a, b) => a.runs[0].i - b.runs[0].i);
|
|
407
|
-
let up = 0, down = 0;
|
|
408
|
-
for (let i = 1; i < byOrder.length; i++) { const d = byOrder[i].y - byOrder[i - 1].y; if (d > 0.01) up++; else if (d < -0.01) down++; }
|
|
409
|
-
const desc = down > up; // y desc when baselines shrink downstream (PDF-native y-up); flipped matrices grow
|
|
410
|
-
rows.sort((a, b) => (desc ? b.y - a.y : a.y - b.y));
|
|
411
|
-
// light x-cluster columns: wide intra-row gaps (≥2em) that recur at a consistent x
|
|
412
|
-
const cands = [];
|
|
413
|
-
let gappy = 0;
|
|
414
|
-
for (const row of rows) {
|
|
415
|
-
const rs = [...row.runs].sort((a, b) => a.x - b.x);
|
|
416
|
-
for (let i = 1; i < rs.length; i++) {
|
|
417
|
-
const gap = rs[i].x - (rs[i - 1].x + rs[i - 1].adv);
|
|
418
|
-
if (gap >= Math.max(rs[i - 1].size, rs[i].size) * 2) { cands.push({ cx: rs[i - 1].x + rs[i - 1].adv + gap / 2, w: gap }); gappy++; }
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
const bands = [];
|
|
422
|
-
for (const g of cands.sort((a, b) => a.cx - b.cx)) {
|
|
423
|
-
const near = bands.find((b) => Math.abs(b.cx - g.cx) <= 24);
|
|
424
|
-
if (near) { near.ws.push(g.w); near.n++; } else bands.push({ cx: g.cx, ws: [g.w], n: 1 });
|
|
425
|
-
}
|
|
426
|
-
const bounds = [];
|
|
427
|
-
for (const b of bands) {
|
|
428
|
-
if (b.n < Math.max(2, Math.ceil(gappy * 0.3))) continue;
|
|
429
|
-
b.ws.sort((a, b) => a - b);
|
|
430
|
-
const half = b.ws[Math.floor(b.ws.length / 2)] / 2;
|
|
431
|
-
if (bounds.every((o) => Math.abs(o.cx - b.cx) > o.half + half)) bounds.push({ cx: b.cx, half });
|
|
432
|
-
if (bounds.length >= 2) break;
|
|
433
|
-
}
|
|
434
|
-
const colOf = (x) => { let n = 0; for (const b of bounds) if (x > b.cx + b.half) n++; return n; };
|
|
435
|
-
const nCols = bounds.length + 1;
|
|
436
|
-
const per = Array.from({ length: nCols }, () => []);
|
|
437
|
-
for (const row of rows) {
|
|
438
|
-
const rs = [...row.runs].sort((a, b) => a.x - b.x);
|
|
439
|
-
for (let c = 0; c < nCols; c++) {
|
|
440
|
-
const frag = rs.filter((r) => colOf(r.x + r.adv / 2) === c);
|
|
441
|
-
if (!frag.length) continue;
|
|
442
|
-
let text = "";
|
|
443
|
-
for (let i = 0; i < frag.length; i++) {
|
|
444
|
-
if (i > 0) {
|
|
445
|
-
const prev = frag[i - 1], cur = frag[i];
|
|
446
|
-
const gap = cur.x - (prev.x + prev.adv);
|
|
447
|
-
if (gap >= prev.size * 0.15) {
|
|
448
|
-
const pFW = prev.fw, cFW = cur.fw || cur.adv / Math.max(1, cur.text.length) / cur.size > 0.85;
|
|
449
|
-
if (pFW && cFW) { /* CJK↔CJK */ }
|
|
450
|
-
else if (pFW) { if (gap >= (prev.adv / Math.max(1, prev.text.length)) * 1.35) text += " "; }
|
|
451
|
-
else if (!cFW) text += " ";
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
text += frag[i].text;
|
|
455
|
-
}
|
|
456
|
-
per[c].push({ text, y: row.y, size: row.size });
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
const lines = [];
|
|
460
|
-
for (let c = 0; c < nCols; c++) {
|
|
461
|
-
per[c].sort((a, b) => (desc ? b.y - a.y : a.y - b.y));
|
|
462
|
-
per[c].forEach((ln, i) => {
|
|
463
|
-
if (i > 0 && Math.abs(ln.y - per[c][i - 1].y) > Math.max(ln.size, per[c][i - 1].size) * 1.9) lines.push("");
|
|
464
|
-
lines.push(ln.text);
|
|
465
|
-
});
|
|
466
|
-
if (c < nCols - 1) lines.push("");
|
|
467
|
-
}
|
|
468
|
-
return lines;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// ── entry ───────────────────────────────────────────
|
|
472
|
-
export function extractPages(doc, pageNos) {
|
|
473
|
-
const root = doc.resolve(doc.trailer.Root);
|
|
474
|
-
if (!root || root.Type !== "Catalog" || !root.Pages?.r) throw new PdfError("catalog has no /Pages tree");
|
|
475
|
-
const refs = doc.__pdfPageRefs || ((doc.__pdfPageRefs = []), collectPageRefs(doc, root.Pages, doc.__pdfPageRefs, new Set()), doc.__pdfPageRefs); // memoized — count + extract share one page-tree walk
|
|
476
|
-
if (!refs.length) throw new PdfError("document has no pages");
|
|
477
|
-
const warn = [];
|
|
478
|
-
const out = [];
|
|
479
|
-
for (const no of pageNos) {
|
|
480
|
-
const ref = refs[no - 1];
|
|
481
|
-
if (!ref) continue;
|
|
482
|
-
const page = doc.resolve(ref);
|
|
483
|
-
const st = { runs: [], images: [], warn, order: 0, leading: 0, formDepth: 0, resources: pageResources(doc, ref), fontCache: new Map(), missingFonts: new Set() };
|
|
484
|
-
const contents = [];
|
|
485
|
-
if (page.Contents?.r) contents.push(page.Contents);
|
|
486
|
-
else if (Array.isArray(page.Contents)) for (const c of page.Contents) if (c?.r) contents.push(c);
|
|
487
|
-
for (const cr of contents) {
|
|
488
|
-
const cs = doc.resolve(cr);
|
|
489
|
-
if (!cs?.stream) continue;
|
|
490
|
-
try { walkContent(doc, decodeStreamBytes(cs.stream, cs.dict), st); }
|
|
491
|
-
catch (e) { warn.push(`page ${no} content stream: ${e.message}`); }
|
|
492
|
-
}
|
|
493
|
-
for (const fn of st.missingFonts) warn.push(`page ${no}: text used missing font "${fn}" — glyphs skipped (F-P3)`);
|
|
494
|
-
out.push({ no, lines: layout(st.runs), hasText: st.runs.length > 0, images: st.images });
|
|
495
|
-
}
|
|
496
|
-
return { pages: out, pageCount: refs.length, warnings: warn };
|
|
497
|
-
}
|