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.
Files changed (127) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +2 -2
  3. package/bin/thincoder.mjs +80 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +7 -4
  6. package/src/advisor/messages.mjs +24 -4
  7. package/src/advisor/run.mjs +35 -33
  8. package/src/advisor.mjs +25 -6
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +102 -19
  11. package/src/agent/helpers.mjs +36 -0
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +18 -2
  16. package/src/agent/spawn-child.mjs +29 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +110 -108
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +121 -102
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +24 -29
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/read-history.mjs +155 -31
  25. package/src/agent-tools/recent-changes.mjs +2 -1
  26. package/src/agent-tools/settings.mjs +7 -17
  27. package/src/agent-tools/subagent-actions.mjs +168 -130
  28. package/src/agent-tools/subagent-async.mjs +129 -174
  29. package/src/agent-tools/subagent-panel.mjs +153 -0
  30. package/src/agent-tools/subagent-run.mjs +202 -0
  31. package/src/agent-tools/subagent-scheduler.mjs +45 -21
  32. package/src/agent-tools/subagent-spawn.mjs +406 -0
  33. package/src/agent-tools/subagent.mjs +107 -555
  34. package/src/agent-tools/verify.mjs +118 -270
  35. package/src/agent.mjs +57 -190
  36. package/src/cli/distill-command.mjs +10 -4
  37. package/src/cli/make-agent.mjs +3 -1
  38. package/src/cli/memory-command.mjs +2 -1
  39. package/src/cli/permission.mjs +2 -2
  40. package/src/cli/setup-wizard.mjs +17 -12
  41. package/src/config.mjs +56 -8
  42. package/src/context.mjs +5 -147
  43. package/src/crash-reports.mjs +123 -0
  44. package/src/distill.mjs +11 -11
  45. package/src/explore-distill.mjs +155 -0
  46. package/src/memory/code-sync.mjs +2 -1
  47. package/src/memory/core.mjs +6 -193
  48. package/src/memory/delete.mjs +234 -0
  49. package/src/memory/docs.mjs +58 -48
  50. package/src/memory.mjs +3 -1
  51. package/src/peer-domains.mjs +265 -0
  52. package/src/peer-instances.mjs +231 -0
  53. package/src/prompt-overlays.mjs +25 -0
  54. package/src/prompts/advisor-design.md +9 -76
  55. package/src/prompts/advisor-round1.md +9 -68
  56. package/src/prompts/advisor-round2.md +7 -54
  57. package/src/prompts/advisor-round3.md +7 -54
  58. package/src/prompts/coder.md +7 -50
  59. package/src/prompts/consult-base.md +4 -24
  60. package/src/prompts/discipline.md +26 -44
  61. package/src/prompts/eng-coder.md +7 -32
  62. package/src/prompts/engineering-sub.md +3 -23
  63. package/src/prompts/engineering.md +53 -306
  64. package/src/prompts/explore.md +3 -12
  65. package/src/prompts/main.md +10 -32
  66. package/src/prompts/methodology-template.md +28 -48
  67. package/src/prompts/plan.md +2 -9
  68. package/src/prompts/system.md +16 -35
  69. package/src/provider/core.mjs +6 -67
  70. package/src/provider/errors.mjs +76 -0
  71. package/src/provider/retry.mjs +8 -45
  72. package/src/session-gc.mjs +214 -0
  73. package/src/session-guard.mjs +47 -0
  74. package/src/session-rename.mjs +38 -0
  75. package/src/session-slots.mjs +181 -58
  76. package/src/session.mjs +48 -89
  77. package/src/token-ttl.mjs +273 -0
  78. package/src/tools/checklist-sync.mjs +181 -0
  79. package/src/tools/checklist.mjs +52 -39
  80. package/src/tools/edit-batch.mjs +109 -10
  81. package/src/tools/edit-diff.mjs +110 -27
  82. package/src/tools/edit.md +17 -12
  83. package/src/tools/execute.mjs +31 -4
  84. package/src/tools/file.mjs +11 -6
  85. package/src/tools/git.mjs +14 -6
  86. package/src/tools/glob-dialect.mjs +130 -0
  87. package/src/tools/glob.md +3 -3
  88. package/src/tools/grep.md +1 -1
  89. package/src/tools/index.mjs +5 -6
  90. package/src/tools/ops.mjs +175 -3
  91. package/src/tools/patch.mjs +3 -3
  92. package/src/tools/question.md +3 -0
  93. package/src/tools/read.md +0 -1
  94. package/src/tools/shared.mjs +14 -13
  95. package/src/tools/system.mjs +44 -9
  96. package/src/tools/wait_for.md +22 -0
  97. package/src/tui/agent-turn.mjs +17 -228
  98. package/src/tui/cmd-config.mjs +48 -7
  99. package/src/tui/cmd-eng.mjs +20 -16
  100. package/src/tui/cmd-mcp.mjs +8 -2
  101. package/src/tui/cmd-new.mjs +3 -2
  102. package/src/tui/cmd-session.mjs +19 -4
  103. package/src/tui/cmd-think.mjs +10 -10
  104. package/src/tui/cmd-upgrade.mjs +19 -4
  105. package/src/tui/config-helpers.mjs +28 -16
  106. package/src/tui/distill-cmd.mjs +1 -1
  107. package/src/tui/index.mjs +3 -2
  108. package/src/tui/interaction.mjs +3 -3
  109. package/src/tui/mouse.mjs +7 -1
  110. package/src/tui/pickers.mjs +40 -22
  111. package/src/tui/render-segments.mjs +27 -10
  112. package/src/tui/startup.mjs +4 -0
  113. package/src/tui/subagent-blocks.mjs +95 -263
  114. package/src/tui/subagent-children.mjs +176 -0
  115. package/src/tui/subagent-freeze.mjs +172 -0
  116. package/src/tui/subagent-panel.mjs +61 -23
  117. package/src/tui/suspension-drive.mjs +351 -0
  118. package/src/tui/tool-args.mjs +3 -3
  119. package/src/tui/tool-display.mjs +142 -0
  120. package/src/tui/tool-events.mjs +37 -173
  121. package/src/tui/tui-lifecycle.mjs +29 -0
  122. package/src/tui/update-notice.mjs +4 -0
  123. package/src/tui/wizard.mjs +12 -6
  124. package/src/tools/pdf-parse-text.mjs +0 -497
  125. package/src/tools/pdf-parse-xref.mjs +0 -499
  126. package/src/tools/pdf.mjs +0 -155
  127. 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
- }