@polishedapps/doc-splitter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/doc-splitter.js +5 -0
- package/dist/main.js +2294 -0
- package/package.json +49 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,2294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// apps/cli/src/main.ts
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { resolve as resolve4 } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
// packages/core/src/formats/document-format.ts
|
|
9
|
+
import { extname } from "node:path";
|
|
10
|
+
|
|
11
|
+
// packages/core/src/issues.ts
|
|
12
|
+
function immutableWarnings(warnings) {
|
|
13
|
+
return Object.freeze([...warnings]);
|
|
14
|
+
}
|
|
15
|
+
function success(value, warnings = []) {
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
ok: true,
|
|
18
|
+
value,
|
|
19
|
+
warnings: immutableWarnings(warnings)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function failure(error, warnings = []) {
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
ok: false,
|
|
25
|
+
error: Object.freeze({ ...error }),
|
|
26
|
+
warnings: immutableWarnings(warnings)
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function warning(code, message, context) {
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
code,
|
|
32
|
+
severity: "warning",
|
|
33
|
+
message,
|
|
34
|
+
...context === void 0 ? {} : { context: Object.freeze({ ...context }) }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// packages/core/src/formats/document-format.ts
|
|
39
|
+
var DOCUMENT_FORMATS = Object.freeze([
|
|
40
|
+
Object.freeze({
|
|
41
|
+
format: "docx",
|
|
42
|
+
displayName: "DOCX",
|
|
43
|
+
extensions: Object.freeze([".docx"])
|
|
44
|
+
}),
|
|
45
|
+
Object.freeze({
|
|
46
|
+
format: "pdf",
|
|
47
|
+
displayName: "PDF",
|
|
48
|
+
extensions: Object.freeze([".pdf"])
|
|
49
|
+
})
|
|
50
|
+
]);
|
|
51
|
+
function detectDocumentFormat(inputFilePath) {
|
|
52
|
+
const extension = extname(inputFilePath).toLowerCase();
|
|
53
|
+
const definition = DOCUMENT_FORMATS.find(
|
|
54
|
+
(candidate) => candidate.extensions.includes(extension)
|
|
55
|
+
);
|
|
56
|
+
if (definition === void 0) {
|
|
57
|
+
return failure({
|
|
58
|
+
code: "unsupported_format",
|
|
59
|
+
message: "Unsupported input format. Supported extensions are .docx and .pdf.",
|
|
60
|
+
context: Object.freeze({ extension })
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return success(definition.format);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// packages/core/src/inspection/inspect-document.ts
|
|
67
|
+
import { readFile } from "node:fs/promises";
|
|
68
|
+
import { basename, resolve } from "node:path";
|
|
69
|
+
|
|
70
|
+
// packages/core/src/formats/docx/docx-adapter.ts
|
|
71
|
+
import {
|
|
72
|
+
Document,
|
|
73
|
+
Paragraph,
|
|
74
|
+
StructuredDocumentTag,
|
|
75
|
+
Table,
|
|
76
|
+
TableOfContentsElement
|
|
77
|
+
} from "docxmlater";
|
|
78
|
+
|
|
79
|
+
// packages/core/src/formats/document-format-adapter.ts
|
|
80
|
+
var InspectionCallbackError = class extends Error {
|
|
81
|
+
cause;
|
|
82
|
+
constructor(cause) {
|
|
83
|
+
super("The inspection progress callback failed.");
|
|
84
|
+
this.name = "InspectionCallbackError";
|
|
85
|
+
this.cause = cause;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
function reportInspectionProgress(callbacks, progress) {
|
|
89
|
+
try {
|
|
90
|
+
callbacks?.onProgress?.(Object.freeze({ ...progress }));
|
|
91
|
+
} catch (cause) {
|
|
92
|
+
throw new InspectionCallbackError(cause);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// packages/core/src/formats/docx/docx-adapter.ts
|
|
97
|
+
function normalizeOutlineLevel(outlineLevel) {
|
|
98
|
+
return outlineLevel !== void 0 && Number.isInteger(outlineLevel) && outlineLevel >= 0 && outlineLevel <= 8 ? outlineLevel + 1 : null;
|
|
99
|
+
}
|
|
100
|
+
function parseHeadingLevel(value) {
|
|
101
|
+
if (value === void 0 || value.trim().length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const digits = [...value].filter((character) => /\d/u.test(character)).join("");
|
|
105
|
+
const level = Number.parseInt(digits, 10);
|
|
106
|
+
const normalized = value.replaceAll(" ", "");
|
|
107
|
+
return normalized.toLowerCase().includes("heading") && level > 0 ? level : null;
|
|
108
|
+
}
|
|
109
|
+
function buildStyleDefinitions(styles) {
|
|
110
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const style of styles) {
|
|
112
|
+
if (style.getType() !== "paragraph") {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const styleId = style.getStyleId();
|
|
116
|
+
if (styleId.trim().length === 0) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const properties = style.getProperties();
|
|
120
|
+
definitions.set(styleId.toLowerCase(), {
|
|
121
|
+
basedOnStyleId: properties.basedOn,
|
|
122
|
+
explicitLevel: normalizeOutlineLevel(
|
|
123
|
+
style.getParagraphFormatting()?.outlineLevel
|
|
124
|
+
),
|
|
125
|
+
fallbackLevel: parseHeadingLevel(style.getName()) ?? parseHeadingLevel(styleId)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return definitions;
|
|
129
|
+
}
|
|
130
|
+
function resolveStyleLevel(styleId, definitions, visitedStyleIds) {
|
|
131
|
+
const normalizedId = styleId.toLowerCase();
|
|
132
|
+
const definition = definitions.get(normalizedId);
|
|
133
|
+
if (definition === void 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (definition.explicitLevel !== null) {
|
|
137
|
+
return definition.explicitLevel;
|
|
138
|
+
}
|
|
139
|
+
if (definition.basedOnStyleId !== void 0 && definition.basedOnStyleId.trim().length > 0 && !visitedStyleIds.has(normalizedId)) {
|
|
140
|
+
visitedStyleIds.add(normalizedId);
|
|
141
|
+
const inheritedLevel = resolveStyleLevel(
|
|
142
|
+
definition.basedOnStyleId,
|
|
143
|
+
definitions,
|
|
144
|
+
visitedStyleIds
|
|
145
|
+
);
|
|
146
|
+
if (inheritedLevel !== null) {
|
|
147
|
+
return inheritedLevel;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return definition.fallbackLevel;
|
|
151
|
+
}
|
|
152
|
+
function resolveParagraphLevel(paragraph, definitions) {
|
|
153
|
+
const directOutlineLevel = normalizeOutlineLevel(
|
|
154
|
+
paragraph.getFormatting().outlineLevel
|
|
155
|
+
);
|
|
156
|
+
if (directOutlineLevel !== null) {
|
|
157
|
+
return directOutlineLevel;
|
|
158
|
+
}
|
|
159
|
+
const styleId = paragraph.getStyle();
|
|
160
|
+
if (styleId === void 0) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return resolveStyleLevel(styleId, definitions, /* @__PURE__ */ new Set()) ?? parseHeadingLevel(styleId);
|
|
164
|
+
}
|
|
165
|
+
function hasVisibleText(value) {
|
|
166
|
+
return value.trim().length > 0;
|
|
167
|
+
}
|
|
168
|
+
function rawXmlHasVisibleText(rawXml) {
|
|
169
|
+
return hasVisibleText(rawXml.replace(/<[^>]*>/gu, " "));
|
|
170
|
+
}
|
|
171
|
+
function hasRawXml(element) {
|
|
172
|
+
return "getRawXml" in element && typeof element.getRawXml === "function";
|
|
173
|
+
}
|
|
174
|
+
function hasSubstantiveContent(element) {
|
|
175
|
+
if (element instanceof Paragraph) {
|
|
176
|
+
return hasVisibleText(element.getText());
|
|
177
|
+
}
|
|
178
|
+
if (element instanceof Table) {
|
|
179
|
+
return element.getRows().some(
|
|
180
|
+
(row) => row.getCells().some((cell) => hasVisibleText(cell.getText()))
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (element instanceof StructuredDocumentTag) {
|
|
184
|
+
return element.getContent().some(hasSubstantiveContent);
|
|
185
|
+
}
|
|
186
|
+
if (element instanceof TableOfContentsElement) {
|
|
187
|
+
return hasVisibleText(element.getTableOfContents().getTitle());
|
|
188
|
+
}
|
|
189
|
+
if (hasRawXml(element)) {
|
|
190
|
+
return rawXmlHasVisibleText(element.getRawXml());
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
async function loadDocx(bytes) {
|
|
195
|
+
return Document.loadFromBuffer(Buffer.from(bytes));
|
|
196
|
+
}
|
|
197
|
+
async function inspectDocx(source, callbacks) {
|
|
198
|
+
let document;
|
|
199
|
+
try {
|
|
200
|
+
document = await loadDocx(source.bytes);
|
|
201
|
+
const bodyElements = document.getBodyElements();
|
|
202
|
+
const styleDefinitions = buildStyleDefinitions(document.getStyles());
|
|
203
|
+
const totalDocumentLength = bodyElements.length + 1;
|
|
204
|
+
const headings = [];
|
|
205
|
+
let contentIndexWithinDocument = 0;
|
|
206
|
+
for (const [indexWithinDocument, element] of bodyElements.entries()) {
|
|
207
|
+
if (hasSubstantiveContent(element)) {
|
|
208
|
+
contentIndexWithinDocument += 1;
|
|
209
|
+
}
|
|
210
|
+
if (element instanceof Paragraph) {
|
|
211
|
+
const level = resolveParagraphLevel(element, styleDefinitions);
|
|
212
|
+
if (level !== null) {
|
|
213
|
+
headings.push(
|
|
214
|
+
Object.freeze({
|
|
215
|
+
text: element.getText(),
|
|
216
|
+
level,
|
|
217
|
+
indexWithinDocument,
|
|
218
|
+
contentIndexWithinDocument
|
|
219
|
+
})
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
reportInspectionProgress(callbacks, {
|
|
224
|
+
completedUnits: indexWithinDocument + 1,
|
|
225
|
+
totalUnits: totalDocumentLength
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
document.getSection();
|
|
229
|
+
reportInspectionProgress(callbacks, {
|
|
230
|
+
completedUnits: totalDocumentLength,
|
|
231
|
+
totalUnits: totalDocumentLength
|
|
232
|
+
});
|
|
233
|
+
return success(
|
|
234
|
+
Object.freeze({
|
|
235
|
+
totalDocumentLength,
|
|
236
|
+
headings: Object.freeze(headings)
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
} catch (cause) {
|
|
240
|
+
if (cause instanceof InspectionCallbackError) {
|
|
241
|
+
throw cause;
|
|
242
|
+
}
|
|
243
|
+
return failure({
|
|
244
|
+
code: "document_unreadable",
|
|
245
|
+
message: "The DOCX document could not be read.",
|
|
246
|
+
context: Object.freeze({ sourceFilePath: source.sourceFilePath }),
|
|
247
|
+
cause
|
|
248
|
+
});
|
|
249
|
+
} finally {
|
|
250
|
+
document?.dispose();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function createDocxSplitDocument(source, startIndexWithinDocument, endIndexWithinDocument) {
|
|
254
|
+
let document;
|
|
255
|
+
try {
|
|
256
|
+
document = await loadDocx(source.bytes);
|
|
257
|
+
for (let index = document.getBodyElementCount() - 1; index >= 0; index -= 1) {
|
|
258
|
+
if (index < startIndexWithinDocument || index >= endIndexWithinDocument) {
|
|
259
|
+
document.removeBodyElementAt(index);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return success(await document.toBuffer());
|
|
263
|
+
} catch (cause) {
|
|
264
|
+
return failure({
|
|
265
|
+
code: "output_write_failed",
|
|
266
|
+
message: "The DOCX split document could not be created.",
|
|
267
|
+
context: Object.freeze({ sourceFilePath: source.sourceFilePath }),
|
|
268
|
+
cause
|
|
269
|
+
});
|
|
270
|
+
} finally {
|
|
271
|
+
document?.dispose();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
var docxAdapter = Object.freeze({
|
|
275
|
+
format: "docx",
|
|
276
|
+
extensions: Object.freeze([".docx"]),
|
|
277
|
+
inspect: inspectDocx,
|
|
278
|
+
createSplitDocument: createDocxSplitDocument
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// packages/core/src/formats/pdf/pdf-adapter.ts
|
|
282
|
+
import {
|
|
283
|
+
PDF,
|
|
284
|
+
PdfArray,
|
|
285
|
+
PdfDict,
|
|
286
|
+
PdfName,
|
|
287
|
+
PdfNumber,
|
|
288
|
+
PdfRef,
|
|
289
|
+
PdfString,
|
|
290
|
+
SecurityError
|
|
291
|
+
} from "@libpdf/core";
|
|
292
|
+
function resolver(document) {
|
|
293
|
+
return (reference) => document.getObject(reference);
|
|
294
|
+
}
|
|
295
|
+
function dereference(document, object) {
|
|
296
|
+
return object instanceof PdfRef ? document.getObject(object) ?? void 0 : object;
|
|
297
|
+
}
|
|
298
|
+
function objectIdentity(raw, resolved) {
|
|
299
|
+
return raw instanceof PdfRef ? raw.toString() : resolved;
|
|
300
|
+
}
|
|
301
|
+
function getOutlineDestination(document, item) {
|
|
302
|
+
const direct = item.get("Dest");
|
|
303
|
+
if (direct !== void 0) {
|
|
304
|
+
return direct;
|
|
305
|
+
}
|
|
306
|
+
const action = item.getDict("A", resolver(document));
|
|
307
|
+
if (action?.getName("S", resolver(document))?.value !== "GoTo") {
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
310
|
+
return action.get("D");
|
|
311
|
+
}
|
|
312
|
+
function collectOutlineEntries(document) {
|
|
313
|
+
const outlines = dereference(document, document.getCatalog().get("Outlines"));
|
|
314
|
+
if (!(outlines instanceof PdfDict)) {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
const entries = [];
|
|
318
|
+
const visited = /* @__PURE__ */ new Set();
|
|
319
|
+
const visitSiblings = (first, level) => {
|
|
320
|
+
let raw = first;
|
|
321
|
+
while (raw !== void 0) {
|
|
322
|
+
const item = dereference(document, raw);
|
|
323
|
+
if (!(item instanceof PdfDict)) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const identity = objectIdentity(raw, item);
|
|
327
|
+
if (visited.has(identity)) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
visited.add(identity);
|
|
331
|
+
if (entries.length >= 1e5) {
|
|
332
|
+
throw new Error("The PDF outline contains too many entries.");
|
|
333
|
+
}
|
|
334
|
+
entries.push(
|
|
335
|
+
Object.freeze({
|
|
336
|
+
title: item.getString("Title", resolver(document))?.asString() ?? "",
|
|
337
|
+
level,
|
|
338
|
+
destination: getOutlineDestination(document, item)
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
visitSiblings(item.get("First"), level + 1);
|
|
342
|
+
raw = item.get("Next");
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
visitSiblings(outlines.get("First"), 1);
|
|
346
|
+
return Object.freeze(entries);
|
|
347
|
+
}
|
|
348
|
+
function namedDestinationKey(object) {
|
|
349
|
+
if (object instanceof PdfString) {
|
|
350
|
+
return object.asString();
|
|
351
|
+
}
|
|
352
|
+
if (object instanceof PdfName) {
|
|
353
|
+
return object.value;
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
function collectNameTree(document, root, output, visited) {
|
|
358
|
+
const dictionary = dereference(document, root);
|
|
359
|
+
if (!(dictionary instanceof PdfDict) || root === void 0) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const identity = objectIdentity(root, dictionary);
|
|
363
|
+
if (visited.has(identity)) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
visited.add(identity);
|
|
367
|
+
const names = dictionary.getArray("Names", resolver(document));
|
|
368
|
+
if (names !== void 0) {
|
|
369
|
+
for (let index = 0; index + 1 < names.length; index += 2) {
|
|
370
|
+
const name = namedDestinationKey(dereference(document, names.at(index)));
|
|
371
|
+
const destination = names.at(index + 1);
|
|
372
|
+
if (name !== null && destination !== void 0 && !output.has(name)) {
|
|
373
|
+
output.set(name, destination);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const children = dictionary.getArray("Kids", resolver(document));
|
|
378
|
+
if (children !== void 0) {
|
|
379
|
+
for (const child of children) {
|
|
380
|
+
collectNameTree(document, child, output, visited);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function buildNamedDestinationIndex(document) {
|
|
385
|
+
const destinations = /* @__PURE__ */ new Map();
|
|
386
|
+
const catalog = document.getCatalog();
|
|
387
|
+
const legacyDestinations = catalog.getDict("Dests", resolver(document));
|
|
388
|
+
if (legacyDestinations !== void 0) {
|
|
389
|
+
for (const [name, destination] of legacyDestinations) {
|
|
390
|
+
destinations.set(name.value, destination);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const names = catalog.getDict("Names", resolver(document));
|
|
394
|
+
if (names !== void 0) {
|
|
395
|
+
collectNameTree(
|
|
396
|
+
document,
|
|
397
|
+
names.get("Dests"),
|
|
398
|
+
destinations,
|
|
399
|
+
/* @__PURE__ */ new Set()
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
return Object.freeze({ destinations });
|
|
403
|
+
}
|
|
404
|
+
function unwrapDestination(document, rawDestination, namedDestinations, visitedNames = /* @__PURE__ */ new Set()) {
|
|
405
|
+
const destination = dereference(document, rawDestination);
|
|
406
|
+
if (destination instanceof PdfArray) {
|
|
407
|
+
return { array: destination };
|
|
408
|
+
}
|
|
409
|
+
if (destination instanceof PdfDict) {
|
|
410
|
+
return unwrapDestination(
|
|
411
|
+
document,
|
|
412
|
+
destination.get("D"),
|
|
413
|
+
namedDestinations,
|
|
414
|
+
visitedNames
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const name = namedDestinationKey(destination);
|
|
418
|
+
if (name !== null) {
|
|
419
|
+
if (visitedNames.has(name)) {
|
|
420
|
+
return { reason: `Named destination '${name}' contains a cycle.` };
|
|
421
|
+
}
|
|
422
|
+
const named = namedDestinations.destinations.get(name);
|
|
423
|
+
if (named === void 0) {
|
|
424
|
+
return { reason: `Named destination '${name}' was not found.` };
|
|
425
|
+
}
|
|
426
|
+
visitedNames.add(name);
|
|
427
|
+
return unwrapDestination(document, named, namedDestinations, visitedNames);
|
|
428
|
+
}
|
|
429
|
+
return { reason: "The outline entry has no resolvable local destination." };
|
|
430
|
+
}
|
|
431
|
+
function resolveDestinationPage(document, rawDestination, namedDestinations, pageIndexesByReference) {
|
|
432
|
+
const unwrapped = unwrapDestination(
|
|
433
|
+
document,
|
|
434
|
+
rawDestination,
|
|
435
|
+
namedDestinations
|
|
436
|
+
);
|
|
437
|
+
if (unwrapped.array === void 0) {
|
|
438
|
+
return {
|
|
439
|
+
reason: unwrapped.reason ?? "The outline destination could not be resolved."
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
const pageTarget = unwrapped.array.at(0);
|
|
443
|
+
if (pageTarget instanceof PdfRef) {
|
|
444
|
+
const pageIndex = pageIndexesByReference.get(pageTarget.toString());
|
|
445
|
+
return pageIndex === void 0 ? { reason: `Page reference '${pageTarget.toString()}' was not found.` } : { pageIndex };
|
|
446
|
+
}
|
|
447
|
+
const resolvedPage = dereference(document, pageTarget);
|
|
448
|
+
if (resolvedPage instanceof PdfDict) {
|
|
449
|
+
const pageReference = document.context.getRef(resolvedPage);
|
|
450
|
+
const pageIndex = pageReference === null ? void 0 : pageIndexesByReference.get(pageReference.toString());
|
|
451
|
+
return pageIndex === void 0 ? { reason: "The destination page dictionary is not in the page tree." } : { pageIndex };
|
|
452
|
+
}
|
|
453
|
+
if (resolvedPage instanceof PdfNumber && Number.isInteger(resolvedPage.value) && resolvedPage.value >= 0 && resolvedPage.value < document.getPageCount()) {
|
|
454
|
+
return { pageIndex: resolvedPage.value };
|
|
455
|
+
}
|
|
456
|
+
return { reason: "The destination does not identify a document page." };
|
|
457
|
+
}
|
|
458
|
+
function unreadablePdf(source, cause) {
|
|
459
|
+
return failure({
|
|
460
|
+
code: "document_unreadable",
|
|
461
|
+
message: "The PDF document could not be read.",
|
|
462
|
+
context: Object.freeze({ sourceFilePath: source.sourceFilePath }),
|
|
463
|
+
cause
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function unsupportedEncryption(source, cause) {
|
|
467
|
+
return failure({
|
|
468
|
+
code: "encrypted_document_unsupported",
|
|
469
|
+
message: "Encrypted PDF documents are not supported.",
|
|
470
|
+
context: Object.freeze({ sourceFilePath: source.sourceFilePath }),
|
|
471
|
+
...cause === void 0 ? {} : { cause }
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
async function inspectPdf(source, callbacks) {
|
|
475
|
+
try {
|
|
476
|
+
const document = await PDF.load(source.bytes);
|
|
477
|
+
if (document.isEncrypted) {
|
|
478
|
+
return unsupportedEncryption(source);
|
|
479
|
+
}
|
|
480
|
+
const pages = document.getPages();
|
|
481
|
+
const pageIndexesByReference = new Map(
|
|
482
|
+
pages.map((page) => [page.ref.toString(), page.index])
|
|
483
|
+
);
|
|
484
|
+
const namedDestinations = buildNamedDestinationIndex(document);
|
|
485
|
+
const outlineEntries = collectOutlineEntries(document);
|
|
486
|
+
const headings = [];
|
|
487
|
+
const warnings = [];
|
|
488
|
+
for (const [index, entry] of outlineEntries.entries()) {
|
|
489
|
+
const destination = resolveDestinationPage(
|
|
490
|
+
document,
|
|
491
|
+
entry.destination,
|
|
492
|
+
namedDestinations,
|
|
493
|
+
pageIndexesByReference
|
|
494
|
+
);
|
|
495
|
+
if (destination.pageIndex === void 0) {
|
|
496
|
+
warnings.push(
|
|
497
|
+
warning(
|
|
498
|
+
"unresolved_pdf_outline_destination",
|
|
499
|
+
`PDF outline entry '${entry.title || "BLANK"}' was skipped: ${destination.reason ?? "unknown destination error"}`,
|
|
500
|
+
{
|
|
501
|
+
title: entry.title,
|
|
502
|
+
level: entry.level,
|
|
503
|
+
outlineIndex: index,
|
|
504
|
+
reason: destination.reason ?? "unknown destination error"
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
} else {
|
|
509
|
+
headings.push(
|
|
510
|
+
Object.freeze({
|
|
511
|
+
text: entry.title,
|
|
512
|
+
level: entry.level,
|
|
513
|
+
indexWithinDocument: destination.pageIndex,
|
|
514
|
+
contentIndexWithinDocument: destination.pageIndex + 1
|
|
515
|
+
})
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
reportInspectionProgress(callbacks, {
|
|
519
|
+
completedUnits: index + 1,
|
|
520
|
+
totalUnits: outlineEntries.length
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
return success(
|
|
524
|
+
Object.freeze({
|
|
525
|
+
totalDocumentLength: document.getPageCount(),
|
|
526
|
+
headings: Object.freeze(headings)
|
|
527
|
+
}),
|
|
528
|
+
warnings
|
|
529
|
+
);
|
|
530
|
+
} catch (cause) {
|
|
531
|
+
if (cause instanceof InspectionCallbackError) {
|
|
532
|
+
throw cause;
|
|
533
|
+
}
|
|
534
|
+
return cause instanceof SecurityError ? unsupportedEncryption(source, cause) : unreadablePdf(source, cause);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async function createPdfSplitDocument(source, startIndexWithinDocument, endIndexWithinDocument) {
|
|
538
|
+
try {
|
|
539
|
+
const document = await PDF.load(source.bytes);
|
|
540
|
+
for (let index = document.getPageCount() - 1; index >= endIndexWithinDocument; index -= 1) {
|
|
541
|
+
document.removePage(index);
|
|
542
|
+
}
|
|
543
|
+
for (let index = startIndexWithinDocument - 1; index >= 0; index -= 1) {
|
|
544
|
+
document.removePage(index);
|
|
545
|
+
}
|
|
546
|
+
return success(await document.save());
|
|
547
|
+
} catch (cause) {
|
|
548
|
+
return failure({
|
|
549
|
+
code: "output_write_failed",
|
|
550
|
+
message: "The PDF split document could not be created.",
|
|
551
|
+
context: Object.freeze({ sourceFilePath: source.sourceFilePath }),
|
|
552
|
+
cause
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
var pdfAdapter = Object.freeze({
|
|
557
|
+
format: "pdf",
|
|
558
|
+
extensions: Object.freeze([".pdf"]),
|
|
559
|
+
inspect: inspectPdf,
|
|
560
|
+
createSplitDocument: createPdfSplitDocument
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// packages/core/src/formats/adapter-registry.ts
|
|
564
|
+
var adapters = /* @__PURE__ */ new Map([
|
|
565
|
+
["docx", docxAdapter],
|
|
566
|
+
["pdf", pdfAdapter]
|
|
567
|
+
]);
|
|
568
|
+
function getDocumentAdapter(format) {
|
|
569
|
+
const adapter = adapters.get(format);
|
|
570
|
+
if (adapter === void 0) {
|
|
571
|
+
return failure({
|
|
572
|
+
code: "internal_error",
|
|
573
|
+
message: `No document adapter is registered for '${format}'.`,
|
|
574
|
+
context: Object.freeze({ format })
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
return success(adapter);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// packages/core/src/inspection/heading-hierarchy.ts
|
|
581
|
+
function invalidHierarchy(message, context) {
|
|
582
|
+
return failure({
|
|
583
|
+
code: "internal_error",
|
|
584
|
+
message,
|
|
585
|
+
...context === void 0 ? {} : { context }
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function normalizeWhitespace(value) {
|
|
589
|
+
return value.trim().replace(/\s+/gu, " ");
|
|
590
|
+
}
|
|
591
|
+
function validateCandidate(candidate, index, previous) {
|
|
592
|
+
if (!Number.isInteger(candidate.level) || candidate.level < 1) {
|
|
593
|
+
return invalidHierarchy("A heading candidate has an invalid level.", {
|
|
594
|
+
candidateIndex: index,
|
|
595
|
+
level: candidate.level
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
if (!Number.isInteger(candidate.indexWithinDocument) || candidate.indexWithinDocument < 0) {
|
|
599
|
+
return invalidHierarchy(
|
|
600
|
+
"A heading candidate has an invalid index within the document.",
|
|
601
|
+
{
|
|
602
|
+
candidateIndex: index,
|
|
603
|
+
indexWithinDocument: candidate.indexWithinDocument
|
|
604
|
+
}
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
if (!Number.isInteger(candidate.contentIndexWithinDocument) || candidate.contentIndexWithinDocument < 0) {
|
|
608
|
+
return invalidHierarchy(
|
|
609
|
+
"A heading candidate has an invalid content index within the document.",
|
|
610
|
+
{
|
|
611
|
+
candidateIndex: index,
|
|
612
|
+
contentIndexWithinDocument: candidate.contentIndexWithinDocument
|
|
613
|
+
}
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
if (previous !== void 0 && (candidate.indexWithinDocument < previous.indexWithinDocument || candidate.contentIndexWithinDocument < previous.contentIndexWithinDocument)) {
|
|
617
|
+
return invalidHierarchy("Heading candidates are not in document order.", {
|
|
618
|
+
candidateIndex: index
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
return success(void 0);
|
|
622
|
+
}
|
|
623
|
+
function findParentNode(level, lastHeadingByLevel) {
|
|
624
|
+
for (let candidateLevel = level - 1; candidateLevel >= 1; candidateLevel -= 1) {
|
|
625
|
+
const parent = lastHeadingByLevel.get(candidateLevel);
|
|
626
|
+
if (parent !== void 0) {
|
|
627
|
+
return parent;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
function normalizeSingleTopLevelHeading(headings) {
|
|
633
|
+
if (headings.length <= 1 || headings[0].level !== 1 || headings.filter((heading) => heading.level === 1).length !== 1) {
|
|
634
|
+
return Object.freeze({
|
|
635
|
+
headings,
|
|
636
|
+
omittedFirstHeading: false
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
const normalizedHeadings = [];
|
|
640
|
+
const positionsByLevel = /* @__PURE__ */ new Map();
|
|
641
|
+
const lastHeadingByLevel = /* @__PURE__ */ new Map();
|
|
642
|
+
for (const sourceHeading of headings.slice(1)) {
|
|
643
|
+
const level = Math.max(1, sourceHeading.level - 1);
|
|
644
|
+
const positionWithinLevel = (positionsByLevel.get(level) ?? 0) + 1;
|
|
645
|
+
const heading = Object.freeze({
|
|
646
|
+
text: sourceHeading.text,
|
|
647
|
+
normalizedText: sourceHeading.normalizedText,
|
|
648
|
+
level,
|
|
649
|
+
indexWithinDocument: sourceHeading.indexWithinDocument,
|
|
650
|
+
contentIndexWithinDocument: sourceHeading.contentIndexWithinDocument - 1,
|
|
651
|
+
positionWithinLevel,
|
|
652
|
+
parentNode: findParentNode(level, lastHeadingByLevel),
|
|
653
|
+
isBlank: sourceHeading.isBlank
|
|
654
|
+
});
|
|
655
|
+
normalizedHeadings.push(heading);
|
|
656
|
+
positionsByLevel.set(level, positionWithinLevel);
|
|
657
|
+
lastHeadingByLevel.set(level, heading);
|
|
658
|
+
for (const previousLevel of [...lastHeadingByLevel.keys()]) {
|
|
659
|
+
if (previousLevel > level) {
|
|
660
|
+
lastHeadingByLevel.delete(previousLevel);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return Object.freeze({
|
|
665
|
+
headings: Object.freeze(normalizedHeadings),
|
|
666
|
+
omittedFirstHeading: true
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
function buildHeadingHierarchy(input) {
|
|
670
|
+
if (!Number.isInteger(input.totalDocumentLength) || input.totalDocumentLength < 0) {
|
|
671
|
+
return invalidHierarchy(
|
|
672
|
+
"The document has an invalid total document length.",
|
|
673
|
+
{
|
|
674
|
+
totalDocumentLength: input.totalDocumentLength
|
|
675
|
+
}
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
const headings = [];
|
|
679
|
+
const warnings = [];
|
|
680
|
+
const positionsByLevel = /* @__PURE__ */ new Map();
|
|
681
|
+
const lastHeadingByLevel = /* @__PURE__ */ new Map();
|
|
682
|
+
for (const [index, candidate] of input.candidates.entries()) {
|
|
683
|
+
const candidateValidation = validateCandidate(
|
|
684
|
+
candidate,
|
|
685
|
+
index,
|
|
686
|
+
input.candidates[index - 1]
|
|
687
|
+
);
|
|
688
|
+
if (!candidateValidation.ok) {
|
|
689
|
+
return candidateValidation;
|
|
690
|
+
}
|
|
691
|
+
const normalized = normalizeWhitespace(candidate.text);
|
|
692
|
+
const isBlank = normalized.length === 0;
|
|
693
|
+
const positionWithinLevel = (positionsByLevel.get(candidate.level) ?? 0) + 1;
|
|
694
|
+
const heading = Object.freeze({
|
|
695
|
+
text: normalized,
|
|
696
|
+
normalizedText: isBlank ? "BLANK" : normalized,
|
|
697
|
+
level: candidate.level,
|
|
698
|
+
indexWithinDocument: candidate.indexWithinDocument,
|
|
699
|
+
contentIndexWithinDocument: candidate.contentIndexWithinDocument,
|
|
700
|
+
positionWithinLevel,
|
|
701
|
+
parentNode: findParentNode(candidate.level, lastHeadingByLevel),
|
|
702
|
+
isBlank
|
|
703
|
+
});
|
|
704
|
+
headings.push(heading);
|
|
705
|
+
positionsByLevel.set(candidate.level, positionWithinLevel);
|
|
706
|
+
lastHeadingByLevel.set(candidate.level, heading);
|
|
707
|
+
for (const level of [...lastHeadingByLevel.keys()]) {
|
|
708
|
+
if (level > candidate.level) {
|
|
709
|
+
lastHeadingByLevel.delete(level);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (isBlank) {
|
|
713
|
+
warnings.push(
|
|
714
|
+
warning("blank_heading", "A blank heading was retained as BLANK.", {
|
|
715
|
+
headingNumber: index + 1,
|
|
716
|
+
indexWithinDocument: candidate.indexWithinDocument
|
|
717
|
+
})
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const normalization = normalizeSingleTopLevelHeading(
|
|
722
|
+
Object.freeze([...headings])
|
|
723
|
+
);
|
|
724
|
+
const retainedWarnings = normalization.omittedFirstHeading ? warnings.filter(
|
|
725
|
+
(issue) => issue.code !== "blank_heading" || issue.context?.headingNumber !== 1
|
|
726
|
+
) : warnings;
|
|
727
|
+
const maximumLevel = normalization.headings.reduce(
|
|
728
|
+
(maximum, heading) => Math.max(maximum, heading.level),
|
|
729
|
+
0
|
|
730
|
+
);
|
|
731
|
+
return success(
|
|
732
|
+
Object.freeze({
|
|
733
|
+
headings: normalization.headings,
|
|
734
|
+
hasCoverContent: normalization.headings.length > 0 && normalization.headings[0].contentIndexWithinDocument > 1,
|
|
735
|
+
maxHeadingLevel: maximumLevel
|
|
736
|
+
}),
|
|
737
|
+
retainedWarnings
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// packages/core/src/inspection/inspect-document.ts
|
|
742
|
+
function filesystemFailure(sourceFilePath, cause) {
|
|
743
|
+
const code = cause !== null && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
|
|
744
|
+
const notFound = code === "ENOENT";
|
|
745
|
+
return failure({
|
|
746
|
+
code: notFound ? "input_not_found" : "input_not_readable",
|
|
747
|
+
message: notFound ? `Input file was not found: ${sourceFilePath}` : `Input file could not be read: ${sourceFilePath}`,
|
|
748
|
+
context: Object.freeze({ sourceFilePath }),
|
|
749
|
+
cause
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
function internalFailure(cause) {
|
|
753
|
+
const callbackFailure = cause instanceof InspectionCallbackError;
|
|
754
|
+
return failure({
|
|
755
|
+
code: "internal_error",
|
|
756
|
+
message: callbackFailure ? "The inspection progress callback failed." : "Document inspection failed unexpectedly.",
|
|
757
|
+
cause: callbackFailure ? cause.cause : cause
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
function headingArrayIndexFromIssue(issue) {
|
|
761
|
+
if (issue.code !== "blank_heading") {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
const headingNumber = issue.context?.headingNumber;
|
|
765
|
+
if (!Number.isInteger(headingNumber) || headingNumber < 1) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
return headingNumber - 1;
|
|
769
|
+
}
|
|
770
|
+
function mergeWarnings(format, adapterWarnings, hierarchyWarnings) {
|
|
771
|
+
if (format !== "pdf" || adapterWarnings.length === 0) {
|
|
772
|
+
return Object.freeze([...adapterWarnings, ...hierarchyWarnings]);
|
|
773
|
+
}
|
|
774
|
+
const unresolvedIndexes = new Set(
|
|
775
|
+
adapterWarnings.flatMap((issue) => {
|
|
776
|
+
const value = issue.context?.outlineIndex;
|
|
777
|
+
return Number.isInteger(value) ? [value] : [];
|
|
778
|
+
})
|
|
779
|
+
);
|
|
780
|
+
const outlineIndexForHeading = (headingIndex) => {
|
|
781
|
+
let retainedIndex = -1;
|
|
782
|
+
for (let outlineIndex = 0; ; outlineIndex += 1) {
|
|
783
|
+
if (!unresolvedIndexes.has(outlineIndex)) {
|
|
784
|
+
retainedIndex += 1;
|
|
785
|
+
if (retainedIndex === headingIndex) {
|
|
786
|
+
return outlineIndex;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
return Object.freeze(
|
|
792
|
+
[...adapterWarnings, ...hierarchyWarnings].map((issue, stableIndex) => {
|
|
793
|
+
const adapterIndex = issue.context?.outlineIndex;
|
|
794
|
+
const headingIndex = headingArrayIndexFromIssue(issue);
|
|
795
|
+
const sourceIndex = Number.isInteger(adapterIndex) ? adapterIndex : headingIndex === null ? Number.MAX_SAFE_INTEGER : outlineIndexForHeading(headingIndex);
|
|
796
|
+
return { issue, sourceIndex, stableIndex };
|
|
797
|
+
}).sort(
|
|
798
|
+
(left, right) => left.sourceIndex - right.sourceIndex || left.stableIndex - right.stableIndex
|
|
799
|
+
).map(({ issue }) => issue)
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
async function inspectDocument(inputFilePath, callbacks) {
|
|
803
|
+
const sourceFilePath = resolve(inputFilePath);
|
|
804
|
+
const sourceFilename = basename(sourceFilePath);
|
|
805
|
+
const detectedFormat = detectDocumentFormat(inputFilePath);
|
|
806
|
+
if (!detectedFormat.ok) {
|
|
807
|
+
return detectedFormat;
|
|
808
|
+
}
|
|
809
|
+
let bytes;
|
|
810
|
+
try {
|
|
811
|
+
bytes = await readFile(sourceFilePath);
|
|
812
|
+
} catch (cause) {
|
|
813
|
+
return filesystemFailure(sourceFilePath, cause);
|
|
814
|
+
}
|
|
815
|
+
const registeredAdapter = getDocumentAdapter(detectedFormat.value);
|
|
816
|
+
if (!registeredAdapter.ok) {
|
|
817
|
+
return registeredAdapter;
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const adapterResult = await registeredAdapter.value.inspect(
|
|
821
|
+
{ sourceFilePath, bytes },
|
|
822
|
+
callbacks
|
|
823
|
+
);
|
|
824
|
+
if (!adapterResult.ok) {
|
|
825
|
+
return adapterResult;
|
|
826
|
+
}
|
|
827
|
+
const hierarchyResult = buildHeadingHierarchy({
|
|
828
|
+
format: detectedFormat.value,
|
|
829
|
+
totalDocumentLength: adapterResult.value.totalDocumentLength,
|
|
830
|
+
candidates: adapterResult.value.headings
|
|
831
|
+
});
|
|
832
|
+
if (!hierarchyResult.ok) {
|
|
833
|
+
return failure(hierarchyResult.error, adapterResult.warnings);
|
|
834
|
+
}
|
|
835
|
+
const warnings = mergeWarnings(
|
|
836
|
+
detectedFormat.value,
|
|
837
|
+
adapterResult.warnings,
|
|
838
|
+
hierarchyResult.warnings
|
|
839
|
+
);
|
|
840
|
+
const inspection = Object.freeze({
|
|
841
|
+
format: detectedFormat.value,
|
|
842
|
+
sourceFilePath,
|
|
843
|
+
sourceFilename,
|
|
844
|
+
sourceBytes: bytes,
|
|
845
|
+
totalDocumentLength: adapterResult.value.totalDocumentLength,
|
|
846
|
+
hasCoverContent: hierarchyResult.value.hasCoverContent,
|
|
847
|
+
maxHeadingLevel: hierarchyResult.value.maxHeadingLevel,
|
|
848
|
+
headings: hierarchyResult.value.headings,
|
|
849
|
+
warnings
|
|
850
|
+
});
|
|
851
|
+
return success(inspection, warnings);
|
|
852
|
+
} catch (cause) {
|
|
853
|
+
return internalFailure(cause);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// packages/core/src/planning/filename-rules.ts
|
|
858
|
+
var WINDOWS_RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
859
|
+
"CON",
|
|
860
|
+
"PRN",
|
|
861
|
+
"AUX",
|
|
862
|
+
"NUL",
|
|
863
|
+
"COM1",
|
|
864
|
+
"COM2",
|
|
865
|
+
"COM3",
|
|
866
|
+
"COM4",
|
|
867
|
+
"COM5",
|
|
868
|
+
"COM6",
|
|
869
|
+
"COM7",
|
|
870
|
+
"COM8",
|
|
871
|
+
"COM9",
|
|
872
|
+
"LPT1",
|
|
873
|
+
"LPT2",
|
|
874
|
+
"LPT3",
|
|
875
|
+
"LPT4",
|
|
876
|
+
"LPT5",
|
|
877
|
+
"LPT6",
|
|
878
|
+
"LPT7",
|
|
879
|
+
"LPT8",
|
|
880
|
+
"LPT9"
|
|
881
|
+
]);
|
|
882
|
+
function normalizeStructuralPunctuation(value) {
|
|
883
|
+
let normalized = "";
|
|
884
|
+
for (const character of value) {
|
|
885
|
+
if (character === ":" || character === ";" || character === "|" || character === "/" || character === "\\") {
|
|
886
|
+
normalized += " - ";
|
|
887
|
+
} else if (character === "?" || character === "!" || character === "*" || character === '"' || character === "<" || character === ">" || /[\p{Cc}]/u.test(character)) {
|
|
888
|
+
normalized += " ";
|
|
889
|
+
} else {
|
|
890
|
+
normalized += character;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return normalized;
|
|
894
|
+
}
|
|
895
|
+
function trimFilesystemUnsafeEdges(value) {
|
|
896
|
+
return value.trim().replace(/^\.+|\.+$/gu, "").trim().replace(/^-+|-+$/gu, "").trim();
|
|
897
|
+
}
|
|
898
|
+
function toTitleCase(value) {
|
|
899
|
+
const lower = value.toLocaleLowerCase();
|
|
900
|
+
return lower.replace(
|
|
901
|
+
new RegExp("(^|[^\\p{L}\\p{N}])(\\p{L})", "gu"),
|
|
902
|
+
(_match, prefix, letter) => `${prefix}${letter.toLocaleUpperCase()}`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
function normalizeFilenameComponent(input, transformToTitleCase) {
|
|
906
|
+
let value = input.trim().length === 0 ? "BLANK" : input.trim();
|
|
907
|
+
value = normalizeStructuralPunctuation(value);
|
|
908
|
+
value = value.replace(/\s+/gu, " ").trim();
|
|
909
|
+
value = trimFilesystemUnsafeEdges(value);
|
|
910
|
+
if (transformToTitleCase) {
|
|
911
|
+
value = toTitleCase(value);
|
|
912
|
+
}
|
|
913
|
+
if (WINDOWS_RESERVED_NAMES.has(value.toUpperCase())) {
|
|
914
|
+
value = `${value}_`;
|
|
915
|
+
}
|
|
916
|
+
return value.trim().length === 0 ? "BLANK" : value;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// packages/core/src/planning/plan-split.ts
|
|
920
|
+
function sortedHeadings(headings, sourceOrder) {
|
|
921
|
+
return [...headings].sort(
|
|
922
|
+
(left, right) => left.indexWithinDocument - right.indexWithinDocument || (sourceOrder.get(left) ?? 0) - (sourceOrder.get(right) ?? 0)
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
function buildChildrenByParent(headings, sourceOrder) {
|
|
926
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
927
|
+
for (const heading of headings) {
|
|
928
|
+
if (heading.parentNode === null) {
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
const siblings = childrenByParent.get(heading.parentNode) ?? [];
|
|
932
|
+
siblings.push(heading);
|
|
933
|
+
childrenByParent.set(heading.parentNode, siblings);
|
|
934
|
+
}
|
|
935
|
+
return new Map(
|
|
936
|
+
[...childrenByParent].map(([parent, children]) => [
|
|
937
|
+
parent,
|
|
938
|
+
Object.freeze(sortedHeadings(children, sourceOrder))
|
|
939
|
+
])
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
function needsParentIntroOutput(heading, children) {
|
|
943
|
+
const firstChild = children[0];
|
|
944
|
+
return firstChild !== void 0 && firstChild.contentIndexWithinDocument > heading.contentIndexWithinDocument + 1;
|
|
945
|
+
}
|
|
946
|
+
function collectOutputHeadings(heading, splitAtLevel, childrenByParent, selected) {
|
|
947
|
+
if (heading.level === splitAtLevel) {
|
|
948
|
+
selected.push(heading);
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
const children = childrenByParent.get(heading) ?? [];
|
|
952
|
+
if (children.length === 0) {
|
|
953
|
+
selected.push(heading);
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
let subtreeProducedOutput = false;
|
|
957
|
+
for (const child of children) {
|
|
958
|
+
subtreeProducedOutput = collectOutputHeadings(child, splitAtLevel, childrenByParent, selected) || subtreeProducedOutput;
|
|
959
|
+
}
|
|
960
|
+
if (subtreeProducedOutput && needsParentIntroOutput(heading, children)) {
|
|
961
|
+
selected.push(heading);
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
if (!subtreeProducedOutput) {
|
|
965
|
+
selected.push(heading);
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
function selectOutputHeadings(headings, sourceOrder, childrenByParent, splitAtLevel) {
|
|
971
|
+
const roots = sortedHeadings(
|
|
972
|
+
headings.filter((heading) => heading.parentNode === null),
|
|
973
|
+
sourceOrder
|
|
974
|
+
);
|
|
975
|
+
const selected = [];
|
|
976
|
+
for (const root of roots) {
|
|
977
|
+
collectOutputHeadings(root, splitAtLevel, childrenByParent, selected);
|
|
978
|
+
}
|
|
979
|
+
return Object.freeze(sortedHeadings(selected, sourceOrder));
|
|
980
|
+
}
|
|
981
|
+
function isDescendantOf(candidate, ancestor) {
|
|
982
|
+
let current = candidate.parentNode;
|
|
983
|
+
while (current !== null) {
|
|
984
|
+
if (current === ancestor) {
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
current = current.parentNode;
|
|
988
|
+
}
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
function isFirstSelectedDescendant(heading, ancestor, selected) {
|
|
992
|
+
return selected.find((candidate) => isDescendantOf(candidate, ancestor)) === heading;
|
|
993
|
+
}
|
|
994
|
+
function outputStartIndex(heading, itemIndex, hasCoverContent, selected, selectedSet) {
|
|
995
|
+
let startIndex = itemIndex === 0 && !hasCoverContent ? 0 : heading.indexWithinDocument;
|
|
996
|
+
let ancestor = heading.parentNode;
|
|
997
|
+
while (ancestor !== null && !selectedSet.has(ancestor) && isFirstSelectedDescendant(heading, ancestor, selected)) {
|
|
998
|
+
startIndex = ancestor.indexWithinDocument;
|
|
999
|
+
ancestor = ancestor.parentNode;
|
|
1000
|
+
}
|
|
1001
|
+
return startIndex;
|
|
1002
|
+
}
|
|
1003
|
+
function isParentIntroOutput(heading, splitAtLevel, childrenByParent, selected) {
|
|
1004
|
+
if (heading.level >= splitAtLevel) {
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
const children = childrenByParent.get(heading) ?? [];
|
|
1008
|
+
return needsParentIntroOutput(heading, children) && selected.some(
|
|
1009
|
+
(candidate) => candidate !== heading && isDescendantOf(candidate, heading)
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
function assignSiblingPositions(siblings, childrenByParent, siblingPositions, siblingCounts) {
|
|
1013
|
+
siblings.forEach((heading, index) => {
|
|
1014
|
+
siblingPositions.set(heading, index + 1);
|
|
1015
|
+
siblingCounts.set(heading, siblings.length);
|
|
1016
|
+
assignSiblingPositions(
|
|
1017
|
+
childrenByParent.get(heading) ?? [],
|
|
1018
|
+
childrenByParent,
|
|
1019
|
+
siblingPositions,
|
|
1020
|
+
siblingCounts
|
|
1021
|
+
);
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
function buildHeadingIndexMetrics(headings, sourceOrder, childrenByParent) {
|
|
1025
|
+
const roots = sortedHeadings(
|
|
1026
|
+
headings.filter((heading) => heading.parentNode === null),
|
|
1027
|
+
sourceOrder
|
|
1028
|
+
);
|
|
1029
|
+
const siblingPositions = /* @__PURE__ */ new Map();
|
|
1030
|
+
const siblingCounts = /* @__PURE__ */ new Map();
|
|
1031
|
+
assignSiblingPositions(
|
|
1032
|
+
roots,
|
|
1033
|
+
childrenByParent,
|
|
1034
|
+
siblingPositions,
|
|
1035
|
+
siblingCounts
|
|
1036
|
+
);
|
|
1037
|
+
return { siblingPositions, siblingCounts };
|
|
1038
|
+
}
|
|
1039
|
+
function indexWidth(heading, metrics) {
|
|
1040
|
+
return String(metrics.siblingCounts.get(heading) ?? 1).length;
|
|
1041
|
+
}
|
|
1042
|
+
function siblingPosition(heading, metrics) {
|
|
1043
|
+
return metrics.siblingPositions.get(heading) ?? heading.positionWithinLevel;
|
|
1044
|
+
}
|
|
1045
|
+
function simpleNodeIndex(heading, metrics) {
|
|
1046
|
+
return String(siblingPosition(heading, metrics)).padStart(
|
|
1047
|
+
indexWidth(heading, metrics),
|
|
1048
|
+
"0"
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
function nodeChain(heading) {
|
|
1052
|
+
const nodes = [];
|
|
1053
|
+
let current = heading;
|
|
1054
|
+
while (current !== null) {
|
|
1055
|
+
nodes.unshift(current);
|
|
1056
|
+
current = current.parentNode;
|
|
1057
|
+
}
|
|
1058
|
+
return nodes;
|
|
1059
|
+
}
|
|
1060
|
+
function nodeChainIndex(heading, metrics) {
|
|
1061
|
+
return nodeChain(heading).map((node) => simpleNodeIndex(node, metrics)).join(".");
|
|
1062
|
+
}
|
|
1063
|
+
function parentIntroDisplayIndex(heading, options, metrics) {
|
|
1064
|
+
return options.useMultiLevelIndexInFilenames ? `${nodeChainIndex(heading, metrics)}.0` : "0";
|
|
1065
|
+
}
|
|
1066
|
+
function outputIndex(heading, options, itemIndex, overallWidth, metrics, parentIntro) {
|
|
1067
|
+
if (parentIntro) {
|
|
1068
|
+
return parentIntroDisplayIndex(heading, options, metrics);
|
|
1069
|
+
}
|
|
1070
|
+
if (options.useMultiLevelIndexInFilenames) {
|
|
1071
|
+
return nodeChainIndex(heading, metrics);
|
|
1072
|
+
}
|
|
1073
|
+
if (options.parentHeadingMode !== "notUsed") {
|
|
1074
|
+
return simpleNodeIndex(heading, metrics);
|
|
1075
|
+
}
|
|
1076
|
+
return String(itemIndex).padStart(overallWidth, "0");
|
|
1077
|
+
}
|
|
1078
|
+
function indexedSegment(headingName, heading, options, itemIndex, overallWidth, metrics, parentIntro) {
|
|
1079
|
+
return options.addIndexToFilenames ? `${outputIndex(heading, options, itemIndex, overallWidth, metrics, parentIntro)} - ${headingName}` : headingName;
|
|
1080
|
+
}
|
|
1081
|
+
function indexedParentSegment(headingName, heading, options, metrics) {
|
|
1082
|
+
if (!options.addIndexToFilenames) {
|
|
1083
|
+
return headingName;
|
|
1084
|
+
}
|
|
1085
|
+
const index = options.useMultiLevelIndexInFilenames ? nodeChainIndex(heading, metrics) : simpleNodeIndex(heading, metrics);
|
|
1086
|
+
return `${index} - ${headingName}`;
|
|
1087
|
+
}
|
|
1088
|
+
function parentSegments(heading, options, metrics) {
|
|
1089
|
+
return nodeChain(heading).slice(0, -1).map(
|
|
1090
|
+
(parent) => indexedParentSegment(
|
|
1091
|
+
normalizeFilenameComponent(parent.text, options.transformToTitleCase),
|
|
1092
|
+
parent,
|
|
1093
|
+
options,
|
|
1094
|
+
metrics
|
|
1095
|
+
)
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
function joinFilenameParts(parts) {
|
|
1099
|
+
return parts.filter((part) => part.trim().length > 0).join(" - ");
|
|
1100
|
+
}
|
|
1101
|
+
function buildHeadingRelativePath(input) {
|
|
1102
|
+
const headingName = normalizeFilenameComponent(
|
|
1103
|
+
input.heading.text,
|
|
1104
|
+
input.options.transformToTitleCase
|
|
1105
|
+
);
|
|
1106
|
+
if (input.options.parentHeadingMode === "notUsed") {
|
|
1107
|
+
const parts = input.options.includeOriginalFilename ? [input.originalStem] : [];
|
|
1108
|
+
if (input.options.addIndexToFilenames) {
|
|
1109
|
+
parts.push(
|
|
1110
|
+
outputIndex(
|
|
1111
|
+
input.heading,
|
|
1112
|
+
input.options,
|
|
1113
|
+
input.itemIndex,
|
|
1114
|
+
input.overallWidth,
|
|
1115
|
+
input.metrics,
|
|
1116
|
+
input.parentIntro
|
|
1117
|
+
)
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
parts.push(headingName);
|
|
1121
|
+
return `${joinFilenameParts(parts)}${input.extension}`;
|
|
1122
|
+
}
|
|
1123
|
+
const parents = parentSegments(input.heading, input.options, input.metrics);
|
|
1124
|
+
const headingSegment = indexedSegment(
|
|
1125
|
+
headingName,
|
|
1126
|
+
input.heading,
|
|
1127
|
+
input.options,
|
|
1128
|
+
input.itemIndex,
|
|
1129
|
+
input.overallWidth,
|
|
1130
|
+
input.metrics,
|
|
1131
|
+
input.parentIntro
|
|
1132
|
+
);
|
|
1133
|
+
if (input.options.parentHeadingMode === "inFilename") {
|
|
1134
|
+
return `${joinFilenameParts([
|
|
1135
|
+
...input.options.includeOriginalFilename ? [input.originalStem] : [],
|
|
1136
|
+
...parents,
|
|
1137
|
+
headingSegment
|
|
1138
|
+
])}${input.extension}`;
|
|
1139
|
+
}
|
|
1140
|
+
if (input.parentIntro) {
|
|
1141
|
+
const folders = [
|
|
1142
|
+
...parents,
|
|
1143
|
+
indexedParentSegment(
|
|
1144
|
+
headingName,
|
|
1145
|
+
input.heading,
|
|
1146
|
+
input.options,
|
|
1147
|
+
input.metrics
|
|
1148
|
+
)
|
|
1149
|
+
];
|
|
1150
|
+
const introHeading = input.options.addIndexToFilenames ? `${parentIntroDisplayIndex(input.heading, input.options, input.metrics)} - ${headingName}` : headingName;
|
|
1151
|
+
const filename2 = `${joinFilenameParts([
|
|
1152
|
+
...input.options.includeOriginalFilename ? [input.originalStem] : [],
|
|
1153
|
+
introHeading
|
|
1154
|
+
])}${input.extension}`;
|
|
1155
|
+
return [...folders, filename2].join("/");
|
|
1156
|
+
}
|
|
1157
|
+
const filename = `${joinFilenameParts([
|
|
1158
|
+
...input.options.includeOriginalFilename ? [input.originalStem] : [],
|
|
1159
|
+
headingSegment
|
|
1160
|
+
])}${input.extension}`;
|
|
1161
|
+
return [...parents, filename].join("/");
|
|
1162
|
+
}
|
|
1163
|
+
function rootHeading(heading) {
|
|
1164
|
+
let current = heading;
|
|
1165
|
+
while (current.parentNode !== null) {
|
|
1166
|
+
current = current.parentNode;
|
|
1167
|
+
}
|
|
1168
|
+
return current;
|
|
1169
|
+
}
|
|
1170
|
+
function buildCoverRelativePath(options, originalStem, extension, selected) {
|
|
1171
|
+
const width = options.parentHeadingMode === "notUsed" ? String(selected.length).length : String(new Set(selected.map(rootHeading)).size).length;
|
|
1172
|
+
const cover = options.addIndexToFilenames ? `${"0".padStart(Math.max(1, width), "0")} - Cover` : "Cover";
|
|
1173
|
+
return `${joinFilenameParts([
|
|
1174
|
+
...options.includeOriginalFilename ? [originalStem] : [],
|
|
1175
|
+
cover
|
|
1176
|
+
])}${extension}`;
|
|
1177
|
+
}
|
|
1178
|
+
function filenameParts(sourceFilename, format) {
|
|
1179
|
+
const finalSeparator = Math.max(
|
|
1180
|
+
sourceFilename.lastIndexOf("/"),
|
|
1181
|
+
sourceFilename.lastIndexOf("\\")
|
|
1182
|
+
);
|
|
1183
|
+
const filename = sourceFilename.slice(finalSeparator + 1);
|
|
1184
|
+
const finalDot = filename.lastIndexOf(".");
|
|
1185
|
+
return finalDot > 0 ? { stem: filename.slice(0, finalDot), extension: filename.slice(finalDot) } : { stem: filename, extension: `.${format}` };
|
|
1186
|
+
}
|
|
1187
|
+
function collisionKey(relativePath) {
|
|
1188
|
+
return relativePath.toLowerCase();
|
|
1189
|
+
}
|
|
1190
|
+
function suffixRelativePath(relativePath, suffix) {
|
|
1191
|
+
const slash = relativePath.lastIndexOf("/");
|
|
1192
|
+
const directory = slash < 0 ? "" : relativePath.slice(0, slash + 1);
|
|
1193
|
+
const filename = relativePath.slice(slash + 1);
|
|
1194
|
+
const dot = filename.lastIndexOf(".");
|
|
1195
|
+
const stem = dot < 0 ? filename : filename.slice(0, dot);
|
|
1196
|
+
const extension = dot < 0 ? "" : filename.slice(dot);
|
|
1197
|
+
return `${directory}${stem} (${suffix})${extension}`;
|
|
1198
|
+
}
|
|
1199
|
+
function ensureUnique(relativePath, counters) {
|
|
1200
|
+
const originalKey = collisionKey(relativePath);
|
|
1201
|
+
if (!counters.has(originalKey)) {
|
|
1202
|
+
counters.set(originalKey, 0);
|
|
1203
|
+
return { relativePath, duplicateWarning: null };
|
|
1204
|
+
}
|
|
1205
|
+
let suffix = (counters.get(originalKey) ?? 0) + 1;
|
|
1206
|
+
counters.set(originalKey, suffix);
|
|
1207
|
+
let uniquePath = suffixRelativePath(relativePath, suffix);
|
|
1208
|
+
while (counters.has(collisionKey(uniquePath))) {
|
|
1209
|
+
suffix += 1;
|
|
1210
|
+
uniquePath = suffixRelativePath(relativePath, suffix);
|
|
1211
|
+
}
|
|
1212
|
+
counters.set(collisionKey(uniquePath), 0);
|
|
1213
|
+
return {
|
|
1214
|
+
relativePath: uniquePath,
|
|
1215
|
+
duplicateWarning: warning(
|
|
1216
|
+
"duplicate_path_suffix",
|
|
1217
|
+
`A duplicate output path was renamed to "${uniquePath}".`,
|
|
1218
|
+
Object.freeze({
|
|
1219
|
+
originalRelativePath: relativePath,
|
|
1220
|
+
relativePath: uniquePath
|
|
1221
|
+
})
|
|
1222
|
+
)
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function planningFailure(message, inspection) {
|
|
1226
|
+
return failure(
|
|
1227
|
+
Object.freeze({ code: "invalid_split_options", message }),
|
|
1228
|
+
inspection.warnings
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
function planSplit(inspection, options) {
|
|
1232
|
+
if (!Number.isInteger(options.splitAtLevel) || options.splitAtLevel < 1) {
|
|
1233
|
+
return planningFailure(
|
|
1234
|
+
"The split level must be a positive integer.",
|
|
1235
|
+
inspection
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
if (inspection.headings.length === 0) {
|
|
1239
|
+
return failure(
|
|
1240
|
+
Object.freeze({
|
|
1241
|
+
code: "no_headings",
|
|
1242
|
+
message: "No usable headings were found in the document."
|
|
1243
|
+
}),
|
|
1244
|
+
inspection.warnings
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
const sourceOrder = new Map(
|
|
1248
|
+
inspection.headings.map((heading, index) => [heading, index])
|
|
1249
|
+
);
|
|
1250
|
+
const childrenByParent = buildChildrenByParent(
|
|
1251
|
+
inspection.headings,
|
|
1252
|
+
sourceOrder
|
|
1253
|
+
);
|
|
1254
|
+
const selected = selectOutputHeadings(
|
|
1255
|
+
inspection.headings,
|
|
1256
|
+
sourceOrder,
|
|
1257
|
+
childrenByParent,
|
|
1258
|
+
options.splitAtLevel
|
|
1259
|
+
);
|
|
1260
|
+
const selectedSet = new Set(selected);
|
|
1261
|
+
const metrics = buildHeadingIndexMetrics(
|
|
1262
|
+
inspection.headings,
|
|
1263
|
+
sourceOrder,
|
|
1264
|
+
childrenByParent
|
|
1265
|
+
);
|
|
1266
|
+
const overallWidth = Math.max(1, String(selected.length).length);
|
|
1267
|
+
const sourceName = filenameParts(
|
|
1268
|
+
inspection.sourceFilename,
|
|
1269
|
+
inspection.format
|
|
1270
|
+
);
|
|
1271
|
+
const optionSnapshot = Object.freeze({ ...options });
|
|
1272
|
+
const starts = [];
|
|
1273
|
+
if (inspection.hasCoverContent) {
|
|
1274
|
+
starts.push({ kind: "cover", startIndexWithinDocument: 0 });
|
|
1275
|
+
}
|
|
1276
|
+
selected.forEach((heading, index) => {
|
|
1277
|
+
starts.push({
|
|
1278
|
+
kind: isParentIntroOutput(
|
|
1279
|
+
heading,
|
|
1280
|
+
options.splitAtLevel,
|
|
1281
|
+
childrenByParent,
|
|
1282
|
+
selected
|
|
1283
|
+
) ? "parent-intro" : "section",
|
|
1284
|
+
heading,
|
|
1285
|
+
startIndexWithinDocument: outputStartIndex(
|
|
1286
|
+
heading,
|
|
1287
|
+
index,
|
|
1288
|
+
inspection.hasCoverContent,
|
|
1289
|
+
selected,
|
|
1290
|
+
selectedSet
|
|
1291
|
+
)
|
|
1292
|
+
});
|
|
1293
|
+
});
|
|
1294
|
+
const collisionCounters = /* @__PURE__ */ new Map();
|
|
1295
|
+
const duplicateWarnings = [];
|
|
1296
|
+
const outputs = starts.map((start, index) => {
|
|
1297
|
+
const nextStart = starts[index + 1]?.startIndexWithinDocument;
|
|
1298
|
+
const unnormalizedEnd = nextStart ?? inspection.totalDocumentLength;
|
|
1299
|
+
const endIndexWithinDocument = inspection.format === "pdf" && unnormalizedEnd <= start.startIndexWithinDocument ? Math.min(
|
|
1300
|
+
inspection.totalDocumentLength,
|
|
1301
|
+
start.startIndexWithinDocument + 1
|
|
1302
|
+
) : unnormalizedEnd;
|
|
1303
|
+
const candidatePath = start.kind === "cover" ? buildCoverRelativePath(
|
|
1304
|
+
options,
|
|
1305
|
+
sourceName.stem,
|
|
1306
|
+
sourceName.extension,
|
|
1307
|
+
selected
|
|
1308
|
+
) : buildHeadingRelativePath({
|
|
1309
|
+
heading: start.heading,
|
|
1310
|
+
options,
|
|
1311
|
+
originalStem: sourceName.stem,
|
|
1312
|
+
extension: sourceName.extension,
|
|
1313
|
+
itemIndex: selected.indexOf(start.heading) + 1,
|
|
1314
|
+
overallWidth,
|
|
1315
|
+
metrics,
|
|
1316
|
+
parentIntro: start.kind === "parent-intro"
|
|
1317
|
+
});
|
|
1318
|
+
const unique = ensureUnique(candidatePath, collisionCounters);
|
|
1319
|
+
const outputWarnings = unique.duplicateWarning === null ? Object.freeze([]) : Object.freeze([unique.duplicateWarning]);
|
|
1320
|
+
if (unique.duplicateWarning !== null) {
|
|
1321
|
+
duplicateWarnings.push(unique.duplicateWarning);
|
|
1322
|
+
}
|
|
1323
|
+
return Object.freeze({
|
|
1324
|
+
kind: start.kind,
|
|
1325
|
+
relativePath: unique.relativePath,
|
|
1326
|
+
startIndexWithinDocument: start.startIndexWithinDocument,
|
|
1327
|
+
endIndexWithinDocument,
|
|
1328
|
+
sourceHeading: start.kind === "cover" ? null : start.heading,
|
|
1329
|
+
warnings: outputWarnings
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
1332
|
+
const warnings = Object.freeze([
|
|
1333
|
+
...inspection.warnings,
|
|
1334
|
+
...duplicateWarnings
|
|
1335
|
+
]);
|
|
1336
|
+
const plan = Object.freeze({
|
|
1337
|
+
format: inspection.format,
|
|
1338
|
+
sourceFilePath: inspection.sourceFilePath,
|
|
1339
|
+
sourceFilenameWithoutExtension: sourceName.stem,
|
|
1340
|
+
options: optionSnapshot,
|
|
1341
|
+
outputs: Object.freeze(outputs),
|
|
1342
|
+
warnings
|
|
1343
|
+
});
|
|
1344
|
+
return success(plan, warnings);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// packages/core/src/execution/execute-split.ts
|
|
1348
|
+
import { mkdir, stat, writeFile } from "node:fs/promises";
|
|
1349
|
+
import { dirname, isAbsolute, join, relative, resolve as resolve2, sep } from "node:path";
|
|
1350
|
+
function withPlanWarnings(error, plan) {
|
|
1351
|
+
return failure(error, plan.warnings);
|
|
1352
|
+
}
|
|
1353
|
+
function resolvePlannedPath(rootDirectoryPath, relativePath) {
|
|
1354
|
+
const resolvedPath = resolve2(rootDirectoryPath, ...relativePath.split("/"));
|
|
1355
|
+
const pathWithinRoot = relative(rootDirectoryPath, resolvedPath);
|
|
1356
|
+
if (pathWithinRoot === "" || pathWithinRoot === ".." || pathWithinRoot.startsWith(`..${sep}`) || isAbsolute(pathWithinRoot)) {
|
|
1357
|
+
return failure({
|
|
1358
|
+
code: "unsafe_output_path",
|
|
1359
|
+
message: `The planned output path leaves the selected output directory: ${relativePath}`,
|
|
1360
|
+
context: Object.freeze({ relativePath })
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
return success(resolvedPath);
|
|
1364
|
+
}
|
|
1365
|
+
function nodeErrorCode(cause) {
|
|
1366
|
+
return cause !== null && typeof cause === "object" && "code" in cause ? String(cause.code) : void 0;
|
|
1367
|
+
}
|
|
1368
|
+
async function pathExists(path) {
|
|
1369
|
+
try {
|
|
1370
|
+
await stat(path);
|
|
1371
|
+
return true;
|
|
1372
|
+
} catch (cause) {
|
|
1373
|
+
if (nodeErrorCode(cause) === "ENOENT") {
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
throw cause;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
async function chooseSourceNamedOutputRoot(parentDirectoryPath, sourceFilenameWithoutExtension) {
|
|
1380
|
+
let suffix = 0;
|
|
1381
|
+
while (true) {
|
|
1382
|
+
const directoryName = suffix === 0 ? sourceFilenameWithoutExtension : `${sourceFilenameWithoutExtension} (${suffix})`;
|
|
1383
|
+
const candidate = join(parentDirectoryPath, directoryName);
|
|
1384
|
+
if (!await pathExists(candidate)) {
|
|
1385
|
+
return candidate;
|
|
1386
|
+
}
|
|
1387
|
+
suffix += 1;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
async function preflightDirectDestinations(outputRoot, finalPaths) {
|
|
1391
|
+
for (const finalPath of finalPaths) {
|
|
1392
|
+
if (await pathExists(finalPath)) {
|
|
1393
|
+
return finalPath;
|
|
1394
|
+
}
|
|
1395
|
+
let parentPath = dirname(finalPath);
|
|
1396
|
+
while (parentPath !== outputRoot) {
|
|
1397
|
+
try {
|
|
1398
|
+
const parentStat = await stat(parentPath);
|
|
1399
|
+
if (!parentStat.isDirectory()) {
|
|
1400
|
+
return parentPath;
|
|
1401
|
+
}
|
|
1402
|
+
} catch (cause) {
|
|
1403
|
+
if (nodeErrorCode(cause) !== "ENOENT") {
|
|
1404
|
+
throw cause;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
parentPath = dirname(parentPath);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
function outputDirectoryFailure(message, directoryPath, cause) {
|
|
1413
|
+
return {
|
|
1414
|
+
code: "output_directory_unavailable",
|
|
1415
|
+
message,
|
|
1416
|
+
context: Object.freeze({ directoryPath }),
|
|
1417
|
+
cause
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
function outputWriteFailure(message, outputPath, cause) {
|
|
1421
|
+
return {
|
|
1422
|
+
code: "output_write_failed",
|
|
1423
|
+
message,
|
|
1424
|
+
context: Object.freeze({ outputPath }),
|
|
1425
|
+
...cause === void 0 ? {} : { cause }
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
async function executeSplit(inspection, plan, executionOptions, callbacks) {
|
|
1429
|
+
if (executionOptions.outputDirectoryMode !== "direct" && executionOptions.outputDirectoryMode !== "sourceNamedSubfolder" || executionOptions.selectedOutputDirectoryPath.trim().length === 0) {
|
|
1430
|
+
return withPlanWarnings(
|
|
1431
|
+
{
|
|
1432
|
+
code: "invalid_split_options",
|
|
1433
|
+
message: "A valid output directory and mode are required."
|
|
1434
|
+
},
|
|
1435
|
+
plan
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
const adapterResult = getDocumentAdapter(inspection.format);
|
|
1439
|
+
if (!adapterResult.ok) {
|
|
1440
|
+
return withPlanWarnings(adapterResult.error, plan);
|
|
1441
|
+
}
|
|
1442
|
+
const selectedDirectoryPath = resolve2(
|
|
1443
|
+
executionOptions.selectedOutputDirectoryPath
|
|
1444
|
+
);
|
|
1445
|
+
let finalOutputDirectoryPath;
|
|
1446
|
+
try {
|
|
1447
|
+
await mkdir(selectedDirectoryPath, { recursive: true });
|
|
1448
|
+
finalOutputDirectoryPath = executionOptions.outputDirectoryMode === "direct" ? selectedDirectoryPath : await chooseSourceNamedOutputRoot(
|
|
1449
|
+
selectedDirectoryPath,
|
|
1450
|
+
plan.sourceFilenameWithoutExtension
|
|
1451
|
+
);
|
|
1452
|
+
} catch (cause) {
|
|
1453
|
+
return withPlanWarnings(
|
|
1454
|
+
outputDirectoryFailure(
|
|
1455
|
+
"The selected output directory is unavailable.",
|
|
1456
|
+
selectedDirectoryPath,
|
|
1457
|
+
cause
|
|
1458
|
+
),
|
|
1459
|
+
plan
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
const finalPaths = [];
|
|
1463
|
+
for (const output of plan.outputs) {
|
|
1464
|
+
const resolvedPath = resolvePlannedPath(
|
|
1465
|
+
finalOutputDirectoryPath,
|
|
1466
|
+
output.relativePath
|
|
1467
|
+
);
|
|
1468
|
+
if (!resolvedPath.ok) {
|
|
1469
|
+
return withPlanWarnings(resolvedPath.error, plan);
|
|
1470
|
+
}
|
|
1471
|
+
finalPaths.push(resolvedPath.value);
|
|
1472
|
+
}
|
|
1473
|
+
if (executionOptions.outputDirectoryMode === "direct") {
|
|
1474
|
+
try {
|
|
1475
|
+
const conflictPath = await preflightDirectDestinations(
|
|
1476
|
+
finalOutputDirectoryPath,
|
|
1477
|
+
finalPaths
|
|
1478
|
+
);
|
|
1479
|
+
if (conflictPath !== null) {
|
|
1480
|
+
return withPlanWarnings(
|
|
1481
|
+
outputWriteFailure(
|
|
1482
|
+
"An output destination already exists or is blocked by a file.",
|
|
1483
|
+
conflictPath
|
|
1484
|
+
),
|
|
1485
|
+
plan
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
} catch (cause) {
|
|
1489
|
+
return withPlanWarnings(
|
|
1490
|
+
outputWriteFailure(
|
|
1491
|
+
"An output destination could not be checked.",
|
|
1492
|
+
finalOutputDirectoryPath,
|
|
1493
|
+
cause
|
|
1494
|
+
),
|
|
1495
|
+
plan
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (executionOptions.outputDirectoryMode === "sourceNamedSubfolder") {
|
|
1500
|
+
try {
|
|
1501
|
+
await mkdir(finalOutputDirectoryPath);
|
|
1502
|
+
} catch (cause) {
|
|
1503
|
+
return withPlanWarnings(
|
|
1504
|
+
outputDirectoryFailure(
|
|
1505
|
+
"The generated output directory could not be created.",
|
|
1506
|
+
finalOutputDirectoryPath,
|
|
1507
|
+
cause
|
|
1508
|
+
),
|
|
1509
|
+
plan
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
for (const [index, output] of plan.outputs.entries()) {
|
|
1514
|
+
const finalPath = finalPaths[index];
|
|
1515
|
+
let splitResult;
|
|
1516
|
+
try {
|
|
1517
|
+
splitResult = await adapterResult.value.createSplitDocument(
|
|
1518
|
+
{
|
|
1519
|
+
sourceFilePath: inspection.sourceFilePath,
|
|
1520
|
+
bytes: inspection.sourceBytes
|
|
1521
|
+
},
|
|
1522
|
+
output.startIndexWithinDocument,
|
|
1523
|
+
output.endIndexWithinDocument
|
|
1524
|
+
);
|
|
1525
|
+
} catch (cause) {
|
|
1526
|
+
return withPlanWarnings(
|
|
1527
|
+
{
|
|
1528
|
+
code: "internal_error",
|
|
1529
|
+
message: "Split-document creation failed unexpectedly.",
|
|
1530
|
+
cause
|
|
1531
|
+
},
|
|
1532
|
+
plan
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
if (!splitResult.ok) {
|
|
1536
|
+
return withPlanWarnings(splitResult.error, plan);
|
|
1537
|
+
}
|
|
1538
|
+
try {
|
|
1539
|
+
await mkdir(dirname(finalPath), { recursive: true });
|
|
1540
|
+
await writeFile(finalPath, splitResult.value, { flag: "wx" });
|
|
1541
|
+
} catch (cause) {
|
|
1542
|
+
return withPlanWarnings(
|
|
1543
|
+
outputWriteFailure(
|
|
1544
|
+
"A split document could not be written.",
|
|
1545
|
+
finalPath,
|
|
1546
|
+
cause
|
|
1547
|
+
),
|
|
1548
|
+
plan
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
try {
|
|
1552
|
+
callbacks?.onProgress?.(
|
|
1553
|
+
Object.freeze({
|
|
1554
|
+
completedFiles: index + 1,
|
|
1555
|
+
totalFiles: plan.outputs.length,
|
|
1556
|
+
relativePath: output.relativePath
|
|
1557
|
+
})
|
|
1558
|
+
);
|
|
1559
|
+
} catch (cause) {
|
|
1560
|
+
return withPlanWarnings(
|
|
1561
|
+
{
|
|
1562
|
+
code: "internal_error",
|
|
1563
|
+
message: "The execution progress callback failed.",
|
|
1564
|
+
cause
|
|
1565
|
+
},
|
|
1566
|
+
plan
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
const createdFilePaths = Object.freeze(finalPaths);
|
|
1571
|
+
return success(
|
|
1572
|
+
Object.freeze({
|
|
1573
|
+
finalOutputDirectoryPath,
|
|
1574
|
+
totalFiles: createdFilePaths.length,
|
|
1575
|
+
createdFiles: createdFilePaths.length,
|
|
1576
|
+
createdFilePaths
|
|
1577
|
+
}),
|
|
1578
|
+
plan.warnings
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// packages/core/src/planning/planning-types.ts
|
|
1583
|
+
var DEFAULT_SPLIT_OPTIONS = Object.freeze({
|
|
1584
|
+
splitAtLevel: 1,
|
|
1585
|
+
parentHeadingMode: "notUsed",
|
|
1586
|
+
addIndexToFilenames: true,
|
|
1587
|
+
useMultiLevelIndexInFilenames: false,
|
|
1588
|
+
includeOriginalFilename: false,
|
|
1589
|
+
transformToTitleCase: true
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
// packages/core/src/index.ts
|
|
1593
|
+
var PROJECT_NAME = "DocSplitter";
|
|
1594
|
+
|
|
1595
|
+
// apps/cli/src/main.ts
|
|
1596
|
+
import { Command, CommanderError, Option } from "commander";
|
|
1597
|
+
|
|
1598
|
+
// apps/cli/src/exit-codes.ts
|
|
1599
|
+
var EXIT_SUCCESS = 0;
|
|
1600
|
+
var EXIT_USAGE = 2;
|
|
1601
|
+
var EXIT_UNSUPPORTED_FORMAT = 3;
|
|
1602
|
+
var EXIT_INPUT = 4;
|
|
1603
|
+
var EXIT_NO_HEADINGS = 5;
|
|
1604
|
+
var EXIT_PLANNING = 6;
|
|
1605
|
+
var EXIT_EXECUTION = 7;
|
|
1606
|
+
var EXIT_INTERNAL = 8;
|
|
1607
|
+
var EXIT_INTERRUPTED = 130;
|
|
1608
|
+
function exitCodeForError(code) {
|
|
1609
|
+
switch (code) {
|
|
1610
|
+
case "unsupported_format":
|
|
1611
|
+
return EXIT_UNSUPPORTED_FORMAT;
|
|
1612
|
+
case "input_not_found":
|
|
1613
|
+
case "input_not_readable":
|
|
1614
|
+
case "document_unreadable":
|
|
1615
|
+
case "encrypted_document_unsupported":
|
|
1616
|
+
return EXIT_INPUT;
|
|
1617
|
+
case "no_headings":
|
|
1618
|
+
return EXIT_NO_HEADINGS;
|
|
1619
|
+
case "invalid_split_options":
|
|
1620
|
+
case "split_level_unavailable":
|
|
1621
|
+
case "unsafe_output_path":
|
|
1622
|
+
return EXIT_PLANNING;
|
|
1623
|
+
case "output_directory_unavailable":
|
|
1624
|
+
case "output_write_failed":
|
|
1625
|
+
return EXIT_EXECUTION;
|
|
1626
|
+
case "internal_error":
|
|
1627
|
+
return EXIT_INTERNAL;
|
|
1628
|
+
default:
|
|
1629
|
+
return EXIT_INTERNAL;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// apps/cli/src/presentation/human-output.ts
|
|
1634
|
+
function renderWarnings(warnings) {
|
|
1635
|
+
if (warnings.length === 0) {
|
|
1636
|
+
return [];
|
|
1637
|
+
}
|
|
1638
|
+
return [
|
|
1639
|
+
"",
|
|
1640
|
+
"Warnings:",
|
|
1641
|
+
...warnings.map((issue) => `- [${issue.code}] ${issue.message}`)
|
|
1642
|
+
];
|
|
1643
|
+
}
|
|
1644
|
+
function renderHumanInspection(inspection) {
|
|
1645
|
+
const headingSummary = inspection.headings.length === 1 ? "1 (maximum level " + inspection.maxHeadingLevel + ")" : `${inspection.headings.length} (maximum level ${inspection.maxHeadingLevel})`;
|
|
1646
|
+
const lines = [
|
|
1647
|
+
`Input: ${inspection.sourceFilePath}`,
|
|
1648
|
+
`Format: ${inspection.format.toUpperCase()}`,
|
|
1649
|
+
`Document length: ${inspection.totalDocumentLength}`,
|
|
1650
|
+
`Cover content: ${inspection.hasCoverContent ? "yes" : "no"}`,
|
|
1651
|
+
`Headings: ${headingSummary}`,
|
|
1652
|
+
"",
|
|
1653
|
+
...inspection.headings.map(
|
|
1654
|
+
(heading) => `${" ".repeat(Math.max(0, heading.level - 1))}- ${heading.normalizedText} (level ${heading.level}, document index ${heading.indexWithinDocument})`
|
|
1655
|
+
),
|
|
1656
|
+
...renderWarnings(inspection.warnings)
|
|
1657
|
+
];
|
|
1658
|
+
return `${lines.join("\n").trimEnd()}
|
|
1659
|
+
`;
|
|
1660
|
+
}
|
|
1661
|
+
function renderHumanPlan(plan) {
|
|
1662
|
+
const outputWarnings = new Set(
|
|
1663
|
+
plan.outputs.flatMap((output) => output.warnings)
|
|
1664
|
+
);
|
|
1665
|
+
const planOnlyWarnings = plan.warnings.filter(
|
|
1666
|
+
(issue) => !outputWarnings.has(issue)
|
|
1667
|
+
);
|
|
1668
|
+
const lines = [
|
|
1669
|
+
`Input: ${plan.sourceFilePath}`,
|
|
1670
|
+
`Format: ${plan.format.toUpperCase()}`,
|
|
1671
|
+
`Outputs: ${plan.outputs.length}`,
|
|
1672
|
+
"",
|
|
1673
|
+
...plan.outputs.map((output) => {
|
|
1674
|
+
const warningCodes = output.warnings.map((issue) => issue.code);
|
|
1675
|
+
const warningSuffix = warningCodes.length === 0 ? "" : ` | warnings: ${warningCodes.join(", ")}`;
|
|
1676
|
+
return `- filename: ${JSON.stringify(output.relativePath)} | kind: ${output.kind} | range: [${output.startIndexWithinDocument}, ${output.endIndexWithinDocument})${warningSuffix}`;
|
|
1677
|
+
}),
|
|
1678
|
+
...renderWarnings(planOnlyWarnings)
|
|
1679
|
+
];
|
|
1680
|
+
return `${lines.join("\n").trimEnd()}
|
|
1681
|
+
`;
|
|
1682
|
+
}
|
|
1683
|
+
function renderHumanSplitPreview(plan) {
|
|
1684
|
+
const lines = [
|
|
1685
|
+
"Files to create:",
|
|
1686
|
+
...plan.outputs.map((output) => `- ${JSON.stringify(output.relativePath)}`),
|
|
1687
|
+
...renderWarnings(plan.warnings)
|
|
1688
|
+
];
|
|
1689
|
+
return `${lines.join("\n").trimEnd()}
|
|
1690
|
+
`;
|
|
1691
|
+
}
|
|
1692
|
+
function renderHumanCancellation() {
|
|
1693
|
+
return "Split cancelled.\n";
|
|
1694
|
+
}
|
|
1695
|
+
function renderHumanSplitSuccess(result, warnings = []) {
|
|
1696
|
+
const fileLabel = result.createdFiles === 1 ? "file" : "files";
|
|
1697
|
+
const lines = [
|
|
1698
|
+
`Done. Created ${result.createdFiles} output ${fileLabel}.`,
|
|
1699
|
+
`Output directory: ${result.finalOutputDirectoryPath}`,
|
|
1700
|
+
"Created files:",
|
|
1701
|
+
...result.createdFilePaths.map((path) => `- ${path}`),
|
|
1702
|
+
...renderWarnings(warnings)
|
|
1703
|
+
];
|
|
1704
|
+
return `${lines.join("\n").trimEnd()}
|
|
1705
|
+
`;
|
|
1706
|
+
}
|
|
1707
|
+
function renderHumanError(error) {
|
|
1708
|
+
return `${error.message} [${error.code}]
|
|
1709
|
+
`;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// apps/cli/src/presentation/json-output.ts
|
|
1713
|
+
function serialize(value) {
|
|
1714
|
+
return `${JSON.stringify(value, null, 2)}
|
|
1715
|
+
`;
|
|
1716
|
+
}
|
|
1717
|
+
function renderJsonFailure(command, error, warnings) {
|
|
1718
|
+
return serialize({
|
|
1719
|
+
schemaVersion: 1,
|
|
1720
|
+
command,
|
|
1721
|
+
ok: false,
|
|
1722
|
+
error: {
|
|
1723
|
+
code: error.code,
|
|
1724
|
+
message: error.message
|
|
1725
|
+
},
|
|
1726
|
+
warnings
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
function jsonHeadingId(heading, headingIds) {
|
|
1730
|
+
const headingId = headingIds.get(heading);
|
|
1731
|
+
if (headingId === void 0) {
|
|
1732
|
+
throw new Error("A heading reference is outside the inspection result.");
|
|
1733
|
+
}
|
|
1734
|
+
return headingId;
|
|
1735
|
+
}
|
|
1736
|
+
function toJsonInspectionResult(inspection) {
|
|
1737
|
+
const headingIds = new Map(
|
|
1738
|
+
inspection.headings.map((heading, index) => [
|
|
1739
|
+
heading,
|
|
1740
|
+
`heading-${index + 1}`
|
|
1741
|
+
])
|
|
1742
|
+
);
|
|
1743
|
+
return {
|
|
1744
|
+
format: inspection.format,
|
|
1745
|
+
sourceFilePath: inspection.sourceFilePath,
|
|
1746
|
+
sourceFilename: inspection.sourceFilename,
|
|
1747
|
+
totalDocumentLength: inspection.totalDocumentLength,
|
|
1748
|
+
hasCoverContent: inspection.hasCoverContent,
|
|
1749
|
+
maxHeadingLevel: inspection.maxHeadingLevel,
|
|
1750
|
+
headings: inspection.headings.map((heading) => ({
|
|
1751
|
+
headingId: jsonHeadingId(heading, headingIds),
|
|
1752
|
+
text: heading.text,
|
|
1753
|
+
normalizedText: heading.normalizedText,
|
|
1754
|
+
level: heading.level,
|
|
1755
|
+
indexWithinDocument: heading.indexWithinDocument,
|
|
1756
|
+
contentIndexWithinDocument: heading.contentIndexWithinDocument,
|
|
1757
|
+
positionWithinLevel: heading.positionWithinLevel,
|
|
1758
|
+
parentHeadingId: heading.parentNode === null ? null : jsonHeadingId(heading.parentNode, headingIds),
|
|
1759
|
+
isBlank: heading.isBlank
|
|
1760
|
+
})),
|
|
1761
|
+
warnings: inspection.warnings
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
function toJsonSplitPlan(inspection, plan) {
|
|
1765
|
+
const headingIds = new Map(
|
|
1766
|
+
inspection.headings.map((heading, index) => [
|
|
1767
|
+
heading,
|
|
1768
|
+
`heading-${index + 1}`
|
|
1769
|
+
])
|
|
1770
|
+
);
|
|
1771
|
+
return {
|
|
1772
|
+
format: plan.format,
|
|
1773
|
+
sourceFilePath: plan.sourceFilePath,
|
|
1774
|
+
sourceFilenameWithoutExtension: plan.sourceFilenameWithoutExtension,
|
|
1775
|
+
options: plan.options,
|
|
1776
|
+
outputs: plan.outputs.map((output) => ({
|
|
1777
|
+
kind: output.kind,
|
|
1778
|
+
relativePath: output.relativePath,
|
|
1779
|
+
startIndexWithinDocument: output.startIndexWithinDocument,
|
|
1780
|
+
endIndexWithinDocument: output.endIndexWithinDocument,
|
|
1781
|
+
sourceHeadingId: output.sourceHeading === null ? null : jsonHeadingId(output.sourceHeading, headingIds),
|
|
1782
|
+
warnings: output.warnings
|
|
1783
|
+
})),
|
|
1784
|
+
warnings: plan.warnings
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
function renderJsonInspectionSuccess(inspection, warnings) {
|
|
1788
|
+
return serialize({
|
|
1789
|
+
schemaVersion: 1,
|
|
1790
|
+
command: "inspect",
|
|
1791
|
+
ok: true,
|
|
1792
|
+
data: toJsonInspectionResult(inspection),
|
|
1793
|
+
warnings
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
function renderJsonInspectionFailure(error, warnings) {
|
|
1797
|
+
return renderJsonFailure("inspect", error, warnings);
|
|
1798
|
+
}
|
|
1799
|
+
function renderJsonPlanSuccess(inspection, plan, warnings) {
|
|
1800
|
+
return serialize({
|
|
1801
|
+
schemaVersion: 1,
|
|
1802
|
+
command: "plan",
|
|
1803
|
+
ok: true,
|
|
1804
|
+
data: toJsonSplitPlan(inspection, plan),
|
|
1805
|
+
warnings
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
function renderJsonPlanFailure(error, warnings) {
|
|
1809
|
+
return renderJsonFailure("plan", error, warnings);
|
|
1810
|
+
}
|
|
1811
|
+
function renderJsonSplitSuccess(inspection, selectedOutputDirectoryPath, outputDirectoryMode, plan, result, warnings) {
|
|
1812
|
+
return serialize({
|
|
1813
|
+
schemaVersion: 1,
|
|
1814
|
+
command: "split",
|
|
1815
|
+
ok: true,
|
|
1816
|
+
data: {
|
|
1817
|
+
sourceFilePath: inspection.sourceFilePath,
|
|
1818
|
+
selectedOutputDirectoryPath,
|
|
1819
|
+
outputDirectoryMode,
|
|
1820
|
+
finalOutputDirectoryPath: result.finalOutputDirectoryPath,
|
|
1821
|
+
totalFiles: result.totalFiles,
|
|
1822
|
+
createdFiles: result.createdFiles,
|
|
1823
|
+
createdRelativePaths: plan.outputs.map((output) => output.relativePath)
|
|
1824
|
+
},
|
|
1825
|
+
warnings
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
function renderJsonSplitFailure(error, warnings) {
|
|
1829
|
+
return renderJsonFailure("split", error, warnings);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// apps/cli/src/commands/inspect-command.ts
|
|
1833
|
+
var processWriters = {
|
|
1834
|
+
stdout: (text) => process.stdout.write(text),
|
|
1835
|
+
stderr: (text) => process.stderr.write(text)
|
|
1836
|
+
};
|
|
1837
|
+
function noHeadingsError() {
|
|
1838
|
+
return Object.freeze({
|
|
1839
|
+
code: "no_headings",
|
|
1840
|
+
message: "No usable headings were found in the document."
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
function internalError(cause) {
|
|
1844
|
+
return Object.freeze({
|
|
1845
|
+
code: "internal_error",
|
|
1846
|
+
message: "The inspect command failed unexpectedly.",
|
|
1847
|
+
cause
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
function presentFailure(error, warnings, options, writers) {
|
|
1851
|
+
if (options.json === true) {
|
|
1852
|
+
writers.stdout(renderJsonInspectionFailure(error, warnings));
|
|
1853
|
+
} else {
|
|
1854
|
+
writers.stderr(renderHumanError(error));
|
|
1855
|
+
}
|
|
1856
|
+
return exitCodeForError(error.code);
|
|
1857
|
+
}
|
|
1858
|
+
async function runInspectCommand(inputFilePath, options = {}, writers = processWriters) {
|
|
1859
|
+
try {
|
|
1860
|
+
const result = await inspectDocument(inputFilePath);
|
|
1861
|
+
if (!result.ok) {
|
|
1862
|
+
return presentFailure(result.error, result.warnings, options, writers);
|
|
1863
|
+
}
|
|
1864
|
+
if (result.value.headings.length === 0) {
|
|
1865
|
+
return presentFailure(
|
|
1866
|
+
noHeadingsError(),
|
|
1867
|
+
result.warnings,
|
|
1868
|
+
options,
|
|
1869
|
+
writers
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
writers.stdout(
|
|
1873
|
+
options.json === true ? renderJsonInspectionSuccess(result.value, result.warnings) : renderHumanInspection(result.value)
|
|
1874
|
+
);
|
|
1875
|
+
return EXIT_SUCCESS;
|
|
1876
|
+
} catch (cause) {
|
|
1877
|
+
const error = internalError(cause);
|
|
1878
|
+
presentFailure(error, [], options, writers);
|
|
1879
|
+
return EXIT_INTERNAL;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// apps/cli/src/commands/split-options.ts
|
|
1884
|
+
function invalidOptionsError(message) {
|
|
1885
|
+
return Object.freeze({ code: "invalid_split_options", message });
|
|
1886
|
+
}
|
|
1887
|
+
function parentHeadingMode(value) {
|
|
1888
|
+
switch (value) {
|
|
1889
|
+
case "filename":
|
|
1890
|
+
return "inFilename";
|
|
1891
|
+
case "folders":
|
|
1892
|
+
return "asFolder";
|
|
1893
|
+
case "none":
|
|
1894
|
+
case void 0:
|
|
1895
|
+
return "notUsed";
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
function parseSplitOptions(input, rawArguments = []) {
|
|
1899
|
+
if (rawArguments.includes("--index") && rawArguments.includes("--no-index") || rawArguments.includes("--title-case") && rawArguments.includes("--no-title-case")) {
|
|
1900
|
+
return {
|
|
1901
|
+
ok: false,
|
|
1902
|
+
error: invalidOptionsError(
|
|
1903
|
+
"Contradictory split options cannot be used together."
|
|
1904
|
+
)
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
const levelText = input.level ?? String(DEFAULT_SPLIT_OPTIONS.splitAtLevel);
|
|
1908
|
+
if (!/^[1-9]\d*$/u.test(levelText)) {
|
|
1909
|
+
return {
|
|
1910
|
+
ok: false,
|
|
1911
|
+
error: invalidOptionsError("The split level must be a positive integer.")
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
const splitAtLevel = Number(levelText);
|
|
1915
|
+
if (!Number.isSafeInteger(splitAtLevel)) {
|
|
1916
|
+
return {
|
|
1917
|
+
ok: false,
|
|
1918
|
+
error: invalidOptionsError("The split level must be a positive integer.")
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
return {
|
|
1922
|
+
ok: true,
|
|
1923
|
+
options: Object.freeze({
|
|
1924
|
+
splitAtLevel,
|
|
1925
|
+
parentHeadingMode: parentHeadingMode(input.parentHeadings),
|
|
1926
|
+
addIndexToFilenames: input.index ?? DEFAULT_SPLIT_OPTIONS.addIndexToFilenames,
|
|
1927
|
+
useMultiLevelIndexInFilenames: input.multilevelIndex ?? DEFAULT_SPLIT_OPTIONS.useMultiLevelIndexInFilenames,
|
|
1928
|
+
includeOriginalFilename: input.originalName ?? DEFAULT_SPLIT_OPTIONS.includeOriginalFilename,
|
|
1929
|
+
transformToTitleCase: input.titleCase ?? DEFAULT_SPLIT_OPTIONS.transformToTitleCase
|
|
1930
|
+
})
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// apps/cli/src/commands/plan-command.ts
|
|
1935
|
+
var processWriters2 = {
|
|
1936
|
+
stdout: (text) => process.stdout.write(text),
|
|
1937
|
+
stderr: (text) => process.stderr.write(text)
|
|
1938
|
+
};
|
|
1939
|
+
function internalError2(cause) {
|
|
1940
|
+
return Object.freeze({
|
|
1941
|
+
code: "internal_error",
|
|
1942
|
+
message: "The plan command failed unexpectedly.",
|
|
1943
|
+
cause
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
function presentFailure2(error, warnings, options, writers) {
|
|
1947
|
+
if (options.json === true) {
|
|
1948
|
+
writers.stdout(renderJsonPlanFailure(error, warnings));
|
|
1949
|
+
} else {
|
|
1950
|
+
writers.stderr(renderHumanError(error));
|
|
1951
|
+
}
|
|
1952
|
+
return exitCodeForError(error.code);
|
|
1953
|
+
}
|
|
1954
|
+
function presentUsageFailure(error, options, writers) {
|
|
1955
|
+
if (options.json === true) {
|
|
1956
|
+
writers.stdout(renderJsonPlanFailure(error, []));
|
|
1957
|
+
} else {
|
|
1958
|
+
writers.stderr(renderHumanError(error));
|
|
1959
|
+
}
|
|
1960
|
+
return EXIT_USAGE;
|
|
1961
|
+
}
|
|
1962
|
+
async function runPlanCommand(inputFilePath, options = {}, writers = processWriters2, rawArguments = []) {
|
|
1963
|
+
try {
|
|
1964
|
+
const parsedOptions = parseSplitOptions(options, rawArguments);
|
|
1965
|
+
if (!parsedOptions.ok) {
|
|
1966
|
+
return presentUsageFailure(parsedOptions.error, options, writers);
|
|
1967
|
+
}
|
|
1968
|
+
const inspection = await inspectDocument(inputFilePath);
|
|
1969
|
+
if (!inspection.ok) {
|
|
1970
|
+
return presentFailure2(
|
|
1971
|
+
inspection.error,
|
|
1972
|
+
inspection.warnings,
|
|
1973
|
+
options,
|
|
1974
|
+
writers
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
const plan = planSplit(inspection.value, parsedOptions.options);
|
|
1978
|
+
if (!plan.ok) {
|
|
1979
|
+
return presentFailure2(plan.error, plan.warnings, options, writers);
|
|
1980
|
+
}
|
|
1981
|
+
writers.stdout(
|
|
1982
|
+
options.json === true ? renderJsonPlanSuccess(inspection.value, plan.value, plan.warnings) : renderHumanPlan(plan.value)
|
|
1983
|
+
);
|
|
1984
|
+
return EXIT_SUCCESS;
|
|
1985
|
+
} catch (cause) {
|
|
1986
|
+
const error = internalError2(cause);
|
|
1987
|
+
presentFailure2(error, [], options, writers);
|
|
1988
|
+
return EXIT_INTERNAL;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// apps/cli/src/commands/split-command.ts
|
|
1993
|
+
import { createInterface } from "node:readline";
|
|
1994
|
+
import { resolve as resolve3 } from "node:path";
|
|
1995
|
+
var CONFIRMATION_PROMPT = "Create these files? [y/N]";
|
|
1996
|
+
async function promptForSplitConfirmation(readLine, write) {
|
|
1997
|
+
while (true) {
|
|
1998
|
+
write(`${CONFIRMATION_PROMPT} `);
|
|
1999
|
+
const answer = await readLine();
|
|
2000
|
+
if (answer === void 0) {
|
|
2001
|
+
write("\n");
|
|
2002
|
+
return "interrupted";
|
|
2003
|
+
}
|
|
2004
|
+
if (answer === null) {
|
|
2005
|
+
write("\n");
|
|
2006
|
+
return "no";
|
|
2007
|
+
}
|
|
2008
|
+
const normalized = answer.trim().toLowerCase();
|
|
2009
|
+
if (normalized === "y" || normalized === "yes") {
|
|
2010
|
+
return "yes";
|
|
2011
|
+
}
|
|
2012
|
+
if (normalized === "" || normalized === "n" || normalized === "no") {
|
|
2013
|
+
return "no";
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
async function confirmWithProcessStreams() {
|
|
2018
|
+
const lines = createInterface({
|
|
2019
|
+
input: process.stdin,
|
|
2020
|
+
output: process.stdout,
|
|
2021
|
+
terminal: process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
2022
|
+
crlfDelay: Infinity
|
|
2023
|
+
});
|
|
2024
|
+
const iterator = lines[Symbol.asyncIterator]();
|
|
2025
|
+
let interrupted;
|
|
2026
|
+
const interruption = new Promise((resolveInterruption) => {
|
|
2027
|
+
interrupted = () => resolveInterruption(void 0);
|
|
2028
|
+
lines.once("SIGINT", interrupted);
|
|
2029
|
+
});
|
|
2030
|
+
try {
|
|
2031
|
+
return await promptForSplitConfirmation(
|
|
2032
|
+
async () => {
|
|
2033
|
+
const next = await Promise.race([iterator.next(), interruption]);
|
|
2034
|
+
if (next === void 0) {
|
|
2035
|
+
return void 0;
|
|
2036
|
+
}
|
|
2037
|
+
return next.done === true ? null : next.value;
|
|
2038
|
+
},
|
|
2039
|
+
(text) => process.stdout.write(text)
|
|
2040
|
+
);
|
|
2041
|
+
} finally {
|
|
2042
|
+
if (interrupted !== void 0) {
|
|
2043
|
+
lines.off("SIGINT", interrupted);
|
|
2044
|
+
}
|
|
2045
|
+
lines.close();
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
var processIo = {
|
|
2049
|
+
stdout: (text) => process.stdout.write(text),
|
|
2050
|
+
stderr: (text) => process.stderr.write(text),
|
|
2051
|
+
confirm: confirmWithProcessStreams,
|
|
2052
|
+
currentWorkingDirectory: () => process.cwd()
|
|
2053
|
+
};
|
|
2054
|
+
function invalidOptionsError2(message) {
|
|
2055
|
+
return Object.freeze({ code: "invalid_split_options", message });
|
|
2056
|
+
}
|
|
2057
|
+
function internalError3(cause) {
|
|
2058
|
+
return Object.freeze({
|
|
2059
|
+
code: "internal_error",
|
|
2060
|
+
message: "The split command failed unexpectedly.",
|
|
2061
|
+
cause
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
function presentFailure3(error, warnings, options, io) {
|
|
2065
|
+
if (options.json === true) {
|
|
2066
|
+
io.stdout(renderJsonSplitFailure(error, warnings));
|
|
2067
|
+
} else {
|
|
2068
|
+
io.stderr(renderHumanError(error));
|
|
2069
|
+
}
|
|
2070
|
+
return exitCodeForError(error.code);
|
|
2071
|
+
}
|
|
2072
|
+
function presentUsageFailure2(error, options, io) {
|
|
2073
|
+
if (options.json === true) {
|
|
2074
|
+
io.stdout(renderJsonSplitFailure(error, []));
|
|
2075
|
+
} else {
|
|
2076
|
+
io.stderr(renderHumanError(error));
|
|
2077
|
+
}
|
|
2078
|
+
return EXIT_USAGE;
|
|
2079
|
+
}
|
|
2080
|
+
async function runSplitCommand(inputFilePath, options = {}, io = processIo, rawArguments = []) {
|
|
2081
|
+
try {
|
|
2082
|
+
const parsedOptions = parseSplitOptions(options, rawArguments);
|
|
2083
|
+
if (!parsedOptions.ok) {
|
|
2084
|
+
return presentUsageFailure2(parsedOptions.error, options, io);
|
|
2085
|
+
}
|
|
2086
|
+
if (options.json === true && options.yes !== true) {
|
|
2087
|
+
return presentUsageFailure2(
|
|
2088
|
+
invalidOptionsError2("Split execution with --json also requires --yes."),
|
|
2089
|
+
options,
|
|
2090
|
+
io
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
const selectedOutputDirectoryPath = resolve3(
|
|
2094
|
+
io.currentWorkingDirectory(),
|
|
2095
|
+
options.output ?? "."
|
|
2096
|
+
);
|
|
2097
|
+
const outputDirectoryMode = options.subfolder === true ? "sourceNamedSubfolder" : "direct";
|
|
2098
|
+
const inspection = await inspectDocument(inputFilePath);
|
|
2099
|
+
if (!inspection.ok) {
|
|
2100
|
+
return presentFailure3(inspection.error, inspection.warnings, options, io);
|
|
2101
|
+
}
|
|
2102
|
+
const plan = planSplit(inspection.value, parsedOptions.options);
|
|
2103
|
+
if (!plan.ok) {
|
|
2104
|
+
return presentFailure3(plan.error, plan.warnings, options, io);
|
|
2105
|
+
}
|
|
2106
|
+
const previewShown = options.json !== true && !(options.quiet === true && options.yes === true);
|
|
2107
|
+
if (previewShown) {
|
|
2108
|
+
io.stdout(renderHumanSplitPreview(plan.value));
|
|
2109
|
+
}
|
|
2110
|
+
if (options.yes !== true) {
|
|
2111
|
+
const confirmation = await io.confirm();
|
|
2112
|
+
if (confirmation === "interrupted") {
|
|
2113
|
+
return EXIT_INTERRUPTED;
|
|
2114
|
+
}
|
|
2115
|
+
if (confirmation === "no") {
|
|
2116
|
+
io.stdout(renderHumanCancellation());
|
|
2117
|
+
return EXIT_SUCCESS;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
if (options.quiet !== true) {
|
|
2121
|
+
io.stderr("Creating output files...\n");
|
|
2122
|
+
}
|
|
2123
|
+
const execution = await executeSplit(
|
|
2124
|
+
inspection.value,
|
|
2125
|
+
plan.value,
|
|
2126
|
+
{
|
|
2127
|
+
selectedOutputDirectoryPath,
|
|
2128
|
+
outputDirectoryMode
|
|
2129
|
+
},
|
|
2130
|
+
options.quiet === true ? void 0 : {
|
|
2131
|
+
onProgress: (progress) => {
|
|
2132
|
+
io.stderr(
|
|
2133
|
+
`Created ${progress.completedFiles} of ${progress.totalFiles} files.
|
|
2134
|
+
`
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
);
|
|
2139
|
+
if (!execution.ok) {
|
|
2140
|
+
return presentFailure3(execution.error, execution.warnings, options, io);
|
|
2141
|
+
}
|
|
2142
|
+
if (options.json === true) {
|
|
2143
|
+
io.stdout(
|
|
2144
|
+
renderJsonSplitSuccess(
|
|
2145
|
+
inspection.value,
|
|
2146
|
+
selectedOutputDirectoryPath,
|
|
2147
|
+
outputDirectoryMode,
|
|
2148
|
+
plan.value,
|
|
2149
|
+
execution.value,
|
|
2150
|
+
execution.warnings
|
|
2151
|
+
)
|
|
2152
|
+
);
|
|
2153
|
+
} else {
|
|
2154
|
+
io.stdout(
|
|
2155
|
+
renderHumanSplitSuccess(
|
|
2156
|
+
execution.value,
|
|
2157
|
+
previewShown ? [] : execution.warnings
|
|
2158
|
+
)
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
return EXIT_SUCCESS;
|
|
2162
|
+
} catch (cause) {
|
|
2163
|
+
const error = internalError3(cause);
|
|
2164
|
+
presentFailure3(error, [], options, io);
|
|
2165
|
+
return EXIT_INTERNAL;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// apps/cli/src/main.ts
|
|
2170
|
+
function readCliVersion() {
|
|
2171
|
+
if (true) {
|
|
2172
|
+
return "0.1.0";
|
|
2173
|
+
}
|
|
2174
|
+
return createRequire(import.meta.url)("../package.json").version;
|
|
2175
|
+
}
|
|
2176
|
+
var CLI_VERSION = readCliVersion();
|
|
2177
|
+
var ROOT_HELP = `
|
|
2178
|
+
Default split workflow:
|
|
2179
|
+
doc-splitter <input> [split options] [--output <directory>] [--subfolder]
|
|
2180
|
+
[--yes] [--json] [--quiet]
|
|
2181
|
+
|
|
2182
|
+
Split options:
|
|
2183
|
+
--level <number> split at this heading level
|
|
2184
|
+
--parent-headings <mode> none, filename, or folders
|
|
2185
|
+
--index / --no-index include or omit filename indexes
|
|
2186
|
+
--multilevel-index use hierarchical filename indexes
|
|
2187
|
+
--original-name prefix filenames with the input stem
|
|
2188
|
+
--title-case / --no-title-case transform or preserve heading-name casing
|
|
2189
|
+
|
|
2190
|
+
Execution options:
|
|
2191
|
+
--output <directory> select the output directory
|
|
2192
|
+
--subfolder create a unique source-named subfolder
|
|
2193
|
+
--yes confirm execution without prompting
|
|
2194
|
+
--json write one versioned JSON result
|
|
2195
|
+
--quiet suppress progress and confirmed preview
|
|
2196
|
+
`;
|
|
2197
|
+
function addSplitOptions(command) {
|
|
2198
|
+
return command.option("--level <number>", "split at this heading level").addOption(
|
|
2199
|
+
new Option(
|
|
2200
|
+
"--parent-headings <mode>",
|
|
2201
|
+
"place parent headings in filenames or folders"
|
|
2202
|
+
).choices(["none", "filename", "folders"])
|
|
2203
|
+
).option("--index", "include indexes in filenames").option("--no-index", "omit indexes from filenames").option("--multilevel-index", "use hierarchical indexes in filenames").option("--original-name", "prefix filenames with the input stem").option("--title-case", "transform heading names to title case").option("--no-title-case", "preserve heading-name casing");
|
|
2204
|
+
}
|
|
2205
|
+
function setProcessExitCode(exitCode) {
|
|
2206
|
+
if (exitCode !== 0) {
|
|
2207
|
+
process.exitCode = exitCode;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
function createCli() {
|
|
2211
|
+
const command = new Command().name("doc-splitter").description(`${PROJECT_NAME} command-line interface`).usage("[options] <input>\n doc-splitter <command> [options]").version(CLI_VERSION).showHelpAfterError().addHelpText("after", ROOT_HELP);
|
|
2212
|
+
command.command("inspect").description("Inspect the heading hierarchy of a DOCX or PDF document").argument("<input>", "input DOCX or PDF file").option("--json", "write one versioned JSON result to stdout").option("--quiet", "suppress progress and nonessential human output").action(async (input, options) => {
|
|
2213
|
+
const exitCode = await runInspectCommand(input, options);
|
|
2214
|
+
setProcessExitCode(exitCode);
|
|
2215
|
+
});
|
|
2216
|
+
addSplitOptions(
|
|
2217
|
+
command.command("plan").description("Calculate output paths and ranges without creating files").argument("<input>", "input DOCX or PDF file")
|
|
2218
|
+
).option("--json", "write one versioned JSON result to stdout").option("--quiet", "suppress progress and nonessential human output").action(
|
|
2219
|
+
async (input, options, actionCommand) => {
|
|
2220
|
+
const exitCode = await runPlanCommand(
|
|
2221
|
+
input,
|
|
2222
|
+
options,
|
|
2223
|
+
void 0,
|
|
2224
|
+
actionCommand.parent?.args ?? []
|
|
2225
|
+
);
|
|
2226
|
+
setProcessExitCode(exitCode);
|
|
2227
|
+
}
|
|
2228
|
+
);
|
|
2229
|
+
addSplitOptions(
|
|
2230
|
+
command.command("__split", { hidden: true, isDefault: true }).description("Split a DOCX or PDF document").argument("<input>", "input DOCX or PDF file")
|
|
2231
|
+
).option("--output <directory>", "select the output directory").option("--subfolder", "create a unique source-named output subfolder").option("--yes", "confirm execution without prompting").option("--json", "write one versioned JSON result to stdout").option("--quiet", "suppress progress and nonessential human output").action(
|
|
2232
|
+
async (input, options, actionCommand) => {
|
|
2233
|
+
const exitCode = await runSplitCommand(
|
|
2234
|
+
input,
|
|
2235
|
+
options,
|
|
2236
|
+
void 0,
|
|
2237
|
+
actionCommand.parent?.args ?? []
|
|
2238
|
+
);
|
|
2239
|
+
setProcessExitCode(exitCode);
|
|
2240
|
+
}
|
|
2241
|
+
);
|
|
2242
|
+
return command;
|
|
2243
|
+
}
|
|
2244
|
+
function jsonCommandForArguments(argv) {
|
|
2245
|
+
switch (argv[2]) {
|
|
2246
|
+
case "inspect":
|
|
2247
|
+
return "inspect";
|
|
2248
|
+
case "plan":
|
|
2249
|
+
return "plan";
|
|
2250
|
+
default:
|
|
2251
|
+
return "split";
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
function presentJsonUsageFailure(argv, error) {
|
|
2255
|
+
const usageError = Object.freeze({
|
|
2256
|
+
code: "invalid_split_options",
|
|
2257
|
+
message: error.message
|
|
2258
|
+
});
|
|
2259
|
+
process.stdout.write(
|
|
2260
|
+
renderJsonFailure(jsonCommandForArguments(argv), usageError, [])
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
async function runCli(argv = process.argv) {
|
|
2264
|
+
const command = createCli().exitOverride();
|
|
2265
|
+
for (const subcommand of command.commands) {
|
|
2266
|
+
subcommand.exitOverride();
|
|
2267
|
+
}
|
|
2268
|
+
try {
|
|
2269
|
+
await command.parseAsync([...argv]);
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
if (error instanceof CommanderError) {
|
|
2272
|
+
if (error.exitCode !== 0) {
|
|
2273
|
+
process.exitCode = EXIT_USAGE;
|
|
2274
|
+
if (argv.includes("--json")) {
|
|
2275
|
+
presentJsonUsageFailure(argv, error);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
throw error;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
var entryPath = process.argv[1];
|
|
2284
|
+
if (entryPath !== void 0 && import.meta.url === pathToFileURL(resolve4(entryPath)).href) {
|
|
2285
|
+
runCli().catch((error) => {
|
|
2286
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2287
|
+
console.error(message);
|
|
2288
|
+
process.exitCode = EXIT_INTERNAL;
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
export {
|
|
2292
|
+
createCli,
|
|
2293
|
+
runCli
|
|
2294
|
+
};
|