epubcheck-standalone-cli 0.0.0-oidc-bootstrap → 5.3.0-build2
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 +37 -0
- package/README.md +112 -0
- package/dist/archive.d.ts +25 -0
- package/dist/archive.js +340 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +855 -0
- package/dist/help-text.d.ts +1 -0
- package/dist/help-text.js +12 -0
- package/dist/list-checks-data.d.ts +1 -0
- package/dist/list-checks-data.js +11 -0
- package/dist/list-checks-locale-data.d.ts +1 -0
- package/dist/list-checks-locale-data.js +22 -0
- package/dist/locale-messages.d.ts +3 -0
- package/dist/locale-messages.js +346 -0
- package/dist/messages.d.ts +93 -0
- package/dist/messages.js +136 -0
- package/package.json +64 -3
package/dist/cli.js
ADDED
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// epubcheck-standalone-cli -- a zero-Java command-line EPUB validator that mirrors the
|
|
3
|
+
// official epubcheck 5.3.0 CLI (com.adobe.epubcheck.tool.EpubChecker /
|
|
4
|
+
// Checker) byte-for-byte, on top of the epubcheck-standalone engine.
|
|
5
|
+
//
|
|
6
|
+
// The behavior here is a faithful re-implementation of epubcheck 5.3.0's
|
|
7
|
+
// EpubChecker.processArguments / run / validateFile / processFile and
|
|
8
|
+
// DefaultReportImpl (see the source files named in the package README). Flags
|
|
9
|
+
// that change what the engine validates or which locale it speaks (--mode,
|
|
10
|
+
// --profile, --locale, -u/--usage) are passed straight through to the engine via
|
|
11
|
+
// the `args` option on the library's single `validate(source, options)` call; the
|
|
12
|
+
// console text is still reconstructed here from the engine's report so the
|
|
13
|
+
// reporting-level filtering, report files, and exit codes stay byte-identical to
|
|
14
|
+
// the jar. Every input runs through that same `validate()` call, differing only in
|
|
15
|
+
// the source handed to it: a packaged `.epub` (`fs`), an expanded directory
|
|
16
|
+
// (`fsDir`, for `--mode exp` and directory inputs), or an http(s) URL (`url`).
|
|
17
|
+
// Custom message overrides (`-c/--customMessages`) ride through the
|
|
18
|
+
// `customMessages` option (or, for a missing overrides file, straight through
|
|
19
|
+
// `args` so the engine emits CHK-001 like the jar). The one capability the engine
|
|
20
|
+
// still cannot provide is an unshipped locale, which the CLI refuses loudly rather
|
|
21
|
+
// than silently diverging -- see `unsupportedReason` and the README "Unsupported
|
|
22
|
+
// flags" table.
|
|
23
|
+
import { writeFile, readFile, realpath, stat, unlink } from "node:fs/promises";
|
|
24
|
+
import { resolve as pathResolve, dirname, basename, join as pathJoin } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { validate } from "epubcheck-standalone";
|
|
27
|
+
import { fs as fsSource, fsDir, url as urlSource } from "epubcheck-standalone/plugins";
|
|
28
|
+
// The console line/summary formatting lives in the LIBRARY (one implementation,
|
|
29
|
+
// shared with the library's batch `formatConsoleReport`); the CLI drives these
|
|
30
|
+
// same primitives from the engine's live per-message/-feature stream so its
|
|
31
|
+
// stdout/stderr stay byte-identical to the jar while still printing during the
|
|
32
|
+
// run. CLI-specific concerns (flag/--quiet handling, reporting-level selection,
|
|
33
|
+
// locale wiring, stream routing) stay here.
|
|
34
|
+
import { renderConsoleMessageLine, countConsoleSeverities, displayedConsoleCounts, renderConsoleSummaryLine, renderValidatingLine, severityReportingLevel, formatTemplate, } from "epubcheck-standalone/formatters";
|
|
35
|
+
import { M, messagesFor, resolveLocale, EPUBCHECK_VERSION, ReportingLevel, } from "./messages.js";
|
|
36
|
+
import { HELP_OUTPUT } from "./help-text.js";
|
|
37
|
+
import { createArchive } from "./archive.js";
|
|
38
|
+
import { LIST_CHECKS_TSV } from "./list-checks-data.js";
|
|
39
|
+
import { LIST_CHECKS_TSV_BY_LOCALE } from "./list-checks-locale-data.js";
|
|
40
|
+
/** Exit code used when a flag is valid epubcheck syntax but unsupportable here. */
|
|
41
|
+
const EXIT_UNSUPPORTED = 2;
|
|
42
|
+
const KNOWN_PROFILES = new Set(["DEFAULT", "IDX", "DICT", "EDUPUB", "PREVIEW"]);
|
|
43
|
+
// Polyfill-safe well-known dispose key (the same key the library's plugins use).
|
|
44
|
+
const DISPOSE = Symbol.dispose ?? Symbol.for("Symbol.dispose");
|
|
45
|
+
/** Async existsSync: stat the path and report success, false on any error --
|
|
46
|
+
* behavior-identical to fs.existsSync (which is itself stat-based) but without
|
|
47
|
+
* blocking the event loop (async law). */
|
|
48
|
+
async function pathExists(p) {
|
|
49
|
+
try {
|
|
50
|
+
await stat(p);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Reproduce Java's FileNotFoundException.getMessage() ("<path> (<strerror>)")
|
|
58
|
+
* for a Node write error, so the --listChecks write-failure stderr matches the
|
|
59
|
+
* jar byte-for-byte. The path is the ORIGINAL argument (as the jar's `new
|
|
60
|
+
* File(arg)` carries it), and the OS strerror is keyed off the errno code. */
|
|
61
|
+
function javaIoErrorMessage(err, originalPath) {
|
|
62
|
+
const code = err?.code;
|
|
63
|
+
const strerror = {
|
|
64
|
+
ENOENT: "No such file or directory",
|
|
65
|
+
EACCES: "Permission denied",
|
|
66
|
+
EISDIR: "Is a directory",
|
|
67
|
+
ENOTDIR: "Not a directory",
|
|
68
|
+
EEXIST: "File exists",
|
|
69
|
+
EROFS: "Read-only file system",
|
|
70
|
+
};
|
|
71
|
+
const text = code && strerror[code] ? strerror[code] : String(err?.message ?? err);
|
|
72
|
+
return `${originalPath} (${text})`;
|
|
73
|
+
}
|
|
74
|
+
class ParseError extends Error {
|
|
75
|
+
}
|
|
76
|
+
export async function run(args) {
|
|
77
|
+
const st = {
|
|
78
|
+
path: null,
|
|
79
|
+
mode: null,
|
|
80
|
+
version: "3.0",
|
|
81
|
+
profile: null,
|
|
82
|
+
expanded: false,
|
|
83
|
+
jsonOutput: false,
|
|
84
|
+
xmlOutput: false,
|
|
85
|
+
xmpOutput: false,
|
|
86
|
+
fileOut: undefined,
|
|
87
|
+
listChecks: false,
|
|
88
|
+
listChecksOut: null,
|
|
89
|
+
customMessagesPath: null,
|
|
90
|
+
displayHelp: false,
|
|
91
|
+
displayVersion: false,
|
|
92
|
+
failOnWarnings: false,
|
|
93
|
+
save: false,
|
|
94
|
+
reportingLevel: ReportingLevel.Info,
|
|
95
|
+
quiet: false,
|
|
96
|
+
localeTag: null,
|
|
97
|
+
};
|
|
98
|
+
// Emit helpers. Everything that epubcheck routes through `outWriter` (stdout)
|
|
99
|
+
// is suppressed under --quiet; System.err and report bodies are not.
|
|
100
|
+
const outln = (s) => {
|
|
101
|
+
if (!st.quiet)
|
|
102
|
+
process.stdout.write(s + "\n");
|
|
103
|
+
};
|
|
104
|
+
const errln = (s) => {
|
|
105
|
+
process.stderr.write(s + "\n");
|
|
106
|
+
};
|
|
107
|
+
// USAGE messages route to stdout and, unlike everything else on stdout, ignore
|
|
108
|
+
// --quiet: DefaultReportImpl.message() pushes the out writer to the report's own
|
|
109
|
+
// (never-quiet) setting just for USAGE before printing.
|
|
110
|
+
const usageln = (s) => {
|
|
111
|
+
process.stdout.write(s + "\n");
|
|
112
|
+
};
|
|
113
|
+
const displayHelp = () => outln(HELP_OUTPUT.slice(0, -1)); // HELP_OUTPUT ends with the println newline
|
|
114
|
+
const displayVersion = () => outln(formatTemplate(M.epubcheck_version_text, EPUBCHECK_VERSION));
|
|
115
|
+
// --- processArguments -------------------------------------------------------
|
|
116
|
+
let parsedOk = true;
|
|
117
|
+
try {
|
|
118
|
+
parsedOk = await processArguments(args, st, { outln, errln, displayHelp, displayVersion });
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
// Mirrors run()'s `catch (Exception ignored) { returnValue = 1 }` for the
|
|
122
|
+
// -v / -mode / -profile "argument expected/invalid" throw paths (which have
|
|
123
|
+
// already printed `-help displays help` to stdout).
|
|
124
|
+
if (e instanceof ParseError)
|
|
125
|
+
return 1;
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
if (!parsedOk)
|
|
129
|
+
return 1;
|
|
130
|
+
// Early returns (before any report is created -> no summary block printed).
|
|
131
|
+
if (st.displayHelp || (st.displayVersion && st.path === null))
|
|
132
|
+
return 0;
|
|
133
|
+
// The (possibly localized) message table for every string the CLI formats
|
|
134
|
+
// itself from here on. English unless a shipped non-English locale was named.
|
|
135
|
+
const msgs = messagesFor(st.localeTag);
|
|
136
|
+
// --listChecks: dump the (embedded) message dictionary, no validation.
|
|
137
|
+
//
|
|
138
|
+
// The jar localizes the Message/Suggestion columns from the report's localized
|
|
139
|
+
// MessageBundle (MessageDictionaryDumper), so `--listChecks --locale <tag>`
|
|
140
|
+
// emits the dictionary in that locale (ID/Severity columns stay locale-
|
|
141
|
+
// invariant). It does NOT validate/refuse the locale on this path (an unshipped
|
|
142
|
+
// tag simply falls back to English, exit 0), so the listChecks branch runs
|
|
143
|
+
// BEFORE the unsupportedReason gate -- matching the jar exactly. Select the
|
|
144
|
+
// localized dump the SAME way a run resolves the locale (resolveLocale): a shipped
|
|
145
|
+
// non-English key -> its dump; English / an English-fallback tag -> the default.
|
|
146
|
+
if (st.listChecks) {
|
|
147
|
+
const listZero = { fatal: 0, error: 0, warning: 0, info: 0, usage: 0 };
|
|
148
|
+
const listKey = st.localeTag !== null ? resolveLocale(st.localeTag) : null;
|
|
149
|
+
const listTsv = (listKey !== null && LIST_CHECKS_TSV_BY_LOCALE[listKey]) || LIST_CHECKS_TSV;
|
|
150
|
+
if (st.listChecksOut === null) {
|
|
151
|
+
// Written straight to System.out; the trailing summary is swallowed
|
|
152
|
+
// because epubcheck's dumper closes the stdout stream.
|
|
153
|
+
process.stdout.write(listTsv);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// EpubChecker.dumpMessageDictionary SWALLOWS a write failure: it prints the
|
|
157
|
+
// absolute path (listChecksOut.getAbsoluteFile()) + the IOException message
|
|
158
|
+
// to stderr, but run() still returns 0 on the listChecks branch and its
|
|
159
|
+
// finally still prints the completion summary. So a write failure exits 0
|
|
160
|
+
// with the summary, NOT 1 without it.
|
|
161
|
+
try {
|
|
162
|
+
await writeFile(st.listChecksOut, listTsv);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
errln(formatTemplate(msgs.error_creating_config_file, pathResolve(process.cwd(), st.listChecksOut)));
|
|
166
|
+
errln(javaIoErrorMessage(e, st.listChecksOut));
|
|
167
|
+
}
|
|
168
|
+
printCompleted(st, listZero, outln, msgs);
|
|
169
|
+
}
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
// Anything the engine cannot faithfully reproduce -> fail loudly.
|
|
173
|
+
const reason = unsupportedReason(st);
|
|
174
|
+
if (reason !== null) {
|
|
175
|
+
errln(`epubcheck-standalone-cli: unsupported option: ${reason}`);
|
|
176
|
+
errln(" This capability is not available in the epubcheck-standalone engine.");
|
|
177
|
+
errln(" See the \"Unsupported flags\" section of the epubcheck-standalone-cli README.");
|
|
178
|
+
return EXIT_UNSUPPORTED;
|
|
179
|
+
}
|
|
180
|
+
const path = st.path;
|
|
181
|
+
const cwd = process.cwd();
|
|
182
|
+
// An http(s) URL input runs the engine's jar-parity URL mode (the URL itself
|
|
183
|
+
// is epubcheck's input path), not read from disk, so the on-disk
|
|
184
|
+
// existence/expanded routing is skipped.
|
|
185
|
+
const isUrl = path.startsWith("http://") || path.startsWith("https://");
|
|
186
|
+
const absPath = isUrl ? path : pathResolve(cwd, path);
|
|
187
|
+
const exists = !isUrl && (await pathExists(absPath));
|
|
188
|
+
const isDir = exists && (await stat(absPath)).isDirectory();
|
|
189
|
+
const zeroCounts = { fatal: 0, error: 0, warning: 0, info: 0, usage: 0 };
|
|
190
|
+
// Expanded (directory / --mode exp) vs packaged/single-file routing, mirroring
|
|
191
|
+
// epubcheck 5.3.0's EpubChecker: `-mode exp` (st.expanded) always means expanded
|
|
192
|
+
// validation, and a directory input with no single-file `-mode` is auto-detected
|
|
193
|
+
// as expanded too (a `.epub`-named directory, or a directory validated under a
|
|
194
|
+
// `--profile`). A single-file `-mode` (xhtml/opf/svg/mo/nav) is never expanded.
|
|
195
|
+
const useExpanded = !isUrl && (st.expanded || (st.mode === null && isDir));
|
|
196
|
+
// The EPUB name epubcheck's DefaultReportImpl.formatMessage prefixes onto every
|
|
197
|
+
// message location -- the same name the engine validated under (computed HERE,
|
|
198
|
+
// before the run, so the live console stream can prefix each message as it
|
|
199
|
+
// arrives): the URL itself for a URL input; the freshly packaged
|
|
200
|
+
// `./<name>.epub` for an explicit `--mode exp` directory (the jar reports the
|
|
201
|
+
// temp archive it built, relative to the working directory, so the prefix keeps
|
|
202
|
+
// the leading `./`); else the bare input basename (a packaged file, a
|
|
203
|
+
// single-file mode, or an auto-detected expanded directory validated in place).
|
|
204
|
+
const reportedName = isUrl
|
|
205
|
+
? path
|
|
206
|
+
: useExpanded && st.expanded
|
|
207
|
+
? `./${basename(absPath)}.epub`
|
|
208
|
+
: basename(absPath);
|
|
209
|
+
const formatActive = st.jsonOutput || st.xmlOutput || st.xmpOutput;
|
|
210
|
+
// Live console streaming (DefaultReportImpl). With NO report format, the CLI
|
|
211
|
+
// writes each console line AS THE ENGINE EMITS IT, exactly like the jar prints
|
|
212
|
+
// during a run: per-message lines via the engine's live `onMessage`, and the
|
|
213
|
+
// "Validating using EPUB version X rules." line via `onFeature` the moment the
|
|
214
|
+
// FORMAT_VERSION feature arrives. The line/summary FORMATTING is the library's
|
|
215
|
+
// shared console renderer (renderConsoleMessageLine / renderValidatingLine),
|
|
216
|
+
// the same primitives the library's batch `formatConsoleReport` uses -- one
|
|
217
|
+
// implementation, streamed here and batched there. Stream ROUTING stays a CLI
|
|
218
|
+
// concern: USAGE goes to stdout (ignoring --quiet, like DefaultReportImpl's
|
|
219
|
+
// pushQuiet), every other severity to stderr, both filtered by the reporting
|
|
220
|
+
// level; the validating line is out-writer output (gated by level <= Info and
|
|
221
|
+
// suppressed by --quiet via outln). A report format suppresses all per-message
|
|
222
|
+
// console output, so no stream is installed then.
|
|
223
|
+
const attachConsoleStream = (opts) => {
|
|
224
|
+
if (formatActive)
|
|
225
|
+
return opts;
|
|
226
|
+
opts.onFeature = (feature) => {
|
|
227
|
+
if (feature.feature === "FORMAT_VERSION" && st.reportingLevel <= ReportingLevel.Info) {
|
|
228
|
+
const vline = renderValidatingLine(feature.value, msgs);
|
|
229
|
+
if (vline !== null)
|
|
230
|
+
outln(vline);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
opts.onMessage = (m) => {
|
|
234
|
+
if (severityReportingLevel(m.severity) < st.reportingLevel)
|
|
235
|
+
return;
|
|
236
|
+
const line = renderConsoleMessageLine(m, reportedName);
|
|
237
|
+
if (m.severity === "USAGE")
|
|
238
|
+
usageln(line);
|
|
239
|
+
else
|
|
240
|
+
errln(line);
|
|
241
|
+
};
|
|
242
|
+
return opts;
|
|
243
|
+
};
|
|
244
|
+
// --- validate() (the real work, via the engine) -----------------------------
|
|
245
|
+
// The newly unlocked flags (--mode, --profile, --locale, -u) ride through to the
|
|
246
|
+
// engine here; everything else stays a JS-layer concern (see buildEngineArgs).
|
|
247
|
+
// Every branch just produces `result`; message rendering reproduces the jar's
|
|
248
|
+
// console formatting from result.messages' raw location fields, prefixing the
|
|
249
|
+
// reported EPUB name derived per branch below. Report documents come from the library rendering
|
|
250
|
+
// the run's live report-event stream (result.reports), byte-identical to
|
|
251
|
+
// epubcheck's own writers and already embedding the right filename.
|
|
252
|
+
let result;
|
|
253
|
+
// -s/--save: the path of the packaged .epub this run wrote (expanded mode
|
|
254
|
+
// only), so the post-run tail can delete it when the check found errors.
|
|
255
|
+
let savedEpubPath = null;
|
|
256
|
+
if (isUrl) {
|
|
257
|
+
// Jar-parity URL mode: the URL is handed to
|
|
258
|
+
// epubcheck as the input path, the engine suspends mid-run while the
|
|
259
|
+
// library's async http bridge downloads it, and message locations carry
|
|
260
|
+
// the URL exactly like the jar's. Download/connection failures no longer throw here: the
|
|
261
|
+
// engine prints the jar's exception headline to stderr and exits 1, so
|
|
262
|
+
// `result` comes back normally; the catch stays as a net for bridge-level
|
|
263
|
+
// failures (e.g. url() rejecting a malformed URL).
|
|
264
|
+
try {
|
|
265
|
+
result = await validate(await urlSource(path), attachConsoleStream(await buildEngineOpts(st, cwd, false)));
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
process.stderr.write(String(e?.stack ?? e) + "\n");
|
|
269
|
+
printCompleted(st, zeroCounts, outln, msgs);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else if (useExpanded) {
|
|
274
|
+
// `--mode exp` requested but the input is not a directory. Mirrors epubcheck's
|
|
275
|
+
// expanded path: "Directory not found" when the path is missing; otherwise no
|
|
276
|
+
// messages are produced and the run finishes with errors (exit 1). Any
|
|
277
|
+
// `-mode`/`-v`-ignored notice for a `.epub` path was already printed above.
|
|
278
|
+
if (!isDir) {
|
|
279
|
+
if (!exists) {
|
|
280
|
+
errln(formatTemplate(msgs.directory_not_found, path));
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
errln(msgs.there_were_errors);
|
|
284
|
+
}
|
|
285
|
+
printCompleted(st, zeroCounts, outln, msgs);
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
// -s/--save with an EXPLICIT `--mode exp`: epubcheck packages the directory
|
|
289
|
+
// into `<canonical-parent>/<dir-name>.epub` BEFORE validating (overwriting
|
|
290
|
+
// any file already there) and deletes it again after a failing check. The
|
|
291
|
+
// jar builds its Archive only on the explicit exp path, so an auto-detected
|
|
292
|
+
// `.epub`-named directory never saves; and a packaging failure aborts with
|
|
293
|
+
// "Check finished with errors" + the completion summary, exit 1, exactly
|
|
294
|
+
// like EpubChecker.processFile's RuntimeException catch (which may leave a
|
|
295
|
+
// partial file behind -- so does this).
|
|
296
|
+
if (st.expanded && st.save) {
|
|
297
|
+
try {
|
|
298
|
+
savedEpubPath = await createArchive(absPath);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
errln(msgs.there_were_errors);
|
|
302
|
+
printCompleted(st, zeroCounts, outln, msgs);
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const dirOpts = await buildEngineOpts(st, cwd, true);
|
|
308
|
+
if (st.expanded) {
|
|
309
|
+
// EXPLICIT `--mode exp`: the jar packages the directory tree into a
|
|
310
|
+
// temp `<name>.epub` and validates that, so message locations carry
|
|
311
|
+
// the packaged name. The library default is 'direct', so set 'exp'
|
|
312
|
+
// explicitly to keep packaging (byte-identical to the jar's `--mode
|
|
313
|
+
// exp`).
|
|
314
|
+
dirOpts.dirMode = "exp";
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// AUTO-DETECTED expanded book (a `.epub`-named directory, or a
|
|
318
|
+
// directory under --profile, with no explicit `--mode exp`): the jar
|
|
319
|
+
// never packages here -- EpubChecker leaves `expanded` false and
|
|
320
|
+
// EpubCheck validates the directory IN PLACE, so message locations
|
|
321
|
+
// carry the directory name itself (`junk.epub/mimetype`), not a
|
|
322
|
+
// doubled `<name>.epub.epub` package name. The library's 'direct'
|
|
323
|
+
// dirMode is exactly that path (and is now the library default).
|
|
324
|
+
dirOpts.dirMode = "direct";
|
|
325
|
+
}
|
|
326
|
+
result = await validate(await fsDir(absPath), attachConsoleStream(dirOpts));
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
process.stderr.write(String(e?.stack ?? e) + "\n");
|
|
330
|
+
printCompleted(st, zeroCounts, outln, msgs);
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
// Missing file -> file_not_found (stderr) + summary block (stdout), exit 1.
|
|
336
|
+
if (!exists) {
|
|
337
|
+
errln(formatTemplate(msgs.file_not_found, path));
|
|
338
|
+
printCompleted(st, zeroCounts, outln, msgs);
|
|
339
|
+
return 1;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
// A single-file `--mode` pointed at a DIRECTORY: epubcheck reads the
|
|
343
|
+
// directory as a file and fails with FATAL(PKG-008) "Unable to read
|
|
344
|
+
// file <abs>", the location carrying the trailing-slash directory form.
|
|
345
|
+
// Hand the engine a directory placeholder (mounted as an empty VFS dir
|
|
346
|
+
// at the host path) so it reproduces that byte-for-byte, rather than a
|
|
347
|
+
// byte source whose read merely throws. A regular (possibly unreadable)
|
|
348
|
+
// file goes straight through fs(), which now DEFERS an unreadable-file
|
|
349
|
+
// error into the run itself -- FATAL(PKG-008) byte-for-byte like the jar.
|
|
350
|
+
const source = isDir
|
|
351
|
+
? {
|
|
352
|
+
name: basename(absPath),
|
|
353
|
+
hostDir: dirname(absPath),
|
|
354
|
+
isDirectory: true,
|
|
355
|
+
size: 0,
|
|
356
|
+
read: () => {
|
|
357
|
+
throw new Error("epubcheck-standalone-cli: a directory has no file bytes");
|
|
358
|
+
},
|
|
359
|
+
[DISPOSE]() { },
|
|
360
|
+
}
|
|
361
|
+
: await fsSource(absPath);
|
|
362
|
+
result = await validate(source, attachConsoleStream(await buildEngineOpts(st, cwd, false)));
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
365
|
+
// Mirrors the top-level `catch` -> exit 1, then the completion summary.
|
|
366
|
+
process.stderr.write(String(e?.stack ?? e) + "\n");
|
|
367
|
+
printCompleted(st, zeroCounts, outln, msgs);
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// A run that returned normally but FAILED before producing any structured
|
|
372
|
+
// message -- e.g. a URL download error (404 / connection refused) -- mirrors
|
|
373
|
+
// EpubChecker.run's catch/finally: the engine already carries the jar-identical
|
|
374
|
+
// exception headline on result.stderr and the jar-parity exit code on
|
|
375
|
+
// result.exitCode. The count-derived tail below never fires (no messages), so
|
|
376
|
+
// it would otherwise print a bogus "No errors or warnings detected." and exit
|
|
377
|
+
// 0; instead surface the engine's failure output verbatim (headline to stderr,
|
|
378
|
+
// completion summary to stdout) and exit with its jar-parity code. System.err
|
|
379
|
+
// ignores --quiet, exactly like the jar.
|
|
380
|
+
if ((result.exitCode ?? 0) !== 0 && result.messages.length === 0) {
|
|
381
|
+
if (result.stderr.length > 0)
|
|
382
|
+
process.stderr.write(result.stderr);
|
|
383
|
+
printCompleted(st, zeroCounts, outln, msgs);
|
|
384
|
+
return result.exitCode;
|
|
385
|
+
}
|
|
386
|
+
const actual = countConsoleSeverities(result.messages);
|
|
387
|
+
const shown = displayedConsoleCounts(actual, st.reportingLevel);
|
|
388
|
+
const reportToConsole = formatActive && st.fileOut === null;
|
|
389
|
+
// 1) The report document (JSON/XML/XMP), if requested. A report format
|
|
390
|
+
// suppresses all per-message console output, so nothing was streamed during
|
|
391
|
+
// the run (attachConsoleStream installs no stream when a format is active).
|
|
392
|
+
if (formatActive) {
|
|
393
|
+
const reportText = renderReport(st, result);
|
|
394
|
+
if (st.fileOut === null) {
|
|
395
|
+
process.stdout.write(reportText); // straight to System.out, ignores --quiet
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
const outPath = st.fileOut;
|
|
399
|
+
try {
|
|
400
|
+
await writeFile(outPath, reportText);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
// JSON writer prints this to stdout and returns 1 on IO failure.
|
|
404
|
+
if (st.jsonOutput)
|
|
405
|
+
outln("Incorrect path to save JsonFile.");
|
|
406
|
+
else
|
|
407
|
+
errln("Error while generating the report.");
|
|
408
|
+
printCompleted(st, shown, outln, msgs);
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// 2 & 3) The "Validating using EPUB version X rules." line and the per-message
|
|
414
|
+
// console lines (DefaultReportImpl) were STREAMED LIVE during validate() by
|
|
415
|
+
// attachConsoleStream -- each written to stdout/stderr the instant the
|
|
416
|
+
// engine emitted it, byte-identical to the jar and printed AS the checker
|
|
417
|
+
// produced it (not batched at the end). Only the run-completion chrome
|
|
418
|
+
// below (validateFile's tail + printEpubCheckCompleted) is emitted here.
|
|
419
|
+
// 4) validateFile's tail: no_errors / there_were_warnings / there_were_errors,
|
|
420
|
+
// computed from the reporting-level-filtered counts.
|
|
421
|
+
let exitCode;
|
|
422
|
+
if (shown.fatal === 0 && shown.error === 0 && shown.warning === 0) {
|
|
423
|
+
if (!reportToConsole)
|
|
424
|
+
outln(msgs.no_errors__or_warnings);
|
|
425
|
+
exitCode = 0;
|
|
426
|
+
}
|
|
427
|
+
else if (shown.warning > 0 && shown.fatal === 0 && shown.error === 0) {
|
|
428
|
+
errln(msgs.there_were_warnings);
|
|
429
|
+
exitCode = st.failOnWarnings ? 1 : 0;
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
errln(msgs.there_were_errors);
|
|
433
|
+
exitCode = 1;
|
|
434
|
+
}
|
|
435
|
+
// 4b) -s/--save deletion (EpubChecker.processFile): the packaged file this run
|
|
436
|
+
// wrote is deleted again, with the announcement on stderr, when the check
|
|
437
|
+
// found errors or fatals -- judged on the same reporting-level-filtered
|
|
438
|
+
// counts as the exit code, so `-f` on an error-only book keeps the file
|
|
439
|
+
// (verified against the jar). Warnings keep it too, even with
|
|
440
|
+
// --failonwarnings. The delete-failure text is the jar's hardcoded
|
|
441
|
+
// Archive.deleteEpubFile string (never localized).
|
|
442
|
+
if (savedEpubPath !== null && (shown.fatal > 0 || shown.error > 0)) {
|
|
443
|
+
errln(msgs.deleting_archive);
|
|
444
|
+
try {
|
|
445
|
+
await unlink(savedEpubPath);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
errln("Unable to delete generated archive.");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// 5) The "Messages: ... / EPUBCheck completed" summary (printEpubCheckCompleted).
|
|
452
|
+
// Suppressed when the report went to the console (epubcheck closes stdout).
|
|
453
|
+
if (!reportToConsole) {
|
|
454
|
+
printCompleted(st, shown, outln, msgs);
|
|
455
|
+
}
|
|
456
|
+
return exitCode;
|
|
457
|
+
}
|
|
458
|
+
/** printEpubCheckCompleted: the summary counters + "EPUBCheck completed". */
|
|
459
|
+
function printCompleted(st, counts, outln, msgs) {
|
|
460
|
+
// The "Messages: ..." line is the library's shared summary renderer (the same
|
|
461
|
+
// one the batch formatConsoleReport uses); the CLI supplies its own localized
|
|
462
|
+
// label table (Messages structurally satisfies ConsoleLabels).
|
|
463
|
+
const line = renderConsoleSummaryLine(counts, st.reportingLevel, msgs);
|
|
464
|
+
if (line.length > 0) {
|
|
465
|
+
// messageCount.append("\n"); outWriter.println(messageCount) -> "...\n\n"
|
|
466
|
+
outln(line + "\n");
|
|
467
|
+
}
|
|
468
|
+
outln(msgs.epubcheck_completed);
|
|
469
|
+
}
|
|
470
|
+
function renderReport(st, result) {
|
|
471
|
+
// The report content (result.reports) is rendered by the library's formatters
|
|
472
|
+
// from the run's live report-event stream -- byte-identical to epubcheck's
|
|
473
|
+
// own --json/--out/--xmp writers, from ONE validation run.
|
|
474
|
+
const reports = result.reports ?? {};
|
|
475
|
+
if (st.xmlOutput)
|
|
476
|
+
return reports.xml ?? "";
|
|
477
|
+
if (st.xmpOutput)
|
|
478
|
+
return reports.xmp ?? "";
|
|
479
|
+
return reports.json ?? "";
|
|
480
|
+
}
|
|
481
|
+
/** The shipped locale tags, in the order epubcheck's help lists them, for the
|
|
482
|
+
* unsupported-locale message. */
|
|
483
|
+
const SUPPORTED_LOCALE_LIST = "da, de, en, es, fr, it, ja, ko-KR, nl, pt-BR, zh-TW";
|
|
484
|
+
/** Returns a human description of the first unsupported flag, or null. */
|
|
485
|
+
function unsupportedReason(st) {
|
|
486
|
+
// A locale is supportable when it resolves to a shipped bundle through its own
|
|
487
|
+
// ResourceBundle candidate chain (region -> language), independently of the host
|
|
488
|
+
// default locale. A tag that would need Java's host-dependent default-locale
|
|
489
|
+
// fallback (bare ko/pt/zh, or an unshipped language like pl/ru) is refused: its
|
|
490
|
+
// jar output is not provably byte-identical across hosts.
|
|
491
|
+
if (st.localeTag !== null && resolveLocale(st.localeTag) === null) {
|
|
492
|
+
return `--locale ${st.localeTag} (the engine image only ships these locales: ${SUPPORTED_LOCALE_LIST})`;
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* The epubcheck CLI arguments to hand to the engine, placed before the input path
|
|
498
|
+
* exactly as on the stock CLI. Only flags the CLI cannot reproduce post-hoc from a
|
|
499
|
+
* default INFO-level run are passed through: `-u` (USAGE is below INFO, so the
|
|
500
|
+
* engine must run at that level for the tap to carry usage messages), `--mode`
|
|
501
|
+
* with its `-v` version (single-file checking), a non-default `--profile`, and
|
|
502
|
+
* `--locale` (localized message text + locale-sensitive validation). Everything
|
|
503
|
+
* else (severity filtering, --quiet, report files, --save, --failonwarnings) stays
|
|
504
|
+
* a JS-layer concern so its byte-identical behavior is preserved.
|
|
505
|
+
*
|
|
506
|
+
* `forDirectory` builds the args for expanded mode (a directory source): the
|
|
507
|
+
* engine injects `--mode exp` itself and rejects a conflicting `--mode`/`-v`, so
|
|
508
|
+
* the single-file mode/version pair is omitted while `-u`, `--profile`, and
|
|
509
|
+
* `--locale` still ride through.
|
|
510
|
+
*/
|
|
511
|
+
function buildEngineArgs(st, forDirectory = false) {
|
|
512
|
+
const args = [];
|
|
513
|
+
if (st.reportingLevel === ReportingLevel.Usage) {
|
|
514
|
+
args.push("-u");
|
|
515
|
+
}
|
|
516
|
+
if (!forDirectory && st.mode !== null) {
|
|
517
|
+
args.push("--mode", st.mode, "-v", st.version);
|
|
518
|
+
}
|
|
519
|
+
if (st.profile !== null && st.profile !== "DEFAULT" && KNOWN_PROFILES.has(st.profile)) {
|
|
520
|
+
args.push("--profile", st.profile.toLowerCase());
|
|
521
|
+
}
|
|
522
|
+
if (st.localeTag !== null) {
|
|
523
|
+
args.push("--locale", st.localeTag);
|
|
524
|
+
}
|
|
525
|
+
return args;
|
|
526
|
+
}
|
|
527
|
+
/** The epubcheck engine options for a run: the passthrough args plus, when
|
|
528
|
+
* -c/--customMessages named a file, the custom-message overrides. An EXISTING
|
|
529
|
+
* overrides file rides through the `customMessages` option (the engine mounts it
|
|
530
|
+
* and points -c at the mounted copy, so the CHK-00x locations print ./<name>);
|
|
531
|
+
* a MISSING one is left on `args` verbatim so the engine emits CHK-001 exactly
|
|
532
|
+
* like the jar does for a missing cwd-relative file. */
|
|
533
|
+
async function buildEngineOpts(st, cwd, forDirectory) {
|
|
534
|
+
const args = buildEngineArgs(st, forDirectory);
|
|
535
|
+
const opts = { args };
|
|
536
|
+
if (st.customMessagesPath !== null) {
|
|
537
|
+
const cmAbs = pathResolve(cwd, st.customMessagesPath);
|
|
538
|
+
// An EXISTING overrides file is fed by CONTENT under its basename (the engine
|
|
539
|
+
// mounts it and points -c at that name); a MISSING one is left on args
|
|
540
|
+
// verbatim so the engine emits CHK-001 like the jar does for a missing file.
|
|
541
|
+
if (await pathExists(cmAbs)) {
|
|
542
|
+
opts.customMessages = await readFile(cmAbs, "utf8");
|
|
543
|
+
opts.customMessagesName = basename(st.customMessagesPath);
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
args.push("-c", st.customMessagesPath);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// Report output comes via the library's reports option (writer-identical
|
|
550
|
+
// documents rendered from the run's live report-event stream).
|
|
551
|
+
const reports = [];
|
|
552
|
+
if (st.jsonOutput)
|
|
553
|
+
reports.push("json");
|
|
554
|
+
if (st.xmlOutput)
|
|
555
|
+
reports.push("xml");
|
|
556
|
+
if (st.xmpOutput)
|
|
557
|
+
reports.push("xmp");
|
|
558
|
+
if (reports.length > 0)
|
|
559
|
+
opts.reports = reports;
|
|
560
|
+
return opts;
|
|
561
|
+
}
|
|
562
|
+
/** Faithful port of EpubChecker.processArguments (returns false == exit 1). */
|
|
563
|
+
async function processArguments(args, st, e) {
|
|
564
|
+
if (args.length < 1) {
|
|
565
|
+
e.errln(M.argument_needed);
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
// Env var pre-load (setCustomMessageFileFromEnvironment).
|
|
569
|
+
const envFile = process.env["ePubCheckCustomMessageFile"];
|
|
570
|
+
if (envFile && envFile.length > 0 && (await pathExists(envFile))) {
|
|
571
|
+
st.customMessagesPath = envFile;
|
|
572
|
+
}
|
|
573
|
+
const argPattern = /^--?(.*)$/;
|
|
574
|
+
for (let i = 0; i < args.length; i++) {
|
|
575
|
+
const arg = args[i];
|
|
576
|
+
const match = argPattern.exec(arg);
|
|
577
|
+
if (match) {
|
|
578
|
+
const key = match[1];
|
|
579
|
+
switch (key) {
|
|
580
|
+
case "v":
|
|
581
|
+
if (i + 1 < args.length) {
|
|
582
|
+
++i;
|
|
583
|
+
const v = args[i];
|
|
584
|
+
if (v === "2.0" || v === "2")
|
|
585
|
+
st.version = "2.0";
|
|
586
|
+
else if (v === "3.0" || v === "3")
|
|
587
|
+
st.version = "3.0";
|
|
588
|
+
else {
|
|
589
|
+
e.outln(M.display_help);
|
|
590
|
+
throw new ParseError("unsupported version");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
e.outln(M.display_help);
|
|
595
|
+
throw new ParseError("version argument expected");
|
|
596
|
+
}
|
|
597
|
+
break;
|
|
598
|
+
case "m":
|
|
599
|
+
case "mode":
|
|
600
|
+
if (i + 1 < args.length) {
|
|
601
|
+
st.mode = args[++i];
|
|
602
|
+
st.expanded = st.mode === "exp";
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
e.outln(M.display_help);
|
|
606
|
+
throw new ParseError("mode argument expected");
|
|
607
|
+
}
|
|
608
|
+
break;
|
|
609
|
+
case "p":
|
|
610
|
+
case "profile":
|
|
611
|
+
if (i + 1 < args.length) {
|
|
612
|
+
const profileStr = args[++i];
|
|
613
|
+
const up = profileStr.toUpperCase();
|
|
614
|
+
if (KNOWN_PROFILES.has(up)) {
|
|
615
|
+
st.profile = up;
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
// epubcheck prints the (mis-keyed) mode_version_ignored text and
|
|
619
|
+
// falls back to the default profile.
|
|
620
|
+
e.errln(M.mode_version_ignored);
|
|
621
|
+
st.profile = "DEFAULT";
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
e.outln(M.display_help);
|
|
626
|
+
throw new ParseError("profile argument expected");
|
|
627
|
+
}
|
|
628
|
+
break;
|
|
629
|
+
case "s":
|
|
630
|
+
case "save":
|
|
631
|
+
// epubcheck's `keep` flag: with an explicit `--mode exp`, keep the
|
|
632
|
+
// packaged .epub the expanded check builds beside the input directory
|
|
633
|
+
// (it is deleted again when the check finds errors or fatals).
|
|
634
|
+
st.save = true;
|
|
635
|
+
break;
|
|
636
|
+
case "o":
|
|
637
|
+
case "out":
|
|
638
|
+
i += await consumeOutputArg(args, i, st, "xml");
|
|
639
|
+
st.xmlOutput = true;
|
|
640
|
+
break;
|
|
641
|
+
case "j":
|
|
642
|
+
case "json":
|
|
643
|
+
i += await consumeOutputArg(args, i, st, "json");
|
|
644
|
+
st.jsonOutput = true;
|
|
645
|
+
break;
|
|
646
|
+
case "x":
|
|
647
|
+
case "xmp":
|
|
648
|
+
i += await consumeOutputArg(args, i, st, "xmp");
|
|
649
|
+
st.xmpOutput = true;
|
|
650
|
+
break;
|
|
651
|
+
case "i":
|
|
652
|
+
case "info":
|
|
653
|
+
st.reportingLevel = ReportingLevel.Info;
|
|
654
|
+
break;
|
|
655
|
+
case "f":
|
|
656
|
+
case "fatal":
|
|
657
|
+
st.reportingLevel = ReportingLevel.Fatal;
|
|
658
|
+
break;
|
|
659
|
+
case "e":
|
|
660
|
+
case "error":
|
|
661
|
+
st.reportingLevel = ReportingLevel.Error;
|
|
662
|
+
break;
|
|
663
|
+
case "w":
|
|
664
|
+
case "warn":
|
|
665
|
+
st.reportingLevel = ReportingLevel.Warning;
|
|
666
|
+
break;
|
|
667
|
+
case "u":
|
|
668
|
+
case "usage":
|
|
669
|
+
st.reportingLevel = ReportingLevel.Usage;
|
|
670
|
+
break;
|
|
671
|
+
case "q":
|
|
672
|
+
case "quiet":
|
|
673
|
+
st.quiet = true;
|
|
674
|
+
break;
|
|
675
|
+
case "failonwarnings":
|
|
676
|
+
st.failOnWarnings = true;
|
|
677
|
+
break;
|
|
678
|
+
case "r":
|
|
679
|
+
case "redir":
|
|
680
|
+
if (i + 1 < args.length) {
|
|
681
|
+
st.fileOut = args[++i];
|
|
682
|
+
}
|
|
683
|
+
break;
|
|
684
|
+
case "c":
|
|
685
|
+
case "customMessages":
|
|
686
|
+
if (i + 1 < args.length) {
|
|
687
|
+
const fileName = args[i + 1];
|
|
688
|
+
if (fileName.toLowerCase() === "none") {
|
|
689
|
+
st.customMessagesPath = null;
|
|
690
|
+
++i;
|
|
691
|
+
}
|
|
692
|
+
else if (!fileName.startsWith("-")) {
|
|
693
|
+
st.customMessagesPath = fileName;
|
|
694
|
+
++i;
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
e.errln(formatTemplate(M.expected_message_filename, fileName));
|
|
698
|
+
e.displayHelp();
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
break;
|
|
703
|
+
case "l":
|
|
704
|
+
case "listChecks":
|
|
705
|
+
if (i + 1 < args.length) {
|
|
706
|
+
if (!args[i + 1].startsWith("-")) {
|
|
707
|
+
st.listChecksOut = args[++i];
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
st.listChecksOut = null;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
st.listChecks = true;
|
|
714
|
+
break;
|
|
715
|
+
case "locale":
|
|
716
|
+
if (i + 1 < args.length) {
|
|
717
|
+
if (args[i + 1].startsWith("-")) {
|
|
718
|
+
e.errln(formatTemplate(M.incorrect_locale, args[i + 1]));
|
|
719
|
+
e.displayHelp();
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
st.localeTag = args[++i];
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
e.errln(formatTemplate(M.missing_locale));
|
|
728
|
+
e.displayHelp();
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
break;
|
|
732
|
+
case "h":
|
|
733
|
+
case "?":
|
|
734
|
+
case "help":
|
|
735
|
+
e.displayHelp();
|
|
736
|
+
st.displayHelp = true;
|
|
737
|
+
break;
|
|
738
|
+
case "version":
|
|
739
|
+
e.displayVersion();
|
|
740
|
+
st.displayVersion = true;
|
|
741
|
+
break;
|
|
742
|
+
default:
|
|
743
|
+
e.errln(formatTemplate(M.unrecognized_argument, arg));
|
|
744
|
+
e.displayHelp();
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
if (st.path === null) {
|
|
750
|
+
st.path = arg;
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
e.errln(formatTemplate(M.unrecognized_argument, arg));
|
|
754
|
+
e.displayHelp();
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if ((st.xmlOutput && st.xmpOutput) ||
|
|
760
|
+
(st.xmlOutput && st.jsonOutput) ||
|
|
761
|
+
(st.xmpOutput && st.jsonOutput)) {
|
|
762
|
+
e.errln(M.output_type_conflict);
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
if (st.path !== null) {
|
|
766
|
+
st.path = st.path.replace(/\\/g, "/");
|
|
767
|
+
}
|
|
768
|
+
if (st.path === null) {
|
|
769
|
+
if (st.listChecks || st.displayHelp || st.displayVersion) {
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
e.errln(M.no_file_specified);
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
else if (/^.+\.[Ee][Pp][Uu][Bb]$/.test(st.path)) {
|
|
776
|
+
if (st.mode !== null || st.version !== "3.0") {
|
|
777
|
+
e.errln(M.mode_version_ignored);
|
|
778
|
+
st.mode = null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
else if (st.mode === null && st.profile === null) {
|
|
782
|
+
e.outln(M.mode_required);
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* The -o/-j/-x value rule (EpubChecker lines 570-647): explicit filename, "-"
|
|
789
|
+
* for console (fileOut=null), or auto-derive the report filename. Sets
|
|
790
|
+
* st.fileOut and returns how many extra args were consumed (0 or 1).
|
|
791
|
+
*/
|
|
792
|
+
async function consumeOutputArg(args, i, st, kind) {
|
|
793
|
+
const next = args[i + 1];
|
|
794
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
795
|
+
st.fileOut = next;
|
|
796
|
+
return 1;
|
|
797
|
+
}
|
|
798
|
+
else if (next !== undefined && next.toLowerCase() === "-") {
|
|
799
|
+
st.fileOut = null;
|
|
800
|
+
return 1;
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
// Auto-derive. epubcheck's own rule (EpubChecker.processArguments): when the
|
|
804
|
+
// input is a directory it writes the report beside it, at
|
|
805
|
+
// <absolute-parent>/<basename>check.<ext>; otherwise it appends
|
|
806
|
+
// "check.<ext>" to the raw path.
|
|
807
|
+
//
|
|
808
|
+
// `path` may still be null here if the flag preceded the positional (e.g.
|
|
809
|
+
// `-o -q`, `-o` last, `-j -x`, or even `-o -q book.epub` -- the positional
|
|
810
|
+
// is seen only later in the loop). The jar builds `new File(path)` at THIS
|
|
811
|
+
// point, so a null path is `new File((String)null)` -> NullPointerException,
|
|
812
|
+
// thrown inside processArguments and swallowed by run()'s
|
|
813
|
+
// `catch (Exception ignored){returnValue=1}` -> exit 1 with NO stdout/stderr.
|
|
814
|
+
// Reproduce that exactly: a silent ParseError (run() returns 1, nothing
|
|
815
|
+
// printed), NOT our former "No file specified in the arguments." text.
|
|
816
|
+
if (st.path === null) {
|
|
817
|
+
throw new ParseError("-o/-j/-x auto-derive with no input path (jar NPE)");
|
|
818
|
+
}
|
|
819
|
+
if ((await pathExists(st.path)) && (await stat(st.path)).isDirectory()) {
|
|
820
|
+
const abs = pathResolve(st.path);
|
|
821
|
+
st.fileOut = pathJoin(dirname(abs), basename(abs) + "check." + kind);
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
st.fileOut = st.path + "check." + kind;
|
|
825
|
+
}
|
|
826
|
+
return 0;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// --- Entry point ------------------------------------------------------------
|
|
830
|
+
// Run only when invoked as the CLI binary (not when imported by tests). Compare
|
|
831
|
+
// resolved real paths so it also fires through the `node_modules/.bin` symlink.
|
|
832
|
+
async function isMainModule() {
|
|
833
|
+
const argv1 = process.argv[1];
|
|
834
|
+
if (!argv1)
|
|
835
|
+
return false;
|
|
836
|
+
try {
|
|
837
|
+
return (await realpath(argv1)) === (await realpath(fileURLToPath(import.meta.url)));
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// Top-level await is available (this is an ES module); resolving the two
|
|
844
|
+
// realpaths asynchronously keeps the bootstrap guard off the event loop like
|
|
845
|
+
// every other fs touch (async law). Importers (the tests) just await the
|
|
846
|
+
// module's evaluation as they already do for any ESM; isMainModule() is false
|
|
847
|
+
// for them so run() is never invoked.
|
|
848
|
+
if (await isMainModule()) {
|
|
849
|
+
run(process.argv.slice(2)).then((code) => {
|
|
850
|
+
process.exitCode = code;
|
|
851
|
+
}, (err) => {
|
|
852
|
+
process.stderr.write(String(err?.stack ?? err) + "\n");
|
|
853
|
+
process.exitCode = 1;
|
|
854
|
+
});
|
|
855
|
+
}
|