formatferry 1.0.29 → 1.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -5
- package/dist/index.js +44 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,11 +44,44 @@ already have real text. It is deliberately absent here — the CLI ships no WASM
|
|
|
44
44
|
OCR engine, which keeps the install small and the conversion fully offline.
|
|
45
45
|
|
|
46
46
|
For a PDF that mixes real text pages with scanned inserts, the conversion
|
|
47
|
-
**succeeds**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
still **succeeds** with exit code 0 — the document has enough text elsewhere
|
|
48
|
+
to avoid the `IMAGE_ONLY` error above — and the scanned pages come through
|
|
49
|
+
empty. That loss is no longer silent: stderr carries one warning naming the
|
|
50
|
+
pages that could not be fully read:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Warning: Page(s) 3 could not be fully read: they have little or no extractable text and OCR is not available in this environment, so their content may be missing or incomplete.
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pipelines that would rather fail than accept a warned conversion can pass
|
|
57
|
+
`--strict`. The complete output is still written (to stdout or the `-o`
|
|
58
|
+
file), and the process then exits 1 after a summary line:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
$ formatferry -i mixed.pdf --strict; echo "exit=$?"
|
|
62
|
+
Warning: Page(s) 3 could not be fully read: they have little or no extractable text and OCR is not available in this environment, so their content may be missing or incomplete.
|
|
63
|
+
Error: --strict: conversion produced 1 warning(s) [pdf.warnings.ocrUnavailablePages].
|
|
64
|
+
exit=1
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The summary names the machine-readable warning code(s) in brackets, so
|
|
68
|
+
pipelines can triage without parsing prose.
|
|
69
|
+
|
|
70
|
+
Be aware that **any** warning trips `--strict`, not only page-loss warnings:
|
|
71
|
+
advisory notices count too. For example, the xlsx warning "This spreadsheet
|
|
72
|
+
has many columns" fires on wide-but-ordinary spreadsheets and would exit 1
|
|
73
|
+
under `--strict`. This any-warning meaning of bare `--strict` is the permanent
|
|
74
|
+
contract: if a curated data-loss-only mode is ever added, it will be a
|
|
75
|
+
separate additive spelling (for example `--strict=loss`) — bare `--strict`
|
|
76
|
+
will not change meaning underneath existing pipelines.
|
|
77
|
+
|
|
78
|
+
Under `--batch`, each file's warnings are printed to stderr as
|
|
79
|
+
`Warning (<file>): <message>`, and `--strict` exits 1 after the batch
|
|
80
|
+
completes (all outputs still written) if any file warned.
|
|
81
|
+
|
|
82
|
+
One caveat for MCP users: whether the external `formatferry-mcp` wrapper
|
|
83
|
+
forwards `--strict` to the CLI has not been verified; if it filters unknown
|
|
84
|
+
flags, the flag is inert there.
|
|
52
85
|
|
|
53
86
|
Conversion warnings that *are* produced (for example unusual table structures)
|
|
54
87
|
are written to stderr, never stdout, so `formatferry -i doc.pdf > out.md` keeps
|
|
@@ -72,6 +105,7 @@ formatferry [options]
|
|
|
72
105
|
| `--url <url>` | Fetch and convert a web article (requires API key) |
|
|
73
106
|
| `--batch <glob>` | Batch convert files matching a glob pattern (Premium) |
|
|
74
107
|
| `--output-dir <dir>` | Output directory for batch mode |
|
|
108
|
+
| `--strict` | Exit with code 1 if the conversion produced any warnings (including advisory ones) |
|
|
75
109
|
|
|
76
110
|
#### Input Modes
|
|
77
111
|
|
package/dist/index.js
CHANGED
|
@@ -61636,6 +61636,9 @@ function localize2(translate, key, vars) {
|
|
|
61636
61636
|
if (key === "pdf.warnings.ocrPageSkippedTooLarge") {
|
|
61637
61637
|
return `Page(s) ${vars.pages} are too large to OCR and were skipped.`;
|
|
61638
61638
|
}
|
|
61639
|
+
if (key === "pdf.warnings.ocrUnavailablePages") {
|
|
61640
|
+
return `Page(s) ${vars.pages} could not be fully read: they have little or no extractable text and OCR is not available in this environment, so their content may be missing or incomplete.`;
|
|
61641
|
+
}
|
|
61639
61642
|
return key;
|
|
61640
61643
|
}
|
|
61641
61644
|
var PdfAdapter = class {
|
|
@@ -62039,6 +62042,9 @@ var PdfAdapter = class {
|
|
|
62039
62042
|
}
|
|
62040
62043
|
}
|
|
62041
62044
|
}
|
|
62045
|
+
if (routedPages.length > 0 && !fullyScanned && !ocrRan) {
|
|
62046
|
+
pushCodedWarning("pdf.warnings.ocrUnavailablePages", routedPages);
|
|
62047
|
+
}
|
|
62042
62048
|
let documentHasUsableText = false;
|
|
62043
62049
|
if (ocrRan) {
|
|
62044
62050
|
const { isUsableText: isUsableText2 } = await Promise.resolve().then(() => (init_pdf_ocr_adapter(), pdf_ocr_adapter_exports));
|
|
@@ -63941,7 +63947,7 @@ async function binaryToMarkdown(buffer, type, format) {
|
|
|
63941
63947
|
preserveLists: true,
|
|
63942
63948
|
customRules: []
|
|
63943
63949
|
});
|
|
63944
|
-
return { markdown: converted.markdown, warnings: result.warnings };
|
|
63950
|
+
return { markdown: converted.markdown, warnings: result.warnings, warningCodes: result.warningCodes };
|
|
63945
63951
|
}
|
|
63946
63952
|
case "pdf": {
|
|
63947
63953
|
const result = await pdfAdapter.parseToText(toArrayBuffer());
|
|
@@ -63952,7 +63958,7 @@ async function binaryToMarkdown(buffer, type, format) {
|
|
|
63952
63958
|
preserveLists: true,
|
|
63953
63959
|
customRules: []
|
|
63954
63960
|
});
|
|
63955
|
-
return { markdown: converted.markdown, warnings: result.warnings };
|
|
63961
|
+
return { markdown: converted.markdown, warnings: result.warnings, warningCodes: result.warningCodes };
|
|
63956
63962
|
}
|
|
63957
63963
|
case "xlsx":
|
|
63958
63964
|
case "csv": {
|
|
@@ -63964,7 +63970,7 @@ async function binaryToMarkdown(buffer, type, format) {
|
|
|
63964
63970
|
preserveLists: true,
|
|
63965
63971
|
customRules: []
|
|
63966
63972
|
});
|
|
63967
|
-
return { markdown: converted.markdown, warnings: result.warnings };
|
|
63973
|
+
return { markdown: converted.markdown, warnings: result.warnings, warningCodes: result.warningCodes };
|
|
63968
63974
|
}
|
|
63969
63975
|
case "json": {
|
|
63970
63976
|
const result = await dataAdapter.parseJsonToHtml(toArrayBuffer());
|
|
@@ -63975,7 +63981,7 @@ async function binaryToMarkdown(buffer, type, format) {
|
|
|
63975
63981
|
preserveLists: true,
|
|
63976
63982
|
customRules: []
|
|
63977
63983
|
});
|
|
63978
|
-
return { markdown: converted.markdown, warnings: result.warnings };
|
|
63984
|
+
return { markdown: converted.markdown, warnings: result.warnings, warningCodes: result.warningCodes };
|
|
63979
63985
|
}
|
|
63980
63986
|
case "xml": {
|
|
63981
63987
|
const result = await dataAdapter.parseXmlToHtml(toArrayBuffer());
|
|
@@ -63986,7 +63992,7 @@ async function binaryToMarkdown(buffer, type, format) {
|
|
|
63986
63992
|
preserveLists: true,
|
|
63987
63993
|
customRules: []
|
|
63988
63994
|
});
|
|
63989
|
-
return { markdown: converted.markdown, warnings: result.warnings };
|
|
63995
|
+
return { markdown: converted.markdown, warnings: result.warnings, warningCodes: result.warningCodes };
|
|
63990
63996
|
}
|
|
63991
63997
|
case "pptx": {
|
|
63992
63998
|
const result = await pptxAdapter.parseToHtml(toArrayBuffer());
|
|
@@ -63997,7 +64003,7 @@ async function binaryToMarkdown(buffer, type, format) {
|
|
|
63997
64003
|
preserveLists: true,
|
|
63998
64004
|
customRules: []
|
|
63999
64005
|
});
|
|
64000
|
-
return { markdown: converted.markdown, warnings: result.warnings };
|
|
64006
|
+
return { markdown: converted.markdown, warnings: result.warnings, warningCodes: result.warningCodes };
|
|
64001
64007
|
}
|
|
64002
64008
|
}
|
|
64003
64009
|
}
|
|
@@ -64111,6 +64117,7 @@ async function convertCommand(options) {
|
|
|
64111
64117
|
}
|
|
64112
64118
|
let markdown;
|
|
64113
64119
|
let conversionWarnings = [];
|
|
64120
|
+
let conversionWarningCodes = [];
|
|
64114
64121
|
if (options.input) {
|
|
64115
64122
|
let fileBuffer;
|
|
64116
64123
|
try {
|
|
@@ -64139,6 +64146,7 @@ async function convertCommand(options) {
|
|
|
64139
64146
|
const binary = await binaryToMarkdown(fileBuffer, inputType, format);
|
|
64140
64147
|
markdown = binary.markdown;
|
|
64141
64148
|
conversionWarnings = binary.warnings;
|
|
64149
|
+
conversionWarningCodes = binary.warningCodes ?? [];
|
|
64142
64150
|
}
|
|
64143
64151
|
} else {
|
|
64144
64152
|
const html = await readStdin();
|
|
@@ -64170,6 +64178,13 @@ async function convertCommand(options) {
|
|
|
64170
64178
|
} else {
|
|
64171
64179
|
process.stdout.write(markdown);
|
|
64172
64180
|
}
|
|
64181
|
+
if (options.strict && conversionWarnings.length > 0) {
|
|
64182
|
+
const codes = [...new Set(conversionWarningCodes.map((w) => w.code))];
|
|
64183
|
+
const codeList = codes.length > 0 ? ` [${codes.join(", ")}]` : "";
|
|
64184
|
+
console.error(`Error: --strict: conversion produced ${conversionWarnings.length} warning(s)${codeList}.`);
|
|
64185
|
+
process.exitCode = 1;
|
|
64186
|
+
return;
|
|
64187
|
+
}
|
|
64173
64188
|
}
|
|
64174
64189
|
|
|
64175
64190
|
// src/commands/batch.ts
|
|
@@ -64195,38 +64210,38 @@ async function convertFile(filePath, format) {
|
|
|
64195
64210
|
preserveLists: true,
|
|
64196
64211
|
customRules: []
|
|
64197
64212
|
});
|
|
64198
|
-
return result.markdown;
|
|
64213
|
+
return { markdown: result.markdown, warnings: [] };
|
|
64199
64214
|
}
|
|
64200
64215
|
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
64201
64216
|
if (ext === ".docx") {
|
|
64202
64217
|
const r = await docxAdapter.parseToHtml(arrayBuffer);
|
|
64203
64218
|
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
64204
|
-
return c.markdown;
|
|
64219
|
+
return { markdown: c.markdown, warnings: r.warnings ?? [] };
|
|
64205
64220
|
}
|
|
64206
64221
|
if (ext === ".pdf") {
|
|
64207
64222
|
const r = await pdfAdapter.parseToText(arrayBuffer);
|
|
64208
64223
|
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
64209
|
-
return c.markdown;
|
|
64224
|
+
return { markdown: c.markdown, warnings: r.warnings ?? [] };
|
|
64210
64225
|
}
|
|
64211
64226
|
if (ext === ".xlsx" || ext === ".csv") {
|
|
64212
64227
|
const r = await spreadsheetAdapter.parseToHtml(arrayBuffer, ext.slice(1), 0);
|
|
64213
64228
|
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
64214
|
-
return c.markdown;
|
|
64229
|
+
return { markdown: c.markdown, warnings: r.warnings ?? [] };
|
|
64215
64230
|
}
|
|
64216
64231
|
if (ext === ".json") {
|
|
64217
64232
|
const r = await dataAdapter.parseJsonToHtml(arrayBuffer);
|
|
64218
64233
|
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
64219
|
-
return c.markdown;
|
|
64234
|
+
return { markdown: c.markdown, warnings: r.warnings ?? [] };
|
|
64220
64235
|
}
|
|
64221
64236
|
if (ext === ".xml") {
|
|
64222
64237
|
const r = await dataAdapter.parseXmlToHtml(arrayBuffer);
|
|
64223
64238
|
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
64224
|
-
return c.markdown;
|
|
64239
|
+
return { markdown: c.markdown, warnings: r.warnings ?? [] };
|
|
64225
64240
|
}
|
|
64226
64241
|
if (ext === ".pptx") {
|
|
64227
64242
|
const r = await pptxAdapter.parseToHtml(arrayBuffer);
|
|
64228
64243
|
const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
|
|
64229
|
-
return c.markdown;
|
|
64244
|
+
return { markdown: c.markdown, warnings: r.warnings ?? [] };
|
|
64230
64245
|
}
|
|
64231
64246
|
throw new Error(`Unsupported file type: ${ext}`);
|
|
64232
64247
|
}
|
|
@@ -64268,11 +64283,17 @@ async function batchCommand(options) {
|
|
|
64268
64283
|
}
|
|
64269
64284
|
const format = options.format ?? "github";
|
|
64270
64285
|
if (options.outputDir) mkdirSync2(options.outputDir, { recursive: true });
|
|
64271
|
-
let successful = 0, failed = 0;
|
|
64286
|
+
let successful = 0, failed = 0, batchWarnings = 0;
|
|
64272
64287
|
let totalOriginal = 0, totalCompressed = 0, toonifiedCount = 0;
|
|
64273
64288
|
for (const filePath of files) {
|
|
64274
64289
|
try {
|
|
64275
|
-
|
|
64290
|
+
const converted = await convertFile(filePath, format);
|
|
64291
|
+
for (const w of converted.warnings) {
|
|
64292
|
+
process.stderr.write(`Warning (${basename(filePath)}): ${w}
|
|
64293
|
+
`);
|
|
64294
|
+
}
|
|
64295
|
+
batchWarnings += converted.warnings.length;
|
|
64296
|
+
let markdown = converted.markdown;
|
|
64276
64297
|
if (options.toonify) {
|
|
64277
64298
|
if (markdown.trim()) {
|
|
64278
64299
|
if (Buffer.byteLength(markdown, "utf8") > TOONIFY_MAX_INPUT_BYTES) {
|
|
@@ -64311,6 +64332,11 @@ async function batchCommand(options) {
|
|
|
64311
64332
|
process.stderr.write(`
|
|
64312
64333
|
Batch complete: ${successful} succeeded, ${failed} failed, ${files.length} total
|
|
64313
64334
|
`);
|
|
64335
|
+
if (options.strict && batchWarnings > 0) {
|
|
64336
|
+
process.stderr.write(`Error: --strict: conversion produced ${batchWarnings} warning(s) across the batch.
|
|
64337
|
+
`);
|
|
64338
|
+
process.exitCode = 1;
|
|
64339
|
+
}
|
|
64314
64340
|
}
|
|
64315
64341
|
|
|
64316
64342
|
// src/commands/auth.ts
|
|
@@ -64518,8 +64544,8 @@ import { readFileSync as readFileSync5 } from "fs";
|
|
|
64518
64544
|
import { resolve, dirname as dirname3 } from "path";
|
|
64519
64545
|
import { fileURLToPath } from "url";
|
|
64520
64546
|
var getVersion = () => {
|
|
64521
|
-
if ("1.0.
|
|
64522
|
-
return "1.0.
|
|
64547
|
+
if ("1.0.30") {
|
|
64548
|
+
return "1.0.30";
|
|
64523
64549
|
}
|
|
64524
64550
|
try {
|
|
64525
64551
|
const __dirname2 = dirname3(fileURLToPath(import.meta.url));
|
|
@@ -64532,7 +64558,7 @@ var getVersion = () => {
|
|
|
64532
64558
|
}
|
|
64533
64559
|
};
|
|
64534
64560
|
var VERSION = getVersion();
|
|
64535
|
-
program.name("formatferry").version(VERSION).description("Convert HTML, DOCX, PDF, XLSX, CSV, JSON, XML, PPTX to Markdown").option("-i, --input <path>", "Input file path").option("-o, --output <path>", "Output file path").option("-f, --format <format>", "Markdown format (github, commonmark, slack, discord, reddit, confluence, gdocs, rmarkdown)", "github").option("--url <url>", "Fetch article from URL").option("--batch <glob>", "Batch convert files (Premium)").option("--output-dir <dir>", "Output directory for batch").option("--toonify", "Compress output to TOON format (Premium)").option("--offline", "Skip entitlement check (use cached or free tier)").option("--timeout <ms>", "License server timeout in milliseconds (default: 2000)", "2000").action(async (options) => {
|
|
64561
|
+
program.name("formatferry").version(VERSION).description("Convert HTML, DOCX, PDF, XLSX, CSV, JSON, XML, PPTX to Markdown").option("-i, --input <path>", "Input file path").option("-o, --output <path>", "Output file path").option("-f, --format <format>", "Markdown format (github, commonmark, slack, discord, reddit, confluence, gdocs, rmarkdown)", "github").option("--url <url>", "Fetch article from URL").option("--batch <glob>", "Batch convert files (Premium)").option("--output-dir <dir>", "Output directory for batch").option("--toonify", "Compress output to TOON format (Premium)").option("--offline", "Skip entitlement check (use cached or free tier)").option("--timeout <ms>", "License server timeout in milliseconds (default: 2000)", "2000").option("--strict", "Exit with code 1 if the conversion produced any warnings (including advisory ones)").action(async (options) => {
|
|
64536
64562
|
if (options.batch) {
|
|
64537
64563
|
await batchCommand(options);
|
|
64538
64564
|
} else {
|