formatferry 1.0.11 → 1.0.12

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 (2) hide show
  1. package/dist/index.js +98 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21361,16 +21361,45 @@ var DATE_LIKE_REGEX = /^\d{1,4}[-/.]\d{1,2}([-/.]\d{1,4})?$/;
21361
21361
  var SUPERSCRIPT_MERGE_GAP_FACTOR = 0.3;
21362
21362
  var VERTICAL_OVERLAP_THRESHOLD = 0.5;
21363
21363
  var COLUMN_SUPPORT_THRESHOLD = 0.6;
21364
- var COLUMN_GAP_MERGE_FACTOR = 0.5;
21364
+ var COLUMN_GAP_MERGE_FACTOR = 0.35;
21365
+ var COLUMN_BOUNDARY_VOTE_TOLERANCE_FACTOR = 0.5;
21366
+ var MIN_CELL_OVERLAP_SCORE = 0.35;
21367
+ var WIDE_WHITESPACE_MIN_RUN = 2;
21365
21368
  var SHORT_CELL_THRESHOLD_LATIN = 40;
21366
21369
  var SHORT_CELL_THRESHOLD_CJK = 20;
21367
21370
  var MIN_CONFIDENCE_EMIT = 0.55;
21368
21371
  var MIN_CONFIDENCE_HIGH = 0.7;
21369
21372
  var PROSE_FLOW_MAX_FRACTION = 0.3;
