@jcode.labs/ragmir 2.17.2 → 2.18.1
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/README.md +53 -43
- package/dist/access-log.d.ts +10 -0
- package/dist/access-log.d.ts.map +1 -1
- package/dist/access-log.js +119 -49
- package/dist/access-log.js.map +1 -1
- package/dist/chunking.d.ts +5 -1
- package/dist/chunking.d.ts.map +1 -1
- package/dist/chunking.js +102 -16
- package/dist/chunking.js.map +1 -1
- package/dist/citation.d.ts +18 -0
- package/dist/citation.d.ts.map +1 -0
- package/dist/citation.js +54 -0
- package/dist/citation.js.map +1 -0
- package/dist/cli-entry.d.ts +3 -0
- package/dist/cli-entry.d.ts.map +1 -0
- package/dist/cli-entry.js +16 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli-fast.d.ts +13 -0
- package/dist/cli-fast.d.ts.map +1 -0
- package/dist/cli-fast.js +145 -0
- package/dist/cli-fast.js.map +1 -0
- package/dist/cli-options.d.ts +3 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +7 -0
- package/dist/cli-options.js.map +1 -1
- package/dist/cli-stdin.d.ts +3 -0
- package/dist/cli-stdin.d.ts.map +1 -0
- package/dist/cli-stdin.js +15 -0
- package/dist/cli-stdin.js.map +1 -0
- package/dist/cli.js +251 -44
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +13 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +155 -10
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +227 -49
- package/dist/config.js.map +1 -1
- package/dist/context-resources.d.ts +4 -2
- package/dist/context-resources.d.ts.map +1 -1
- package/dist/context-resources.js +55 -24
- package/dist/context-resources.js.map +1 -1
- package/dist/defaults.d.ts +27 -0
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +42 -2
- package/dist/defaults.js.map +1 -1
- package/dist/destroy.d.ts +2 -2
- package/dist/destroy.d.ts.map +1 -1
- package/dist/destroy.js +34 -20
- package/dist/destroy.js.map +1 -1
- package/dist/doctor.d.ts +3 -2
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +86 -41
- package/dist/doctor.js.map +1 -1
- package/dist/durable-file.d.ts +13 -0
- package/dist/durable-file.d.ts.map +1 -0
- package/dist/durable-file.js +56 -0
- package/dist/durable-file.js.map +1 -0
- package/dist/embeddings.d.ts +14 -3
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +233 -95
- package/dist/embeddings.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evaluate.d.ts +4 -3
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +611 -110
- package/dist/evaluate.js.map +1 -1
- package/dist/files.d.ts +4 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +119 -33
- package/dist/files.js.map +1 -1
- package/dist/generation-retention.d.ts +52 -0
- package/dist/generation-retention.d.ts.map +1 -0
- package/dist/generation-retention.js +318 -0
- package/dist/generation-retention.js.map +1 -0
- package/dist/index-diagnostics.d.ts +3 -2
- package/dist/index-diagnostics.d.ts.map +1 -1
- package/dist/index-diagnostics.js +32 -3
- package/dist/index-diagnostics.js.map +1 -1
- package/dist/index-policy.d.ts.map +1 -1
- package/dist/index-policy.js +3 -0
- package/dist/index-policy.js.map +1 -1
- package/dist/index-write-lock.d.ts +16 -1
- package/dist/index-write-lock.d.ts.map +1 -1
- package/dist/index-write-lock.js +392 -13
- package/dist/index-write-lock.js.map +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +11 -2
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +680 -165
- package/dist/ingest.js.map +1 -1
- package/dist/ingestion-metrics.d.ts +58 -0
- package/dist/ingestion-metrics.d.ts.map +1 -0
- package/dist/ingestion-metrics.js +216 -0
- package/dist/ingestion-metrics.js.map +1 -0
- package/dist/ingestion-state.d.ts +11 -2
- package/dist/ingestion-state.d.ts.map +1 -1
- package/dist/ingestion-state.js +480 -49
- package/dist/ingestion-state.js.map +1 -1
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +11 -1
- package/dist/limits.js.map +1 -1
- package/dist/mcp-output.d.ts +44 -4
- package/dist/mcp-output.d.ts.map +1 -1
- package/dist/mcp-output.js +132 -159
- package/dist/mcp-output.js.map +1 -1
- package/dist/mcp-summaries.d.ts +32 -0
- package/dist/mcp-summaries.d.ts.map +1 -0
- package/dist/mcp-summaries.js +227 -0
- package/dist/mcp-summaries.js.map +1 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +85 -46
- package/dist/mcp.js.map +1 -1
- package/dist/ocr-cache.d.ts +15 -0
- package/dist/ocr-cache.d.ts.map +1 -0
- package/dist/ocr-cache.js +90 -0
- package/dist/ocr-cache.js.map +1 -0
- package/dist/ocr.d.ts +31 -0
- package/dist/ocr.d.ts.map +1 -1
- package/dist/ocr.js +193 -17
- package/dist/ocr.js.map +1 -1
- package/dist/parsing.d.ts.map +1 -1
- package/dist/parsing.js +482 -85
- package/dist/parsing.js.map +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +10 -11
- package/dist/preview.js.map +1 -1
- package/dist/quality-report.d.ts +12 -0
- package/dist/quality-report.d.ts.map +1 -0
- package/dist/quality-report.js +146 -0
- package/dist/quality-report.js.map +1 -0
- package/dist/query.d.ts +7 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +371 -284
- package/dist/query.js.map +1 -1
- package/dist/ranking.d.ts +38 -0
- package/dist/ranking.d.ts.map +1 -0
- package/dist/ranking.js +278 -0
- package/dist/ranking.js.map +1 -0
- package/dist/redaction.d.ts +6 -1
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +118 -4
- package/dist/redaction.js.map +1 -1
- package/dist/research.d.ts +6 -1
- package/dist/research.d.ts.map +1 -1
- package/dist/research.js +330 -130
- package/dist/research.js.map +1 -1
- package/dist/runtime-info.d.ts +3 -0
- package/dist/runtime-info.d.ts.map +1 -0
- package/dist/runtime-info.js +81 -0
- package/dist/runtime-info.js.map +1 -0
- package/dist/security.d.ts +3 -2
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +242 -45
- package/dist/security.js.map +1 -1
- package/dist/semantic-config.d.ts +7 -1
- package/dist/semantic-config.d.ts.map +1 -1
- package/dist/semantic-config.js +30 -4
- package/dist/semantic-config.js.map +1 -1
- package/dist/setup.js +6 -3
- package/dist/setup.js.map +1 -1
- package/dist/skill.js +34 -12
- package/dist/skill.js.map +1 -1
- package/dist/source-fingerprint-cache.d.ts +20 -0
- package/dist/source-fingerprint-cache.d.ts.map +1 -0
- package/dist/source-fingerprint-cache.js +133 -0
- package/dist/source-fingerprint-cache.js.map +1 -0
- package/dist/storage-maintenance.d.ts +46 -0
- package/dist/storage-maintenance.d.ts.map +1 -0
- package/dist/storage-maintenance.js +329 -0
- package/dist/storage-maintenance.js.map +1 -0
- package/dist/store.d.ts +39 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +559 -24
- package/dist/store.js.map +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +12 -2
- package/dist/text.js.map +1 -1
- package/dist/types.d.ts +414 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-index.d.ts +42 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +249 -0
- package/dist/vector-index.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workload.d.ts +22 -0
- package/dist/workload.d.ts.map +1 -0
- package/dist/workload.js +181 -0
- package/dist/workload.js.map +1 -0
- package/examples/document-evidence-benchmark/README.md +25 -63
- package/examples/document-evidence-benchmark/golden-queries.json +1 -1
- package/examples/library-api-demo/README.md +17 -57
- package/examples/sovereign-rag-demo/README.md +37 -97
- package/package.json +36 -11
package/dist/parsing.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createRequire as _createRequire } from "module";
|
|
|
2
2
|
const __require = _createRequire(import.meta.url);
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
5
6
|
import { strFromU8, unzipSync } from "fflate";
|
|
6
7
|
import { htmlToText } from "html-to-text";
|
|
7
8
|
import readExcelFile from "read-excel-file/node";
|
|
@@ -9,11 +10,15 @@ const mammoth = __require("mammoth");
|
|
|
9
10
|
import { getDocumentProxy } from "unpdf";
|
|
10
11
|
import YAML from "yaml";
|
|
11
12
|
import { OCR_IMAGE_EXTENSIONS } from "./files.js";
|
|
13
|
+
import { isRecord } from "./guards.js";
|
|
12
14
|
import { MAX_EXTERNAL_TEXT_STDIO_BYTES, MAX_OFFICE_TEXT_ENTRY_COUNT, MAX_OFFICE_XML_ENTRY_BYTES, MAX_OFFICE_XML_TOTAL_BYTES, MAX_PDF_PAGES, MAX_PDF_TEXT_CHARACTERS, } from "./limits.js";
|
|
15
|
+
import { MAX_OCR_BATCH_PAGES, pdfOcrCommandIdentity } from "./ocr.js";
|
|
16
|
+
import { PDF_OCR_PARSER_POLICY, readPdfOcrCache, writePdfOcrCache, } from "./ocr-cache.js";
|
|
13
17
|
import { throwIfAborted } from "./operation.js";
|
|
14
18
|
const EXTERNAL_COMMAND_KILL_GRACE_MS = 2_000;
|
|
15
19
|
const LONG_BASE64_TEXT_PATTERN = /\b[A-Za-z0-9+/]{240,}={0,2}\b/gu;
|
|
16
|
-
const OFFICE_TEXT_ENTRY_PATTERN = /\.(?:xml|rels|xhtml|html|htm)$/iu;
|
|
20
|
+
const OFFICE_TEXT_ENTRY_PATTERN = /\.(?:xml|rels|xhtml|html|htm|opf)$/iu;
|
|
21
|
+
const NATURAL_PATH_ORDER = new Intl.Collator("en", { numeric: true, sensitivity: "base" });
|
|
17
22
|
const HTML_TO_TEXT_OPTIONS = {
|
|
18
23
|
wordwrap: false,
|
|
19
24
|
selectors: [
|
|
@@ -25,22 +30,27 @@ export async function parseFile(file, options = {}) {
|
|
|
25
30
|
throwIfAborted(options.signal);
|
|
26
31
|
let text;
|
|
27
32
|
let pages;
|
|
33
|
+
let regions;
|
|
34
|
+
let ocr;
|
|
35
|
+
let sourceLineCoordinates = false;
|
|
28
36
|
switch (file.extension) {
|
|
29
37
|
case ".pdf":
|
|
30
38
|
;
|
|
31
|
-
({ text, pages } = await parsePdf(file
|
|
39
|
+
({ text, pages, regions, ocr } = await parsePdf(file, options));
|
|
32
40
|
break;
|
|
33
41
|
case ".doc":
|
|
34
42
|
text = await parseLegacyWord(file.absolutePath, options);
|
|
35
43
|
break;
|
|
36
44
|
case ".docx":
|
|
37
|
-
text = await parseDocx(file.absolutePath);
|
|
45
|
+
text = await parseDocx(file.absolutePath, options.signal);
|
|
38
46
|
break;
|
|
39
47
|
case ".pptx":
|
|
40
|
-
|
|
48
|
+
;
|
|
49
|
+
({ text, regions } = await parsePptx(file.absolutePath, options.signal));
|
|
41
50
|
break;
|
|
42
51
|
case ".xlsx":
|
|
43
|
-
|
|
52
|
+
;
|
|
53
|
+
({ text, regions } = await parseXlsx(file.absolutePath, options.signal));
|
|
44
54
|
break;
|
|
45
55
|
case ".avif":
|
|
46
56
|
case ".bmp":
|
|
@@ -58,61 +68,138 @@ export async function parseFile(file, options = {}) {
|
|
|
58
68
|
case ".odt":
|
|
59
69
|
case ".ods":
|
|
60
70
|
case ".odp":
|
|
61
|
-
text = await parseOpenDocument(file.absolutePath);
|
|
71
|
+
text = await parseOpenDocument(file.absolutePath, options.signal);
|
|
62
72
|
break;
|
|
63
73
|
case ".epub":
|
|
64
|
-
|
|
74
|
+
;
|
|
75
|
+
({ text, regions } = await parseEpub(file.absolutePath, options.signal));
|
|
65
76
|
break;
|
|
66
77
|
case ".html":
|
|
67
78
|
case ".htm":
|
|
68
79
|
text = htmlToText(await readFile(file.absolutePath, "utf8"), HTML_TO_TEXT_OPTIONS);
|
|
69
80
|
break;
|
|
70
81
|
case ".json":
|
|
71
|
-
case ".ipynb":
|
|
82
|
+
case ".ipynb": {
|
|
72
83
|
text = JSON.stringify(JSON.parse(await readFile(file.absolutePath, "utf8")), null, 2);
|
|
73
84
|
break;
|
|
85
|
+
}
|
|
74
86
|
case ".yaml":
|
|
75
|
-
case ".yml":
|
|
87
|
+
case ".yml": {
|
|
76
88
|
text = YAML.stringify(YAML.parse(await readFile(file.absolutePath, "utf8")));
|
|
77
89
|
break;
|
|
90
|
+
}
|
|
78
91
|
case ".rtf":
|
|
79
92
|
text = stripRtf(await readFile(file.absolutePath, "utf8"));
|
|
80
93
|
break;
|
|
81
94
|
default:
|
|
82
95
|
text = await readFile(file.absolutePath, "utf8");
|
|
96
|
+
sourceLineCoordinates = true;
|
|
83
97
|
}
|
|
84
98
|
throwIfAborted(options.signal);
|
|
85
|
-
const document = {
|
|
99
|
+
const document = {
|
|
100
|
+
file,
|
|
101
|
+
text: regions ? text : normalizeText(text, sourceLineCoordinates),
|
|
102
|
+
sourceLineCoordinates,
|
|
103
|
+
};
|
|
86
104
|
if (pages) {
|
|
87
105
|
document.pages = pages;
|
|
88
106
|
}
|
|
107
|
+
if (regions) {
|
|
108
|
+
document.regions = regions;
|
|
109
|
+
}
|
|
110
|
+
if (ocr) {
|
|
111
|
+
document.ocr = ocr;
|
|
112
|
+
}
|
|
89
113
|
return document;
|
|
90
114
|
}
|
|
91
|
-
async function parseDocx(filePath) {
|
|
115
|
+
async function parseDocx(filePath, signal) {
|
|
92
116
|
const buffer = await readFile(filePath);
|
|
93
|
-
|
|
117
|
+
throwIfAborted(signal);
|
|
118
|
+
validateOfficeArchive(buffer, signal);
|
|
94
119
|
const result = await mammoth.extractRawText({ buffer });
|
|
120
|
+
throwIfAborted(signal);
|
|
95
121
|
return result.value;
|
|
96
122
|
}
|
|
97
|
-
async function parsePptx(filePath) {
|
|
98
|
-
const entries = unzipOfficeFile(await readFile(filePath));
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
])
|
|
123
|
+
async function parsePptx(filePath, signal) {
|
|
124
|
+
const entries = unzipOfficeFile(await readFile(filePath), signal);
|
|
125
|
+
const slides = orderedPresentationSlides(entries, signal);
|
|
126
|
+
const notes = new Map(numberedOfficeParts(entries, /^ppt\/notesSlides\/notesSlide(\d+)\.xml$/u, signal).map((part) => [part.number, part.text]));
|
|
127
|
+
const parts = [];
|
|
128
|
+
for (const [index, slide] of slides.entries()) {
|
|
129
|
+
throwIfAborted(signal);
|
|
130
|
+
const noteText = presentationSlideNotes(entries, slide.number) ?? notes.get(slide.number) ?? "";
|
|
131
|
+
const text = [slide.text, noteText].filter(Boolean).join("\n\n");
|
|
132
|
+
const slideNumber = index + 1;
|
|
133
|
+
if (text) {
|
|
134
|
+
parts.push({
|
|
135
|
+
text,
|
|
136
|
+
contextPath: `Slide ${slideNumber}`,
|
|
137
|
+
location: {
|
|
138
|
+
kind: "slide",
|
|
139
|
+
start: slideNumber,
|
|
140
|
+
end: slideNumber,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return joinLocatedParts(parts, signal);
|
|
146
|
+
}
|
|
147
|
+
function presentationSlideNotes(entries, slideNumber) {
|
|
148
|
+
const relationships = entries.get(`ppt/slides/_rels/slide${slideNumber}.xml.rels`);
|
|
149
|
+
if (!relationships) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
for (const relationship of xmlStartTags(relationships, "Relationship")) {
|
|
153
|
+
const target = xmlAttribute(relationship, "Target");
|
|
154
|
+
if (!target) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const entry = path.posix.normalize(path.posix.join("ppt/slides", target));
|
|
158
|
+
if (!/^ppt\/notesSlides\/notesSlide\d+\.xml$/u.test(entry)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const xml = entries.get(entry);
|
|
162
|
+
if (xml) {
|
|
163
|
+
return xmlToText(xml);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
103
167
|
}
|
|
104
|
-
async function parseXlsx(filePath) {
|
|
168
|
+
async function parseXlsx(filePath, signal) {
|
|
105
169
|
const buffer = await readFile(filePath);
|
|
106
|
-
|
|
170
|
+
throwIfAborted(signal);
|
|
171
|
+
validateOfficeArchive(buffer, signal);
|
|
107
172
|
const workbook = await readExcelFile(buffer, { trim: false });
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
173
|
+
throwIfAborted(signal);
|
|
174
|
+
const parts = [];
|
|
175
|
+
for (const [sheetIndex, sheet] of workbook.entries()) {
|
|
176
|
+
throwIfAborted(signal);
|
|
177
|
+
let firstRow = true;
|
|
178
|
+
for (const [rowIndex, rawRow] of sheet.data.entries()) {
|
|
179
|
+
throwIfAborted(signal);
|
|
180
|
+
const row = spreadsheetRowToText(rawRow);
|
|
181
|
+
const firstColumn = row.findIndex(Boolean);
|
|
182
|
+
if (firstColumn < 0) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const lastColumn = lastNonEmptyIndex(row);
|
|
186
|
+
const rowNumber = rowIndex + 1;
|
|
187
|
+
parts.push({
|
|
188
|
+
text: `${firstRow ? `# ${sheet.sheet}\n` : ""}${row.join("\t")}`,
|
|
189
|
+
contextPath: `Sheet: ${sheet.sheet}`,
|
|
190
|
+
location: {
|
|
191
|
+
kind: "sheet",
|
|
192
|
+
start: sheetIndex + 1,
|
|
193
|
+
end: sheetIndex + 1,
|
|
194
|
+
label: sheet.sheet,
|
|
195
|
+
cellStart: `${spreadsheetColumnName(firstColumn + 1)}${rowNumber}`,
|
|
196
|
+
cellEnd: `${spreadsheetColumnName(lastColumn + 1)}${rowNumber}`,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
firstRow = false;
|
|
113
200
|
}
|
|
114
201
|
}
|
|
115
|
-
return
|
|
202
|
+
return joinLocatedParts(parts, signal);
|
|
116
203
|
}
|
|
117
204
|
async function parseImage(filePath, extension, options) {
|
|
118
205
|
if (!OCR_IMAGE_EXTENSIONS.has(extension)) {
|
|
@@ -143,59 +230,220 @@ async function parseLegacyWord(filePath, options) {
|
|
|
143
230
|
...(options.signal ? { signal: options.signal } : {}),
|
|
144
231
|
});
|
|
145
232
|
}
|
|
146
|
-
async function parseOpenDocument(filePath) {
|
|
147
|
-
const entries = unzipOfficeFile(await readFile(filePath));
|
|
148
|
-
return xmlEntriesToText(entries, [/^content\.xml$/u, /^meta\.xml$/u]);
|
|
233
|
+
async function parseOpenDocument(filePath, signal) {
|
|
234
|
+
const entries = unzipOfficeFile(await readFile(filePath), signal);
|
|
235
|
+
return xmlEntriesToText(entries, [/^content\.xml$/u, /^meta\.xml$/u], signal);
|
|
149
236
|
}
|
|
150
|
-
async function parseEpub(filePath) {
|
|
151
|
-
const entries = unzipOfficeFile(await readFile(filePath));
|
|
237
|
+
async function parseEpub(filePath, signal) {
|
|
238
|
+
const entries = unzipOfficeFile(await readFile(filePath), signal);
|
|
239
|
+
const orderedEntries = epubReadingOrder(entries, signal);
|
|
152
240
|
const parts = [];
|
|
153
|
-
for (const [
|
|
154
|
-
|
|
241
|
+
for (const [index, name] of orderedEntries.entries()) {
|
|
242
|
+
throwIfAborted(signal);
|
|
243
|
+
const content = entries.get(name);
|
|
244
|
+
if (content === undefined) {
|
|
155
245
|
continue;
|
|
156
246
|
}
|
|
157
247
|
const text = htmlToText(content, HTML_TO_TEXT_OPTIONS);
|
|
158
248
|
if (text.trim()) {
|
|
159
|
-
parts.push(
|
|
249
|
+
parts.push({
|
|
250
|
+
text,
|
|
251
|
+
contextPath: `EPUB: ${name}`,
|
|
252
|
+
location: {
|
|
253
|
+
kind: "epub",
|
|
254
|
+
start: index + 1,
|
|
255
|
+
end: index + 1,
|
|
256
|
+
label: name,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
160
259
|
}
|
|
161
260
|
}
|
|
162
|
-
return parts
|
|
261
|
+
return joinLocatedParts(parts, signal);
|
|
262
|
+
}
|
|
263
|
+
function numberedOfficeParts(entries, pattern, signal) {
|
|
264
|
+
const parts = [];
|
|
265
|
+
for (const [name, xml] of entries) {
|
|
266
|
+
throwIfAborted(signal);
|
|
267
|
+
const match = pattern.exec(name);
|
|
268
|
+
const number = Number(match?.[1]);
|
|
269
|
+
const text = match && Number.isSafeInteger(number) && number > 0 ? xmlToText(xml) : "";
|
|
270
|
+
if (text) {
|
|
271
|
+
parts.push({ number, text });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return parts.sort((left, right) => left.number - right.number);
|
|
275
|
+
}
|
|
276
|
+
function orderedPresentationSlides(entries, signal) {
|
|
277
|
+
const natural = numberedOfficeParts(entries, /^ppt\/slides\/slide(\d+)\.xml$/u, signal);
|
|
278
|
+
const presentation = entries.get("ppt/presentation.xml");
|
|
279
|
+
const relationships = entries.get("ppt/_rels/presentation.xml.rels");
|
|
280
|
+
if (!presentation || !relationships) {
|
|
281
|
+
return natural;
|
|
282
|
+
}
|
|
283
|
+
const targets = new Map();
|
|
284
|
+
for (const relationship of xmlStartTags(relationships, "Relationship")) {
|
|
285
|
+
throwIfAborted(signal);
|
|
286
|
+
const id = xmlAttribute(relationship, "Id");
|
|
287
|
+
const target = xmlAttribute(relationship, "Target");
|
|
288
|
+
if (id && target) {
|
|
289
|
+
targets.set(id, path.posix.normalize(path.posix.join("ppt", target)));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const byNumber = new Map(natural.map((slide) => [slide.number, slide]));
|
|
293
|
+
const ordered = [];
|
|
294
|
+
for (const slide of xmlStartTags(presentation, "sldId")) {
|
|
295
|
+
throwIfAborted(signal);
|
|
296
|
+
const relationshipId = xmlAttribute(slide, "r:id");
|
|
297
|
+
const target = relationshipId ? targets.get(relationshipId) : undefined;
|
|
298
|
+
const match = target ? /^ppt\/slides\/slide(\d+)\.xml$/u.exec(target) : null;
|
|
299
|
+
const number = Number(match?.[1]);
|
|
300
|
+
const part = Number.isSafeInteger(number) ? byNumber.get(number) : undefined;
|
|
301
|
+
if (!part) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
byNumber.delete(number);
|
|
305
|
+
ordered.push(part);
|
|
306
|
+
}
|
|
307
|
+
return ordered.length > 0 ? [...ordered, ...byNumber.values()] : natural;
|
|
308
|
+
}
|
|
309
|
+
function joinLocatedParts(parts, signal) {
|
|
310
|
+
let text = "";
|
|
311
|
+
const regions = [];
|
|
312
|
+
for (const part of parts) {
|
|
313
|
+
throwIfAborted(signal);
|
|
314
|
+
const normalized = normalizeText(part.text);
|
|
315
|
+
if (!normalized) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (text) {
|
|
319
|
+
text += "\n\n";
|
|
320
|
+
}
|
|
321
|
+
const charStart = text.length;
|
|
322
|
+
text += normalized;
|
|
323
|
+
regions.push({
|
|
324
|
+
charStart,
|
|
325
|
+
charEnd: text.length,
|
|
326
|
+
contextPath: part.contextPath,
|
|
327
|
+
location: part.location,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return { text, regions };
|
|
331
|
+
}
|
|
332
|
+
function epubReadingOrder(entries, signal) {
|
|
333
|
+
const htmlEntries = [];
|
|
334
|
+
for (const name of entries.keys()) {
|
|
335
|
+
throwIfAborted(signal);
|
|
336
|
+
if (/\.(?:xhtml|html|htm)$/iu.test(name)) {
|
|
337
|
+
htmlEntries.push(name);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
htmlEntries.sort((left, right) => NATURAL_PATH_ORDER.compare(left, right));
|
|
341
|
+
const containerPath = [...entries.keys()].find((name) => name.toLowerCase() === "meta-inf/container.xml");
|
|
342
|
+
const container = containerPath ? entries.get(containerPath) : undefined;
|
|
343
|
+
const packagePath = container ? firstXmlAttribute(container, "rootfile", "full-path") : null;
|
|
344
|
+
const packageXml = packagePath ? entries.get(packagePath) : undefined;
|
|
345
|
+
if (!packagePath || !packageXml) {
|
|
346
|
+
return htmlEntries;
|
|
347
|
+
}
|
|
348
|
+
const packageDirectory = path.posix.dirname(packagePath);
|
|
349
|
+
const manifest = new Map();
|
|
350
|
+
for (const element of xmlStartTags(packageXml, "item")) {
|
|
351
|
+
throwIfAborted(signal);
|
|
352
|
+
const id = xmlAttribute(element, "id");
|
|
353
|
+
const href = xmlAttribute(element, "href");
|
|
354
|
+
if (id && href) {
|
|
355
|
+
manifest.set(id, resolveEpubEntry(packageDirectory, href));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const spine = [];
|
|
359
|
+
for (const element of xmlStartTags(packageXml, "itemref")) {
|
|
360
|
+
throwIfAborted(signal);
|
|
361
|
+
const id = xmlAttribute(element, "idref");
|
|
362
|
+
const entry = id ? manifest.get(id) : undefined;
|
|
363
|
+
if (entry && entries.has(entry)) {
|
|
364
|
+
spine.push(entry);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return spine.length > 0 ? [...new Set(spine)] : htmlEntries;
|
|
368
|
+
}
|
|
369
|
+
function resolveEpubEntry(packageDirectory, href) {
|
|
370
|
+
const withoutFragment = href.split("#", 1)[0] ?? href;
|
|
371
|
+
let decoded = withoutFragment;
|
|
372
|
+
try {
|
|
373
|
+
decoded = decodeURIComponent(withoutFragment);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// Keep the literal manifest path when percent encoding is malformed.
|
|
377
|
+
}
|
|
378
|
+
return path.posix.normalize(path.posix.join(packageDirectory, decoded));
|
|
379
|
+
}
|
|
380
|
+
function xmlStartTags(xml, localName) {
|
|
381
|
+
return [...xml.matchAll(new RegExp(`<(?:[A-Za-z_][\\w.-]*:)?${localName}\\b[^>]*>`, "giu"))].map((match) => match[0]);
|
|
382
|
+
}
|
|
383
|
+
function firstXmlAttribute(xml, element, attribute) {
|
|
384
|
+
const tag = xmlStartTags(xml, element)[0];
|
|
385
|
+
return tag ? xmlAttribute(tag, attribute) : null;
|
|
163
386
|
}
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
387
|
+
function xmlAttribute(element, name) {
|
|
388
|
+
const match = new RegExp(`\\b${name}\\s*=\\s*(["'])(.*?)\\1`, "iu").exec(element);
|
|
389
|
+
return match?.[2] ? decodeXmlEntities(match[2]) : null;
|
|
390
|
+
}
|
|
391
|
+
function validateOfficeArchive(buffer, signal) {
|
|
392
|
+
const state = officeArchiveLimitsState();
|
|
393
|
+
unzipSync(new Uint8Array(buffer), {
|
|
394
|
+
filter: (file) => {
|
|
395
|
+
throwIfAborted(signal);
|
|
396
|
+
includeOfficeTextEntry(file, state);
|
|
397
|
+
return false;
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
assertOfficeArchiveLimits(state);
|
|
401
|
+
}
|
|
402
|
+
function unzipOfficeFile(buffer, signal) {
|
|
403
|
+
const state = officeArchiveLimitsState();
|
|
168
404
|
const unzipped = unzipSync(new Uint8Array(buffer), {
|
|
169
405
|
filter: (file) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
if (file.originalSize > MAX_OFFICE_XML_ENTRY_BYTES) {
|
|
174
|
-
rejectedForSafetyLimit = true;
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
textEntryCount += 1;
|
|
178
|
-
totalTextBytes += file.originalSize;
|
|
179
|
-
if (textEntryCount > MAX_OFFICE_TEXT_ENTRY_COUNT ||
|
|
180
|
-
totalTextBytes > MAX_OFFICE_XML_TOTAL_BYTES) {
|
|
181
|
-
rejectedForSafetyLimit = true;
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
return true;
|
|
406
|
+
throwIfAborted(signal);
|
|
407
|
+
return includeOfficeTextEntry(file, state);
|
|
185
408
|
},
|
|
186
409
|
});
|
|
187
|
-
|
|
188
|
-
throw new Error("Archive text payload exceeds Ragmir safety limits.");
|
|
189
|
-
}
|
|
410
|
+
assertOfficeArchiveLimits(state);
|
|
190
411
|
const entries = new Map();
|
|
191
412
|
for (const [name, content] of Object.entries(unzipped)) {
|
|
413
|
+
throwIfAborted(signal);
|
|
192
414
|
entries.set(name, strFromU8(content));
|
|
193
415
|
}
|
|
194
416
|
return entries;
|
|
195
417
|
}
|
|
196
|
-
function
|
|
418
|
+
function officeArchiveLimitsState() {
|
|
419
|
+
return { textEntryCount: 0, totalTextBytes: 0, rejected: false };
|
|
420
|
+
}
|
|
421
|
+
function includeOfficeTextEntry(file, state) {
|
|
422
|
+
if (!OFFICE_TEXT_ENTRY_PATTERN.test(file.name)) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
if (file.originalSize > MAX_OFFICE_XML_ENTRY_BYTES) {
|
|
426
|
+
state.rejected = true;
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
state.textEntryCount += 1;
|
|
430
|
+
state.totalTextBytes += file.originalSize;
|
|
431
|
+
if (state.textEntryCount > MAX_OFFICE_TEXT_ENTRY_COUNT ||
|
|
432
|
+
state.totalTextBytes > MAX_OFFICE_XML_TOTAL_BYTES) {
|
|
433
|
+
state.rejected = true;
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
function assertOfficeArchiveLimits(state) {
|
|
439
|
+
if (state.rejected) {
|
|
440
|
+
throw new Error("Archive text payload exceeds Ragmir safety limits.");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function xmlEntriesToText(entries, patterns, signal) {
|
|
197
444
|
const parts = [];
|
|
198
445
|
for (const [name, xml] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
446
|
+
throwIfAborted(signal);
|
|
199
447
|
if (patterns.some((pattern) => pattern.test(name))) {
|
|
200
448
|
const text = xmlToText(xml);
|
|
201
449
|
if (text) {
|
|
@@ -227,6 +475,24 @@ function spreadsheetCellToText(value) {
|
|
|
227
475
|
}
|
|
228
476
|
return JSON.stringify(value);
|
|
229
477
|
}
|
|
478
|
+
function spreadsheetColumnName(column) {
|
|
479
|
+
let current = column;
|
|
480
|
+
let name = "";
|
|
481
|
+
while (current > 0) {
|
|
482
|
+
current -= 1;
|
|
483
|
+
name = String.fromCharCode(65 + (current % 26)) + name;
|
|
484
|
+
current = Math.floor(current / 26);
|
|
485
|
+
}
|
|
486
|
+
return name;
|
|
487
|
+
}
|
|
488
|
+
function lastNonEmptyIndex(values) {
|
|
489
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
490
|
+
if (values[index]) {
|
|
491
|
+
return index;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return -1;
|
|
495
|
+
}
|
|
230
496
|
function xmlToText(xml) {
|
|
231
497
|
return normalizeText(decodeXmlEntities(xml
|
|
232
498
|
.replace(/<w:tab\/>/gu, " ")
|
|
@@ -250,8 +516,8 @@ function decodeXmlEntities(input) {
|
|
|
250
516
|
.replace(/'/gu, "'")
|
|
251
517
|
.replace(/&/gu, "&");
|
|
252
518
|
}
|
|
253
|
-
async function parsePdf(
|
|
254
|
-
const buffer = await readFile(
|
|
519
|
+
async function parsePdf(file, options) {
|
|
520
|
+
const buffer = await readFile(file.absolutePath);
|
|
255
521
|
let pdf;
|
|
256
522
|
try {
|
|
257
523
|
pdf = await getDocumentProxy(new Uint8Array(buffer));
|
|
@@ -263,12 +529,11 @@ async function parsePdf(filePath, options) {
|
|
|
263
529
|
}
|
|
264
530
|
throw error;
|
|
265
531
|
}
|
|
532
|
+
const pageTexts = [];
|
|
266
533
|
try {
|
|
267
534
|
if (pdf.numPages > MAX_PDF_PAGES) {
|
|
268
535
|
throw new Error(`PDF has ${pdf.numPages} pages; the safety limit is ${MAX_PDF_PAGES}.`);
|
|
269
536
|
}
|
|
270
|
-
const pageTexts = [];
|
|
271
|
-
let totalCharacters = 0;
|
|
272
537
|
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber += 1) {
|
|
273
538
|
const page = await pdf.getPage(pageNumber);
|
|
274
539
|
let pageText = "";
|
|
@@ -284,28 +549,148 @@ async function parsePdf(filePath, options) {
|
|
|
284
549
|
page.cleanup();
|
|
285
550
|
}
|
|
286
551
|
pageText = normalizePdfPageText(pageText);
|
|
287
|
-
if (!pageText && options.pdfOcrCommand && options.pdfOcrCommand.length > 0) {
|
|
288
|
-
pageText = normalizePdfPageText(await runExternalTextCommand(filePath, {
|
|
289
|
-
command: options.pdfOcrCommand,
|
|
290
|
-
cwd: options.projectRoot,
|
|
291
|
-
label: `OCR command for PDF page ${pageNumber}`,
|
|
292
|
-
timeoutMs: options.pdfOcrTimeoutMs ?? 120_000,
|
|
293
|
-
pathEnvName: "RAGMIR_PDF_PATH",
|
|
294
|
-
pageNumber,
|
|
295
|
-
...(options.signal ? { signal: options.signal } : {}),
|
|
296
|
-
}));
|
|
297
|
-
}
|
|
298
|
-
totalCharacters += pageText.length;
|
|
299
|
-
if (totalCharacters > MAX_PDF_TEXT_CHARACTERS) {
|
|
300
|
-
throw new Error(`PDF text exceeds the ${MAX_PDF_TEXT_CHARACTERS} character safety limit.`);
|
|
301
|
-
}
|
|
302
552
|
pageTexts.push(pageText);
|
|
303
553
|
}
|
|
304
|
-
return joinPdfPages(pageTexts);
|
|
305
554
|
}
|
|
306
555
|
finally {
|
|
307
556
|
await pdf.destroy();
|
|
308
557
|
}
|
|
558
|
+
const blankPages = pageTexts.flatMap((text, index) => (text ? [] : [index + 1]));
|
|
559
|
+
const ocr = blankPages.length > 0 && options.pdfOcrCommand && options.pdfOcrCommand.length > 0
|
|
560
|
+
? await fillPdfOcrPages(file, pageTexts, blankPages, options)
|
|
561
|
+
: undefined;
|
|
562
|
+
const totalCharacters = pageTexts.reduce((total, pageText) => total + pageText.length, 0);
|
|
563
|
+
if (totalCharacters > MAX_PDF_TEXT_CHARACTERS) {
|
|
564
|
+
throw new Error(`PDF text exceeds the ${MAX_PDF_TEXT_CHARACTERS} character safety limit.`);
|
|
565
|
+
}
|
|
566
|
+
return { ...joinPdfPages(pageTexts), ...(ocr ? { ocr } : {}) };
|
|
567
|
+
}
|
|
568
|
+
async function fillPdfOcrPages(file, pageTexts, blankPages, options) {
|
|
569
|
+
const startedAt = performance.now();
|
|
570
|
+
const command = options.pdfOcrCommand ?? [];
|
|
571
|
+
const projectRoot = options.projectRoot ?? path.dirname(file.absolutePath);
|
|
572
|
+
const commandIdentity = await pdfOcrCommandIdentity(command, projectRoot);
|
|
573
|
+
const metrics = {
|
|
574
|
+
pages: blankPages.length,
|
|
575
|
+
cacheHits: 0,
|
|
576
|
+
cacheMisses: 0,
|
|
577
|
+
batches: 0,
|
|
578
|
+
subprocesses: commandIdentity.subprocesses,
|
|
579
|
+
durationMs: 0,
|
|
580
|
+
};
|
|
581
|
+
const missingPages = [];
|
|
582
|
+
for (const pageNumber of blankPages) {
|
|
583
|
+
const cached = await readPdfOcrCache(projectRoot, pdfOcrCacheIdentity(file.checksum, pageNumber, commandIdentity));
|
|
584
|
+
if (cached === null) {
|
|
585
|
+
metrics.cacheMisses += 1;
|
|
586
|
+
missingPages.push(pageNumber);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
metrics.cacheHits += 1;
|
|
590
|
+
pageTexts[pageNumber - 1] = cached;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (commandIdentity.supportsBatch) {
|
|
594
|
+
for (const batch of pdfOcrPageBatches(missingPages)) {
|
|
595
|
+
const output = await runExternalTextCommand(file.absolutePath, {
|
|
596
|
+
command,
|
|
597
|
+
cwd: options.projectRoot,
|
|
598
|
+
label: `OCR command for PDF pages ${batch.join(",")}`,
|
|
599
|
+
timeoutMs: options.pdfOcrTimeoutMs ?? 120_000,
|
|
600
|
+
pathEnvName: "RAGMIR_PDF_PATH",
|
|
601
|
+
pageNumbers: batch,
|
|
602
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
603
|
+
});
|
|
604
|
+
const result = parsePdfOcrBatchOutput(output, batch);
|
|
605
|
+
metrics.batches += 1;
|
|
606
|
+
metrics.subprocesses += 1 + result.subprocesses;
|
|
607
|
+
for (const page of result.pages) {
|
|
608
|
+
const text = normalizePdfPageText(page.text);
|
|
609
|
+
pageTexts[page.page - 1] = text;
|
|
610
|
+
await writePdfOcrCache(projectRoot, pdfOcrCacheIdentity(file.checksum, page.page, commandIdentity), text);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
for (const pageNumber of missingPages) {
|
|
616
|
+
const text = normalizePdfPageText(await runExternalTextCommand(file.absolutePath, {
|
|
617
|
+
command,
|
|
618
|
+
cwd: options.projectRoot,
|
|
619
|
+
label: `OCR command for PDF page ${pageNumber}`,
|
|
620
|
+
timeoutMs: options.pdfOcrTimeoutMs ?? 120_000,
|
|
621
|
+
pathEnvName: "RAGMIR_PDF_PATH",
|
|
622
|
+
pageNumber,
|
|
623
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
624
|
+
}));
|
|
625
|
+
metrics.batches += 1;
|
|
626
|
+
metrics.subprocesses += 1;
|
|
627
|
+
pageTexts[pageNumber - 1] = text;
|
|
628
|
+
await writePdfOcrCache(projectRoot, pdfOcrCacheIdentity(file.checksum, pageNumber, commandIdentity), text);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
metrics.durationMs = Math.round((performance.now() - startedAt) * 1_000) / 1_000;
|
|
632
|
+
return metrics;
|
|
633
|
+
}
|
|
634
|
+
function pdfOcrCacheIdentity(sourceChecksum, page, command) {
|
|
635
|
+
return {
|
|
636
|
+
sourceChecksum,
|
|
637
|
+
page,
|
|
638
|
+
engine: command.engine,
|
|
639
|
+
engineVersion: command.engineVersion,
|
|
640
|
+
language: command.language,
|
|
641
|
+
dpi: command.dpi,
|
|
642
|
+
parserPolicy: PDF_OCR_PARSER_POLICY,
|
|
643
|
+
commandFingerprint: command.commandFingerprint,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function pdfOcrPageBatches(pages) {
|
|
647
|
+
const batches = [];
|
|
648
|
+
for (const page of pages) {
|
|
649
|
+
const current = batches.at(-1);
|
|
650
|
+
if (!current ||
|
|
651
|
+
current.length >= MAX_OCR_BATCH_PAGES ||
|
|
652
|
+
page - (current[0] ?? page) >= MAX_OCR_BATCH_PAGES) {
|
|
653
|
+
batches.push([page]);
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
current.push(page);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return batches;
|
|
660
|
+
}
|
|
661
|
+
function parsePdfOcrBatchOutput(output, requestedPages) {
|
|
662
|
+
let value;
|
|
663
|
+
try {
|
|
664
|
+
value = JSON.parse(output);
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
throw new Error("Batched PDF OCR command returned invalid JSON.");
|
|
668
|
+
}
|
|
669
|
+
if (!isRecord(value)) {
|
|
670
|
+
throw new Error("Batched PDF OCR command returned an invalid result.");
|
|
671
|
+
}
|
|
672
|
+
if (!Number.isInteger(value.subprocesses) ||
|
|
673
|
+
Number(value.subprocesses) < 0 ||
|
|
674
|
+
Number(value.subprocesses) > 100 ||
|
|
675
|
+
!Array.isArray(value.pages)) {
|
|
676
|
+
throw new Error("Batched PDF OCR command returned invalid process diagnostics.");
|
|
677
|
+
}
|
|
678
|
+
const pages = value.pages.flatMap((entry) => {
|
|
679
|
+
if (isRecord(entry) && Number.isInteger(entry.page) && typeof entry.text === "string") {
|
|
680
|
+
return [
|
|
681
|
+
{
|
|
682
|
+
page: Number(entry.page),
|
|
683
|
+
text: entry.text,
|
|
684
|
+
},
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
return [];
|
|
688
|
+
});
|
|
689
|
+
if (pages.length !== requestedPages.length ||
|
|
690
|
+
pages.some((page, index) => page.page !== requestedPages[index])) {
|
|
691
|
+
throw new Error("Batched PDF OCR command returned pages outside the requested ordered batch.");
|
|
692
|
+
}
|
|
693
|
+
return { pages, subprocesses: Number(value.subprocesses) };
|
|
309
694
|
}
|
|
310
695
|
function isPdfTextItem(value) {
|
|
311
696
|
return (typeof value === "object" && value !== null && "str" in value && typeof value.str === "string");
|
|
@@ -316,15 +701,23 @@ function normalizePdfPageText(text) {
|
|
|
316
701
|
function joinPdfPages(pageTexts) {
|
|
317
702
|
let text = "";
|
|
318
703
|
const pages = [];
|
|
704
|
+
const regions = [];
|
|
319
705
|
for (const [index, pageText] of pageTexts.entries()) {
|
|
320
706
|
if (index > 0) {
|
|
321
707
|
text += "\n\n";
|
|
322
708
|
}
|
|
323
709
|
const charStart = text.length;
|
|
324
710
|
text += pageText;
|
|
325
|
-
|
|
711
|
+
const pageNumber = index + 1;
|
|
712
|
+
pages.push({ pageNumber, charStart, charEnd: text.length });
|
|
713
|
+
regions.push({
|
|
714
|
+
charStart,
|
|
715
|
+
charEnd: text.length,
|
|
716
|
+
contextPath: `Page ${pageNumber}`,
|
|
717
|
+
location: { kind: "page", start: pageNumber, end: pageNumber },
|
|
718
|
+
});
|
|
326
719
|
}
|
|
327
|
-
return { text, pages };
|
|
720
|
+
return { text, pages, regions };
|
|
328
721
|
}
|
|
329
722
|
async function runExternalTextCommand(filePath, options) {
|
|
330
723
|
throwIfAborted(options.signal);
|
|
@@ -336,8 +729,9 @@ async function runExternalTextCommand(filePath, options) {
|
|
|
336
729
|
const hasInputPlaceholder = command.some((part) => part.includes("{input}"));
|
|
337
730
|
const args = configuredArgs.map((part) => part.replaceAll("{input}", filePath));
|
|
338
731
|
const pageNumber = options.pageNumber === undefined ? "" : String(options.pageNumber);
|
|
732
|
+
const pageNumbers = options.pageNumbers?.join(",") ?? "";
|
|
339
733
|
for (const [index, arg] of args.entries()) {
|
|
340
|
-
args[index] = arg.replaceAll("{page}", pageNumber);
|
|
734
|
+
args[index] = arg.replaceAll("{page}", pageNumber).replaceAll("{pages}", pageNumbers);
|
|
341
735
|
}
|
|
342
736
|
if (!hasInputPlaceholder) {
|
|
343
737
|
args.push(filePath);
|
|
@@ -350,6 +744,7 @@ async function runExternalTextCommand(filePath, options) {
|
|
|
350
744
|
...externalCommandEnvironment(),
|
|
351
745
|
[options.pathEnvName]: filePath,
|
|
352
746
|
...(options.pageNumber === undefined ? {} : { RAGMIR_PDF_PAGE: pageNumber }),
|
|
747
|
+
...(options.pageNumbers === undefined ? {} : { RAGMIR_PDF_PAGES: pageNumbers }),
|
|
353
748
|
},
|
|
354
749
|
stdio: ["ignore", "pipe", "pipe"],
|
|
355
750
|
});
|
|
@@ -478,10 +873,12 @@ function externalCommandEnvironment() {
|
|
|
478
873
|
return value === undefined ? [] : [[name, value]];
|
|
479
874
|
}));
|
|
480
875
|
}
|
|
481
|
-
function normalizeText(input) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
.replace(
|
|
876
|
+
function normalizeText(input, preserveSourceLines = false) {
|
|
877
|
+
const normalized = input.replace(LONG_BASE64_TEXT_PATTERN, " ").replace(/\r\n?/gu, "\n");
|
|
878
|
+
if (preserveSourceLines) {
|
|
879
|
+
return normalized.replace(/[ \t]+\n/gu, "\n").trimEnd();
|
|
880
|
+
}
|
|
881
|
+
return normalized
|
|
485
882
|
.replace(/[ \t]+\n/g, "\n")
|
|
486
883
|
.replace(/\n{4,}/g, "\n\n\n")
|
|
487
884
|
.trim();
|