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.
Files changed (3) hide show
  1. package/README.md +39 -5
  2. package/dist/index.js +44 -18
  3. 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** and the scanned pages come through empty. The document has enough
48
- text elsewhere to avoid the `IMAGE_ONLY` error above, so the exit code is 0 and
49
- the only sign is a page heading with nothing under it. Check the output when a
50
- PDF may contain scans — for this case the CLI cannot tell you which pages it
51
- could not read, because no OCR ran and so nothing reported a per-page failure.
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
- let markdown = await convertFile(filePath, format);
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.29") {
64522
- return "1.0.29";
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formatferry",
3
- "version": "1.0.29",
3
+ "version": "1.0.30",
4
4
  "description": "FormatFerry CLI — Convert HTML, DOCX, PDF, XLSX, CSV, JSON, XML, PPTX to Markdown",
5
5
  "keywords": [
6
6
  "markdown",