21373
+ function splitWideWhitespaceItems(items) {
21374
+ const out = [];
21375
+ for (const it of items) {
21376
+ const tokens = splitAtWideRuns(it.str, WIDE_WHITESPACE_MIN_RUN);
21377
+ if (tokens.length < 2 || it.str.length < 4 || it.width <= 0) {
21378
+ out.push(it);
21379
+ continue;
21380
+ }
21381
+ const perChar = it.width / it.str.length;
21382
+ for (const t of tokens) {
21383
+ const subX = it.x + t.start * perChar;
21384
+ const subRight = it.x + t.end * perChar;
21385
+ const subWidth = Math.max(subRight - subX, perChar);
21386
+ out.push({
21387
+ ...it,
21388
+ str: t.str,
21389
+ x: subX,
21390
+ width: subWidth,
21391
+ right: subX + subWidth
21392
+ });
21393
+ }
21394
+ }
21395
+ return out;
21396
+ }
21370
21397
  function preFilterAndMerge(items) {
21371
- const alive = items.filter(
21398
+ const filtered = items.filter(
21372
21399
  (it) => it.str.trim().length > 0 && it.width > 0 && it.height > 0
21373
21400
  );
21401
+ if (filtered.length === 0) return [];
21402
+ const alive = splitWideWhitespaceItems(filtered);
21374
21403
  if (alive.length === 0) return [];
21375
21404
  const widths = alive.map((it) => estimateCharWidth(it)).filter((w) => w > 0);
21376
21405
  const medianCharWidth = median(widths) || 1;
@@ -21428,11 +21457,13 @@ function detectColumns(rows) {
21428
21457
  if (allItems.length === 0) return [];
21429
21458
  const medianCharWidth = median(allItems.map((it) => estimateCharWidth(it)).filter((w) => w > 0)) || 1;
21430
21459
  const gapTol = medianCharWidth * COLUMN_GAP_MERGE_FACTOR;
21460
+ const hardBoundaries = voteColumnBoundaries(rows, medianCharWidth);
21431
21461
  const intervals = allItems.map((it) => ({ x0: it.x, x1: it.right })).sort((a, b) => a.x0 - b.x0);
21432
21462
  const merged = [];
21433
21463
  for (const iv of intervals) {
21434
21464
  const last = merged[merged.length - 1];
21435
- if (last && iv.x0 - last.x1 <= gapTol) {
21465
+ const crossesHardBoundary = last !== void 0 && hardBoundaries.some((b) => b >= last.x1 && b <= iv.x0);
21466
+ if (last && iv.x0 - last.x1 <= gapTol && !crossesHardBoundary) {
21436
21467
  last.x1 = Math.max(last.x1, iv.x1);
21437
21468
  } else {
21438
21469
  merged.push({ ...iv });
@@ -21611,6 +21642,67 @@ function estimateCharWidth(item) {
21611
21642
  const chars = item.str.length;
21612
21643
  return chars > 0 ? item.width / chars : 0;
21613
21644
  }
21645
+ function splitAtWideRuns(str, minRun) {
21646
+ const tokens = [];
21647
+ const isWs = (ch) => ch === " " || ch === " " || ch === "\xA0";
21648
+ let i = 0;
21649
+ while (i < str.length) {
21650
+ while (i < str.length && isWs(str[i])) i++;
21651
+ if (i >= str.length) break;
21652
+ const start = i;
21653
+ let end = i;
21654
+ while (i < str.length) {
21655
+ if (isWs(str[i])) {
21656
+ let j = i;
21657
+ while (j < str.length && isWs(str[j])) j++;
21658
+ if (j - i >= minRun) break;
21659
+ i = j;
21660
+ } else {
21661
+ i++;
21662
+ end = i;
21663
+ }
21664
+ }
21665
+ if (end > start) tokens.push({ str: str.slice(start, end), start, end });
21666
+ }
21667
+ return tokens;
21668
+ }
21669
+ function voteColumnBoundaries(rows, medianCharWidth) {
21670
+ if (rows.length < 2 || medianCharWidth <= 0) return [];
21671
+ const candidates = [];
21672
+ rows.forEach((row, rIdx) => {
21673
+ const sorted = [...row.items].sort((a, b) => a.x - b.x);
21674
+ for (let i = 1; i < sorted.length; i++) {
21675
+ const gap = sorted[i].x - sorted[i - 1].right;
21676
+ if (gap > 0) {
21677
+ candidates.push({
21678
+ mid: (sorted[i - 1].right + sorted[i].x) / 2,
21679
+ rowIdx: rIdx
21680
+ });
21681
+ }
21682
+ }
21683
+ });
21684
+ if (candidates.length === 0) return [];
21685
+ candidates.sort((a, b) => a.mid - b.mid);
21686
+ const tolerance = medianCharWidth * COLUMN_BOUNDARY_VOTE_TOLERANCE_FACTOR;
21687
+ const clusters = [];
21688
+ for (const c of candidates) {
21689
+ const last = clusters[clusters.length - 1];
21690
+ if (last && c.mid - last.mids[last.mids.length - 1] <= tolerance) {
21691
+ last.mids.push(c.mid);
21692
+ last.rowSet.add(c.rowIdx);
21693
+ } else {
21694
+ clusters.push({ mids: [c.mid], rowSet: /* @__PURE__ */ new Set([c.rowIdx]) });
21695
+ }
21696
+ }
21697
+ const hard = [];
21698
+ for (const cl of clusters) {
21699
+ if (cl.rowSet.size / rows.length >= COLUMN_SUPPORT_THRESHOLD) {
21700
+ hard.push(median(cl.mids));
21701
+ }
21702
+ }
21703
+ hard.sort((a, b) => a - b);
21704
+ return hard;
21705
+ }
21614
21706
  function median(xs) {
21615
21707
  if (xs.length === 0) return 0;
21616
21708
  const sorted = [...xs].sort((a, b) => a - b);
@@ -21702,7 +21794,7 @@ function assignRowToColumns(row, columns) {
21702
21794
  best = c;
21703
21795
  }
21704
21796
  }
21705
- if (best >= 0 && bestScore > 0.2) buckets[best].push(item);
21797
+ if (best >= 0 && bestScore > MIN_CELL_OVERLAP_SCORE) buckets[best].push(item);
21706
21798
  }
21707
21799
  for (let c = 0; c < columns.length; c++) {
21708
21800
  const bucket = buckets[c].sort((a, b) => a.x - b.x);
@@ -23862,8 +23954,8 @@ import { readFileSync as readFileSync4 } from "fs";
23862
23954
  import { resolve, dirname as dirname3 } from "path";
23863
23955
  import { fileURLToPath } from "url";
23864
23956
  var getVersion = () => {
23865
- if ("1.0.11") {
23866
- return "1.0.11";
23957
+ if ("1.0.12") {
23958
+ return "1.0.12";
23867
23959
  }
23868
23960
  try {
23869
23961
  const __dirname = dirname3(fileURLToPath(import.meta.url));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formatferry",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "FormatFerry CLI — Convert HTML, DOCX, PDF, XLSX, CSV to Markdown",
5
5
  "keywords": [
6
6
  "markdown",