formatferry 1.0.13 → 1.0.14

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 +651 -46
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -19705,9 +19705,12 @@ var InputSanitizer = class {
19705
19705
  "table",
19706
19706
  "thead",
19707
19707
  "tbody",
19708
+ "tfoot",
19708
19709
  "tr",
19709
19710
  "td",
19710
19711
  "th",
19712
+ "figure",
19713
+ "figcaption",
19711
19714
  "a",
19712
19715
  "img",
19713
19716
  "sup",
@@ -21373,7 +21376,9 @@ var _flags = {
21373
21376
  enableHtml: true,
21374
21377
  enablePdfTableDetection: true,
21375
21378
  enableData: true,
21376
- enablePptx: true
21379
+ enablePptx: true,
21380
+ enablePdfEnhanced: true,
21381
+ enablePdfMultiColumn: true
21377
21382
  };
21378
21383
  function getFeatureFlags() {
21379
21384
  return { ..._flags };
@@ -21640,12 +21645,25 @@ function findTableRegions(items, options = {}) {
21640
21645
  if (best) break;
21641
21646
  else continue;
21642
21647
  }
21643
- const confidence = scoreCandidate(coalesced, columns);
21648
+ const rawConfidence = scoreCandidate(coalesced, columns);
21649
+ const bbox = computeBbox(coalesced);
21650
+ const ruleBonus = (() => {
21651
+ const hRules = options.ruleSeparators?.filter((r) => r.orientation === "h") ?? [];
21652
+ if (hRules.length === 0) return 0;
21653
+ const tol = 10;
21654
+ const hasTop = hRules.some(
21655
+ (r) => Math.abs(r.position - bbox.y1) <= tol && r.span[0] <= bbox.x0 + tol && r.span[1] >= bbox.x1 - tol
21656
+ );
21657
+ const hasBottom = hRules.some(
21658
+ (r) => Math.abs(r.position - bbox.y0) <= tol && r.span[0] <= bbox.x0 + tol && r.span[1] >= bbox.x1 - tol
21659
+ );
21660
+ return (hasTop ? 0.075 : 0) + (hasBottom ? 0.075 : 0);
21661
+ })();
21662
+ const confidence = Math.min(1, rawConfidence + ruleBonus);
21644
21663
  if (confidence < MIN_CONFIDENCE_EMIT) {
21645
21664
  if (best) break;
21646
21665
  continue;
21647
21666
  }
21648
- const bbox = computeBbox(coalesced);
21649
21667
  const medianH = median(coalesced.flatMap((r) => r.items.map((it) => it.height))) || 0;
21650
21668
  const mayContinue = options.pageBottomY !== void 0 && medianH > 0 && bbox.y0 - options.pageBottomY < medianH * 2;
21651
21669
  best = {
@@ -21953,6 +21971,142 @@ function looksLikeHeader(row) {
21953
21971
  return boldish / n >= 0.5 || allCaps / n >= 0.5 || trailingColon / n >= 0.5;
21954
21972
  }
21955
21973
 
21974
+ // ../../src/shared/adapters/pdf-struct-tree-adapter.ts
21975
+ function buildMcidMap(items) {
21976
+ const map = /* @__PURE__ */ new Map();
21977
+ let currentId = null;
21978
+ for (const item of items) {
21979
+ const itemType = "type" in item ? item.type : void 0;
21980
+ if (itemType === "beginMarkedContentProps") {
21981
+ const mc = item;
21982
+ if (mc.id != null) {
21983
+ currentId = mc.id;
21984
+ if (!map.has(currentId)) map.set(currentId, "");
21985
+ }
21986
+ } else if (itemType === "endMarkedContent") {
21987
+ currentId = null;
21988
+ } else if ("str" in item && currentId !== null) {
21989
+ const existing = map.get(currentId) ?? "";
21990
+ map.set(currentId, existing + item.str);
21991
+ }
21992
+ }
21993
+ return map;
21994
+ }
21995
+ function countNodes(node, limit) {
21996
+ let n = 1;
21997
+ if ("children" in node && node.children) {
21998
+ for (const child of node.children) {
21999
+ n += countNodes(child, limit);
22000
+ if (n >= limit) return n;
22001
+ }
22002
+ }
22003
+ return n;
22004
+ }
22005
+ function esc(str) {
22006
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
22007
+ }
22008
+ function isContent(node) {
22009
+ return "id" in node && !("role" in node);
22010
+ }
22011
+ function walkChildren(children, mcidMap, depth) {
22012
+ return children.map((c) => walkNode(c, mcidMap, depth)).join("");
22013
+ }
22014
+ function walkNode(node, mcidMap, depth = 0) {
22015
+ if (depth > 50) return "";
22016
+ if (isContent(node)) {
22017
+ const text = mcidMap.get(node.id) ?? "";
22018
+ return esc(text);
22019
+ }
22020
+ const children = node.children ?? [];
22021
+ const inner = walkChildren(children, mcidMap, depth + 1);
22022
+ switch (node.role) {
22023
+ case "H":
22024
+ return `<h1>${inner}</h1>`;
22025
+ case "H1":
22026
+ return `<h1>${inner}</h1>`;
22027
+ case "H2":
22028
+ return `<h2>${inner}</h2>`;
22029
+ case "H3":
22030
+ return `<h3>${inner}</h3>`;
22031
+ case "H4":
22032
+ return `<h4>${inner}</h4>`;
22033
+ case "H5":
22034
+ return `<h5>${inner}</h5>`;
22035
+ case "H6":
22036
+ return `<h6>${inner}</h6>`;
22037
+ case "P":
22038
+ return inner ? `<p>${inner}</p>` : "";
22039
+ case "L":
22040
+ return inner ? `<ul>${inner}</ul>` : "";
22041
+ case "LI":
22042
+ return inner ? `<li>${inner}</li>` : "";
22043
+ case "Lbl":
22044
+ return inner ? `<span>${inner}</span> ` : "";
22045
+ case "LBody":
22046
+ return inner;
22047
+ case "Table":
22048
+ return inner ? `<table>${inner}</table>` : "";
22049
+ case "THead":
22050
+ return inner ? `<thead>${inner}</thead>` : "";
22051
+ case "TBody":
22052
+ return inner ? `<tbody>${inner}</tbody>` : "";
22053
+ case "TFoot":
22054
+ return inner ? `<tfoot>${inner}</tfoot>` : "";
22055
+ case "TR":
22056
+ return inner ? `<tr>${inner}</tr>` : "";
22057
+ case "TH":
22058
+ return `<th>${inner}</th>`;
22059
+ case "TD":
22060
+ return `<td>${inner}</td>`;
22061
+ case "Figure": {
22062
+ const alt = node.alt ?? "";
22063
+ const caption = alt ? `<figcaption>${esc(alt)}</figcaption>` : "";
22064
+ return `<figure>${caption}${inner}</figure>`;
22065
+ }
22066
+ case "BlockQuote":
22067
+ return inner ? `<blockquote>${inner}</blockquote>` : "";
22068
+ case "Code":
22069
+ return inner ? `<code>${inner}</code>` : "";
22070
+ // Inline / passthrough roles
22071
+ case "Span":
22072
+ case "Link":
22073
+ case "Reference":
22074
+ case "Note":
22075
+ case "Annot":
22076
+ case "Strong":
22077
+ case "Em":
22078
+ case "Sub":
22079
+ case "Sup":
22080
+ return inner;
22081
+ // Grouping roles — just pass through children
22082
+ default:
22083
+ return inner;
22084
+ }
22085
+ }
22086
+ async function structTreeToHtml(page) {
22087
+ let structTree;
22088
+ try {
22089
+ structTree = await page.getStructTree();
22090
+ } catch {
22091
+ return null;
22092
+ }
22093
+ if (!structTree || !structTree.children || structTree.children.length === 0) {
22094
+ return null;
22095
+ }
22096
+ if (countNodes(structTree, 5) < 5) return null;
22097
+ let textContent;
22098
+ try {
22099
+ textContent = await page.getTextContent({ includeMarkedContent: true });
22100
+ } catch {
22101
+ return null;
22102
+ }
22103
+ const mcidMap = buildMcidMap(textContent.items);
22104
+ const html = walkNode(structTree, mcidMap);
22105
+ const textOnly = html.replace(/<[^>]+>/g, "").trim();
22106
+ if (!textOnly) return null;
22107
+ return html;
22108
+ }
22109
+
21956
22110
  // ../../src/shared/adapters/pdf-adapter.ts
21957
22111
  var PdfParseError = class extends Error {
21958
22112
  constructor(message, code, originalError) {
@@ -21969,6 +22123,52 @@ var FORM_LABEL_GAP_FACTOR = 3;
21969
22123
  function escapeHtml(str) {
21970
22124
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
21971
22125
  }
22126
+ function computeFontSize(transform) {
22127
+ return Math.sqrt(transform[2] ** 2 + transform[3] ** 2);
22128
+ }
22129
+ function normalizeLigatures(text) {
22130
+ return text.normalize("NFC").replace(/ff/g, "ff").replace(/fi/g, "fi").replace(/fl/g, "fl").replace(/ffi/g, "ffi").replace(/ffl/g, "ffl");
22131
+ }
22132
+ function hasStructTree(page) {
22133
+ return typeof page.getStructTree === "function";
22134
+ }
22135
+ function isSafeUrl(url) {
22136
+ return /^(?:https?|mailto|ftp):/i.test(url);
22137
+ }
22138
+ function detectFontStyle(fontName) {
22139
+ if (!fontName) return null;
22140
+ const bold = /(Bold|Black|Heavy|Semibold|Demi)/i.test(fontName) && !/Blackletter/i.test(fontName);
22141
+ const italic = /(Italic|Oblique|Slanted)/i.test(fontName);
22142
+ if (bold && italic) return "bold-italic";
22143
+ if (bold) return "bold";
22144
+ if (italic) return "italic";
22145
+ return null;
22146
+ }
22147
+ function buildOutlineSet(outline) {
22148
+ const set = /* @__PURE__ */ new Set();
22149
+ if (!outline) return set;
22150
+ function walk(items) {
22151
+ for (const item of items) {
22152
+ if (item.title) {
22153
+ set.add(normalizeLigatures(item.title).replace(/\s+/g, " ").trim().toLowerCase());
22154
+ }
22155
+ if (item.items) walk(item.items);
22156
+ }
22157
+ }
22158
+ walk(outline);
22159
+ return set;
22160
+ }
22161
+ function fuzzyMatchOutline(text, outlineTitles) {
22162
+ if (outlineTitles.size === 0) return false;
22163
+ for (const title of outlineTitles) {
22164
+ if (title.length < 3) continue;
22165
+ if (text.includes(title) || title.includes(text)) {
22166
+ const ratio = Math.min(text.length, title.length) / Math.max(text.length, title.length);
22167
+ if (ratio >= 0.8) return true;
22168
+ }
22169
+ }
22170
+ return false;
22171
+ }
21972
22172
  function detectRotation(transform) {
21973
22173
  const EPS = 1e-3;
21974
22174
  const [sx, shy, shx, sy] = transform;
@@ -21992,6 +22192,7 @@ function textItemToPositioned(item) {
21992
22192
  const descent = height * DESCENDER_BUDGET_FACTOR;
21993
22193
  const y = ty - descent;
21994
22194
  const paddedHeight = height + descent;
22195
+ const fs = computeFontSize(item.transform);
21995
22196
  return {
21996
22197
  str: item.str,
21997
22198
  x: tx,
@@ -22001,7 +22202,8 @@ function textItemToPositioned(item) {
22001
22202
  right: tx + width,
22002
22203
  rotation,
22003
22204
  fontName: item.fontName,
22004
- source: "text"
22205
+ source: "text",
22206
+ fontSize: fs > 0 ? fs : rawHeight > 0 ? rawHeight : void 0
22005
22207
  };
22006
22208
  }
22007
22209
  function widgetToPositionedItems(ann) {
@@ -22101,9 +22303,309 @@ function itemsToLinearText(items) {
22101
22303
  }
22102
22304
  return lines.join("\n").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
22103
22305
  }
22104
- function renderPage(items, pageNum, nextPageNum, enableTableDetection, translate, pageBottomY) {
22306
+ function renderTextBlockEnhanced(items, medianPageFontSize, outlineTitles) {
22307
+ if (items.length === 0) return { htmlBlocks: [], textContent: "" };
22308
+ const sorted = [...items].sort(
22309
+ (a, b) => b.y + b.height - (a.y + a.height) || a.x - b.x
22310
+ );
22311
+ const rows = [];
22312
+ for (const it of sorted) {
22313
+ const last = rows[rows.length - 1];
22314
+ if (last && last.some(
22315
+ (li) => Math.max(
22316
+ 0,
22317
+ Math.min(li.y + li.height, it.y + it.height) - Math.max(li.y, it.y)
22318
+ ) / Math.max(Math.min(li.height, it.height), 1e-6) >= 0.5
22319
+ )) {
22320
+ last.push(it);
22321
+ } else {
22322
+ rows.push([it]);
22323
+ }
22324
+ }
22325
+ const htmlBlocks = [];
22326
+ const textLines = [];
22327
+ const col3Rows = getFeatureFlags().enablePdfMultiColumn ? detectAndReorderColumns(rows) : rows;
22328
+ const finalRows = repairRowHyphenation(col3Rows);
22329
+ let pendingListKind = null;
22330
+ const pendingListHtml = [];
22331
+ const flushPendingList = () => {
22332
+ if (pendingListHtml.length === 0) return;
22333
+ const tag = pendingListKind === "ol" ? "ol" : "ul";
22334
+ htmlBlocks.push(`<${tag}>${pendingListHtml.join("")}</${tag}>`);
22335
+ pendingListKind = null;
22336
+ pendingListHtml.length = 0;
22337
+ };
22338
+ for (const row of finalRows) {
22339
+ row.sort((a, b) => a.x - b.x);
22340
+ const fsSamples = row.map((it) => it.fontSize ?? 0).filter((f) => f > 0);
22341
+ const rowFs = fsSamples.length > 0 ? [...fsSamples].sort((a, b) => a - b)[Math.floor(fsSamples.length / 2)] : medianPageFontSize;
22342
+ const rowWidths = row.map((it) => it.str.length > 0 ? it.width / it.str.length : 0).filter((w) => w > 0);
22343
+ const rowMedianCharWidth = rowWidths.length ? [...rowWidths].sort((a, b) => a - b)[Math.floor(rowWidths.length / 2)] || 1 : 1;
22344
+ const rowLabelGap = rowMedianCharWidth * FORM_LABEL_GAP_FACTOR;
22345
+ let plainText = "";
22346
+ let inlineHtml = "";
22347
+ let hasAnyStyle = false;
22348
+ for (let i = 0; i < row.length; i++) {
22349
+ const it = row[i];
22350
+ let sep = "";
22351
+ if (i > 0) {
22352
+ const prev = row[i - 1];
22353
+ const gap = it.x - prev.right;
22354
+ const prevStr = prev.str.trimEnd();
22355
+ sep = /:$/.test(prevStr) && gap > rowLabelGap ? "\xA0\xA0" : " ";
22356
+ }
22357
+ plainText += sep + it.str;
22358
+ const escaped = escapeHtml(normalizeLigatures(it.str));
22359
+ const style = detectFontStyle(it.fontName);
22360
+ if (style) hasAnyStyle = true;
22361
+ let styledPart = style === "bold-italic" ? `<strong><em>${escaped}</em></strong>` : style === "bold" ? `<strong>${escaped}</strong>` : style === "italic" ? `<em>${escaped}</em>` : escaped;
22362
+ if (it.linkUrl && isSafeUrl(it.linkUrl)) {
22363
+ styledPart = `<a href="${escapeHtml(it.linkUrl)}">${styledPart}</a>`;
22364
+ }
22365
+ inlineHtml += (i > 0 ? escapeHtml(sep) : "") + styledPart;
22366
+ }
22367
+ plainText = plainText.replace(/[ \t]+/g, " ").trim();
22368
+ if (!plainText) continue;
22369
+ const normalizedPlain = normalizeLigatures(plainText);
22370
+ const normalized = normalizedPlain.replace(/\s+/g, " ").trim().toLowerCase();
22371
+ if (getFeatureFlags().enablePdfEnhanced) {
22372
+ const listMatch = detectListMarker(normalizedPlain);
22373
+ if (listMatch) {
22374
+ if (pendingListKind !== null && pendingListKind !== listMatch.kind) {
22375
+ flushPendingList();
22376
+ }
22377
+ pendingListKind = listMatch.kind;
22378
+ const escapedBody = escapeHtml(normalizeLigatures(listMatch.bodyText));
22379
+ pendingListHtml.push(`<li>${escapedBody}</li>`);
22380
+ textLines.push(normalizedPlain);
22381
+ continue;
22382
+ }
22383
+ }
22384
+ flushPendingList();
22385
+ const ratio = medianPageFontSize > 0 ? rowFs / medianPageFontSize : 1;
22386
+ let headingLevel = 0;
22387
+ if (ratio >= 1.5) headingLevel = 1;
22388
+ else if (ratio >= 1.25) headingLevel = 2;
22389
+ else if (ratio >= 1.1) headingLevel = 3;
22390
+ if (headingLevel === 0 && outlineTitles.size > 0) {
22391
+ if (outlineTitles.has(normalized) || fuzzyMatchOutline(normalized, outlineTitles)) {
22392
+ headingLevel = 2;
22393
+ }
22394
+ }
22395
+ textLines.push(normalizedPlain);
22396
+ if (headingLevel > 0) {
22397
+ htmlBlocks.push(`<h${headingLevel}>${inlineHtml}</h${headingLevel}>`);
22398
+ } else {
22399
+ const hasWideGap = /( {3,}|\u00A0\u00A0|\t)/.test(normalizedPlain);
22400
+ const isShort = normalizedPlain.length < 100 && !/[.,:;!?]$/.test(normalizedPlain) && !/\n/.test(normalizedPlain) && !hasWideGap;
22401
+ if (isShort && !hasAnyStyle) {
22402
+ htmlBlocks.push(`<p><strong>${inlineHtml}</strong></p>`);
22403
+ } else {
22404
+ htmlBlocks.push(`<p>${inlineHtml}</p>`);
22405
+ }
22406
+ }
22407
+ }
22408
+ flushPendingList();
22409
+ return {
22410
+ htmlBlocks,
22411
+ textContent: textLines.join("\n").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim()
22412
+ };
22413
+ }
22414
+ function repairRowHyphenation(rows) {
22415
+ if (rows.length < 2) return rows;
22416
+ const result = [];
22417
+ let i = 0;
22418
+ while (i < rows.length) {
22419
+ if (i + 1 < rows.length) {
22420
+ const row = rows[i];
22421
+ const nextRow = rows[i + 1];
22422
+ const lastItem = [...row].sort((a, b) => b.right - a.right)[0];
22423
+ const firstNext = [...nextRow].sort((a, b) => a.x - b.x)[0];
22424
+ if (lastItem && firstNext && /-$/.test(lastItem.str) && new RegExp("^\\p{Ll}", "u").test(firstNext.str)) {
22425
+ const fixed = { ...lastItem, str: lastItem.str.slice(0, -1) };
22426
+ result.push([...row.map((it) => it === lastItem ? fixed : it), ...nextRow]);
22427
+ i += 2;
22428
+ continue;
22429
+ }
22430
+ }
22431
+ result.push(rows[i]);
22432
+ i++;
22433
+ }
22434
+ return result;
22435
+ }
22436
+ function detectListMarker(plainText) {
22437
+ const bulletMatch = /^[•◦◆▪▸–\-*·]\s+(.+)/u.exec(plainText);
22438
+ if (bulletMatch) return { kind: "ul", bodyText: bulletMatch[1] };
22439
+ const numberedMatch = /^\d+[.)]\s+(.+)/.exec(plainText);
22440
+ if (numberedMatch) return { kind: "ol", bodyText: numberedMatch[1] };
22441
+ const letteredMatch = /^[a-z][.)]\s+(.+)/.exec(plainText);
22442
+ if (letteredMatch) return { kind: "ol", bodyText: letteredMatch[1] };
22443
+ return null;
22444
+ }
22445
+ function detectAndReorderColumns(rows) {
22446
+ if (rows.length < 4) return rows;
22447
+ const allItems = rows.flat();
22448
+ const itemWidths = allItems.filter((it) => it.width > 0 && it.str.trim().length > 0).map((it) => it.width);
22449
+ if (itemWidths.length === 0) return rows;
22450
+ const sortedWidths = [...itemWidths].sort((a, b) => a - b);
22451
+ const medianWidth = sortedWidths[Math.floor(sortedWidths.length / 2)];
22452
+ const gutterThreshold = medianWidth * 3;
22453
+ const xMin = allItems.reduce((m, it) => Math.min(m, it.x), Infinity);
22454
+ const xMax = allItems.reduce((m, it) => Math.max(m, it.right), -Infinity);
22455
+ const midLeft = xMin + (xMax - xMin) * 0.25;
22456
+ const midRight = xMin + (xMax - xMin) * 0.75;
22457
+ const gutterMidpoints = [];
22458
+ for (const row of rows) {
22459
+ if (row.length < 2) continue;
22460
+ const rowSorted = [...row].sort((a, b) => a.x - b.x);
22461
+ for (let i = 1; i < rowSorted.length; i++) {
22462
+ const prev = rowSorted[i - 1];
22463
+ const curr = rowSorted[i];
22464
+ const gap = curr.x - prev.right;
22465
+ if (gap < gutterThreshold) continue;
22466
+ const midX = (prev.right + curr.x) / 2;
22467
+ if (midX >= midLeft && midX <= midRight) {
22468
+ gutterMidpoints.push(midX);
22469
+ break;
22470
+ }
22471
+ }
22472
+ }
22473
+ const multiItemRowCount = rows.filter((r) => r.length >= 2).length;
22474
+ const minVotes = Math.max(2, Math.ceil(multiItemRowCount * 0.35));
22475
+ if (gutterMidpoints.length < minVotes) return rows;
22476
+ const sortedGutters = [...gutterMidpoints].sort((a, b) => a - b);
22477
+ const gutterX = sortedGutters[Math.floor(sortedGutters.length / 2)];
22478
+ const leftRows = [];
22479
+ const rightRows = [];
22480
+ for (const row of rows) {
22481
+ const leftItems = row.filter((it) => it.x + it.width / 2 <= gutterX);
22482
+ const rightItems = row.filter((it) => it.x + it.width / 2 > gutterX);
22483
+ if (leftItems.length > 0) leftRows.push(leftItems);
22484
+ if (rightItems.length > 0) rightRows.push(rightItems);
22485
+ }
22486
+ if (leftRows.length < 2 || rightRows.length < 2) return rows;
22487
+ const sortTopDown = (arr) => [...arr].sort((a, b) => {
22488
+ const aTop = a.reduce((m, it) => Math.max(m, it.y + it.height), -Infinity);
22489
+ const bTop = b.reduce((m, it) => Math.max(m, it.y + it.height), -Infinity);
22490
+ return bTop - aTop;
22491
+ });
22492
+ return [...sortTopDown(leftRows), ...sortTopDown(rightRows)];
22493
+ }
22494
+ function buildHeaderFooterSuppressSet(pageDataArray) {
22495
+ const totalPages = pageDataArray.length;
22496
+ if (totalPages < 3) return /* @__PURE__ */ new Set();
22497
+ const HEADER_THRESHOLD = 0.92;
22498
+ const FOOTER_THRESHOLD = 0.08;
22499
+ const Y_TOLERANCE = 5;
22500
+ const MIN_PAGE_FRACTION = 0.4;
22501
+ const occurrences = /* @__PURE__ */ new Map();
22502
+ for (const { pageNum, items, viewportHeight } of pageDataArray) {
22503
+ for (const it of items) {
22504
+ const itemTop = it.y + it.height;
22505
+ const inHeader = itemTop > viewportHeight * HEADER_THRESHOLD;
22506
+ const inFooter = it.y < viewportHeight * FOOTER_THRESHOLD;
22507
+ if (!inHeader && !inFooter) continue;
22508
+ const normalized = it.str.replace(/\s+/g, " ").trim().toLowerCase();
22509
+ if (normalized.length === 0) continue;
22510
+ const yBucket = Math.round(it.y / Y_TOLERANCE) * Y_TOLERANCE;
22511
+ const key = `${normalized}@${yBucket}`;
22512
+ if (!occurrences.has(key)) occurrences.set(key, /* @__PURE__ */ new Set());
22513
+ occurrences.get(key).add(pageNum);
22514
+ }
22515
+ }
22516
+ const suppressSet = /* @__PURE__ */ new Set();
22517
+ for (const [key, pageSet] of occurrences) {
22518
+ if (pageSet.size / totalPages >= MIN_PAGE_FRACTION) {
22519
+ suppressSet.add(key);
22520
+ }
22521
+ }
22522
+ return suppressSet;
22523
+ }
22524
+ function isHeaderFooterItem(it, suppressSet, Y_TOLERANCE = 5) {
22525
+ if (suppressSet.size === 0) return false;
22526
+ const normalized = it.str.replace(/\s+/g, " ").trim().toLowerCase();
22527
+ if (normalized.length === 0) return false;
22528
+ const yBucket = Math.round(it.y / Y_TOLERANCE) * Y_TOLERANCE;
22529
+ return suppressSet.has(`${normalized}@${yBucket}`);
22530
+ }
22531
+ var PDF_OP = { moveTo: 13, lineTo: 14, closePath: 18, rectangle: 92 };
22532
+ function extractVectorLines(fnArray, argsArray, pageWidth, pageHeight) {
22533
+ const MIN_H_SPAN = pageWidth * 0.3;
22534
+ const MIN_V_SPAN = pageHeight * 0.3;
22535
+ const lines = [];
22536
+ let curX = 0;
22537
+ let curY = 0;
22538
+ let pathStartX = 0;
22539
+ let pathStartY = 0;
22540
+ const pushIfSignificant = (x0, y0, x1, y1) => {
22541
+ const dx = Math.abs(x1 - x0);
22542
+ const dy = Math.abs(y1 - y0);
22543
+ if (dy < 2 && dx >= MIN_H_SPAN) {
22544
+ lines.push({
22545
+ orientation: "h",
22546
+ position: (y0 + y1) / 2,
22547
+ span: [Math.min(x0, x1), Math.max(x0, x1)]
22548
+ });
22549
+ } else if (dx < 2 && dy >= MIN_V_SPAN) {
22550
+ lines.push({
22551
+ orientation: "v",
22552
+ position: (x0 + x1) / 2,
22553
+ span: [Math.min(y0, y1), Math.max(y0, y1)]
22554
+ });
22555
+ }
22556
+ };
22557
+ for (let i = 0; i < fnArray.length; i++) {
22558
+ const op = fnArray[i];
22559
+ const args = argsArray[i] ?? [];
22560
+ if (op === PDF_OP.moveTo) {
22561
+ curX = args[0] ?? 0;
22562
+ curY = args[1] ?? 0;
22563
+ pathStartX = curX;
22564
+ pathStartY = curY;
22565
+ } else if (op === PDF_OP.lineTo) {
22566
+ const nx = args[0] ?? 0;
22567
+ const ny = args[1] ?? 0;
22568
+ pushIfSignificant(curX, curY, nx, ny);
22569
+ curX = nx;
22570
+ curY = ny;
22571
+ } else if (op === PDF_OP.rectangle) {
22572
+ const rx = args[0] ?? 0;
22573
+ const ry = args[1] ?? 0;
22574
+ const rw = args[2] ?? 0;
22575
+ const rh = args[3] ?? 0;
22576
+ pushIfSignificant(rx, ry, rx + rw, ry);
22577
+ pushIfSignificant(rx, ry + rh, rx + rw, ry + rh);
22578
+ pushIfSignificant(rx, ry, rx, ry + rh);
22579
+ pushIfSignificant(rx + rw, ry, rx + rw, ry + rh);
22580
+ } else if (op === PDF_OP.closePath) {
22581
+ pushIfSignificant(curX, curY, pathStartX, pathStartY);
22582
+ curX = pathStartX;
22583
+ curY = pathStartY;
22584
+ }
22585
+ }
22586
+ return lines;
22587
+ }
22588
+ function applyLinkAnnotations(items, annotations) {
22589
+ const linkAnns = annotations.filter(
22590
+ (a) => a.subtype === "Link" && a.url && a.rect && a.rect.length === 4
22591
+ );
22592
+ if (linkAnns.length === 0) return;
22593
+ for (const it of items) {
22594
+ const itArea = Math.max(it.width * it.height, 1e-6);
22595
+ for (const ann of linkAnns) {
22596
+ const [llx, lly, urx, ury] = ann.rect;
22597
+ const ox = Math.max(0, Math.min(it.right, urx) - Math.max(it.x, llx));
22598
+ const oy = Math.max(0, Math.min(it.y + it.height, ury) - Math.max(it.y, lly));
22599
+ if (ox * oy / itArea >= 0.2) {
22600
+ it.linkUrl = ann.url;
22601
+ break;
22602
+ }
22603
+ }
22604
+ }
22605
+ }
22606
+ function renderPage(items, pageNum, nextPageNum, enableTableDetection, translate, pageBottomY, enhancedOpts, ruleSeparators) {
22105
22607
  const warnings = [];
22106
- const candidates = enableTableDetection ? findTableRegions(items, { pageBottomY }) : [];
22608
+ const candidates = enableTableDetection ? findTableRegions(items, { pageBottomY, ruleSeparators }) : [];
22107
22609
  const htmlBlocks = [];
22108
22610
  const candsByY = [...candidates].sort((a, b) => b.bbox.y1 - a.bbox.y1);
22109
22611
  const itemOverlapsCandidate = (it, c) => {
@@ -22167,36 +22669,46 @@ function renderPage(items, pageNum, nextPageNum, enableTableDetection, translate
22167
22669
  }
22168
22670
  linearTextParts.push(tableToText(block.cand));
22169
22671
  } else {
22170
- const text = itemsToLinearText(block.items);
22171
- if (!text) continue;
22172
- linearTextParts.push(text);
22173
- const medH = (() => {
22174
- const hs = block.items.map((i) => i.height).filter((h) => h > 0);
22175
- if (hs.length === 0) return 10;
22176
- return hs.sort((a, b) => a - b)[Math.floor(hs.length / 2)] || 10;
22177
- })();
22178
- const topY = block.items.reduce(
22179
- (m, it) => Math.max(m, it.y + it.height),
22180
- -Infinity
22181
- );
22182
- const topRowItems = block.items.filter(
22183
- (it) => topY - (it.y + it.height) <= medH * 0.5
22184
- );
22185
- const topRowLooksTabular = topRowItems.length >= 4 && topRowItems.some((a) => topRowItems.some((b) => b.x - a.right > medH * 2));
22186
- const paragraphs = text.split(/\n{2,}/);
22187
- let paraIdx = 0;
22188
- for (const para of paragraphs) {
22189
- const trimmed = para.trim();
22190
- paraIdx++;
22191
- if (!trimmed) continue;
22192
- const hasWideGap = /( {3,}|\u00A0\u00A0|\t)/.test(trimmed);
22193
- const isFirstPara = paraIdx === 1;
22194
- const looksTabular = hasWideGap || isFirstPara && topRowLooksTabular;
22195
- const short = trimmed.length < 100 && !/[.,:;!?]$/.test(trimmed) && !/\n/.test(trimmed) && !looksTabular;
22196
- if (short) {
22197
- htmlBlocks.push(`<p><strong>${escapeHtml(trimmed)}</strong></p>`);
22198
- } else {
22199
- htmlBlocks.push(`<p>${escapeHtml(trimmed)}</p>`);
22672
+ if (enhancedOpts) {
22673
+ const { htmlBlocks: eBlocks, textContent: eText } = renderTextBlockEnhanced(
22674
+ block.items,
22675
+ enhancedOpts.medianFontSize,
22676
+ enhancedOpts.outlineTitles
22677
+ );
22678
+ if (eText) linearTextParts.push(eText);
22679
+ for (const b of eBlocks) htmlBlocks.push(b);
22680
+ } else {
22681
+ const text = itemsToLinearText(block.items);
22682
+ if (!text) continue;
22683
+ linearTextParts.push(text);
22684
+ const medH = (() => {
22685
+ const hs = block.items.map((i) => i.height).filter((h) => h > 0);
22686
+ if (hs.length === 0) return 10;
22687
+ return hs.sort((a, b) => a - b)[Math.floor(hs.length / 2)] || 10;
22688
+ })();
22689
+ const topY = block.items.reduce(
22690
+ (m, it) => Math.max(m, it.y + it.height),
22691
+ -Infinity
22692
+ );
22693
+ const topRowItems = block.items.filter(
22694
+ (it) => topY - (it.y + it.height) <= medH * 0.5
22695
+ );
22696
+ const topRowLooksTabular = topRowItems.length >= 4 && topRowItems.some((a) => topRowItems.some((b) => b.x - a.right > medH * 2));
22697
+ const paragraphs = text.split(/\n{2,}/);
22698
+ let paraIdx = 0;
22699
+ for (const para of paragraphs) {
22700
+ const trimmed = para.trim();
22701
+ paraIdx++;
22702
+ if (!trimmed) continue;
22703
+ const hasWideGap = /( {3,}|\u00A0\u00A0|\t)/.test(trimmed);
22704
+ const isFirstPara = paraIdx === 1;
22705
+ const looksTabular = hasWideGap || isFirstPara && topRowLooksTabular;
22706
+ const short = trimmed.length < 100 && !/[.,:;!?]$/.test(trimmed) && !/\n/.test(trimmed) && !looksTabular;
22707
+ if (short) {
22708
+ htmlBlocks.push(`<p><strong>${escapeHtml(trimmed)}</strong></p>`);
22709
+ } else {
22710
+ htmlBlocks.push(`<p>${escapeHtml(trimmed)}</p>`);
22711
+ }
22200
22712
  }
22201
22713
  }
22202
22714
  }
@@ -22292,6 +22804,43 @@ var PdfAdapter = class {
22292
22804
  }
22293
22805
  } catch {
22294
22806
  }
22807
+ let isTaggedPdf = false;
22808
+ let outlineTitles = /* @__PURE__ */ new Set();
22809
+ if (getFeatureFlags().enablePdfEnhanced) {
22810
+ try {
22811
+ const markInfo = await pdf.getMarkInfo();
22812
+ isTaggedPdf = markInfo?.Marked === true;
22813
+ } catch {
22814
+ }
22815
+ try {
22816
+ const outline = await pdf.getOutline();
22817
+ outlineTitles = buildOutlineSet(outline);
22818
+ } catch {
22819
+ }
22820
+ }
22821
+ let hfSuppressSet = /* @__PURE__ */ new Set();
22822
+ if (getFeatureFlags().enablePdfEnhanced && numPages >= 3 && numPages <= 200) {
22823
+ const hfCandidates = [];
22824
+ for (let pn = 1; pn <= numPages; pn++) {
22825
+ try {
22826
+ const hfPage = await pdf.getPage(pn);
22827
+ const hfViewport = hfPage.getViewport({ scale: 1 });
22828
+ const hfText = await hfPage.getTextContent();
22829
+ const hfAnns = await hfPage.getAnnotations().catch(() => []);
22830
+ const hfPositioned = [];
22831
+ for (const item of hfText.items) {
22832
+ if (!("str" in item) || typeof item.str !== "string") continue;
22833
+ const p = textItemToPositioned(item);
22834
+ if (p) hfPositioned.push(p);
22835
+ }
22836
+ const hfWidgets = hfAnns.flatMap(widgetToPositionedItems);
22837
+ const hfItems = hfWidgets.length > 0 ? [...dedupAgainstWidgets(hfPositioned, hfWidgets), ...hfWidgets] : hfPositioned;
22838
+ hfCandidates.push({ pageNum: pn, items: hfItems, viewportHeight: hfViewport.height });
22839
+ } catch {
22840
+ }
22841
+ }
22842
+ hfSuppressSet = buildHeaderFooterSuppressSet(hfCandidates);
22843
+ }
22295
22844
  for (let pageNum = 1; pageNum <= numPages; pageNum++) {
22296
22845
  onProgress?.(pageNum / numPages);
22297
22846
  const page = await pdf.getPage(pageNum);
@@ -22314,14 +22863,75 @@ var PdfAdapter = class {
22314
22863
  }
22315
22864
  const widgetItems = annotations.flatMap(widgetToPositionedItems);
22316
22865
  const items = widgetItems.length > 0 ? [...dedupAgainstWidgets(positionedText, widgetItems), ...widgetItems] : positionedText;
22866
+ if (getFeatureFlags().enablePdfEnhanced) {
22867
+ applyLinkAnnotations(items, annotations);
22868
+ }
22869
+ const renderItems = getFeatureFlags().enablePdfEnhanced && hfSuppressSet.size > 0 ? items.filter((it) => !isHeaderFooterItem(it, hfSuppressSet)) : items;
22870
+ if (getFeatureFlags().enablePdfEnhanced && isTaggedPdf) {
22871
+ let structHtml = null;
22872
+ try {
22873
+ if (hasStructTree(page)) {
22874
+ structHtml = await structTreeToHtml(page);
22875
+ }
22876
+ } catch {
22877
+ }
22878
+ if (structHtml !== null) {
22879
+ const structTextLen = structHtml.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim().length;
22880
+ const geoTextLen = renderItems.map((it) => it.str).join(" ").length;
22881
+ if (geoTextLen > 0 && structTextLen / geoTextLen < 0.7) {
22882
+ structHtml = null;
22883
+ }
22884
+ }
22885
+ if (structHtml !== null) {
22886
+ const plainText = structHtml.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
22887
+ totalTextLength += plainText.length;
22888
+ let hasImages2 = false;
22889
+ try {
22890
+ const opList = await page.getOperatorList();
22891
+ hasImages2 = opList.fnArray.some((op) => op === 85 || op === 86);
22892
+ } catch {
22893
+ }
22894
+ pages.push({
22895
+ pageNumber: pageNum,
22896
+ textContent: plainText,
22897
+ hasImages: hasImages2,
22898
+ viewportWidth: viewport.width,
22899
+ htmlBlocks: [structHtml],
22900
+ tablesDetected: 0
22901
+ });
22902
+ continue;
22903
+ }
22904
+ }
22905
+ let enhancedOpts;
22906
+ if (getFeatureFlags().enablePdfEnhanced) {
22907
+ const fsSamples = positionedText.map((it) => it.fontSize ?? 0).filter((f) => f > 0);
22908
+ const medianFontSize = fsSamples.length > 0 ? [...fsSamples].sort((a, b) => a - b)[Math.floor(fsSamples.length / 2)] : 12;
22909
+ enhancedOpts = { medianFontSize, outlineTitles };
22910
+ }
22317
22911
  const pageBottomY = Array.isArray(page.view) && page.view.length >= 2 ? page.view[1] : 0;
22912
+ let pageOpList;
22913
+ let ruleSeparators;
22914
+ try {
22915
+ pageOpList = await page.getOperatorList();
22916
+ if (getFeatureFlags().enablePdfEnhanced && tableDetectionEnabled) {
22917
+ ruleSeparators = extractVectorLines(
22918
+ pageOpList.fnArray,
22919
+ pageOpList.argsArray ?? [],
22920
+ viewport.width,
22921
+ viewport.height
22922
+ );
22923
+ }
22924
+ } catch {
22925
+ }
22318
22926
  const rendered = renderPage(
22319
- items,
22927
+ renderItems,
22320
22928
  pageNum,
22321
22929
  pageNum < numPages ? pageNum + 1 : void 0,
22322
22930
  tableDetectionEnabled,
22323
22931
  translate,
22324
- pageBottomY
22932
+ pageBottomY,
22933
+ enhancedOpts,
22934
+ ruleSeparators
22325
22935
  );
22326
22936
  for (const w of rendered.warnings) warnings.push(w);
22327
22937
  if (!hasMultiColumnWarning && !rendered.anyTableDetected && xPositions.length > 10) {
@@ -22345,12 +22955,7 @@ var PdfAdapter = class {
22345
22955
  }
22346
22956
  }
22347
22957
  totalTextLength += rendered.textContent.length;
22348
- let hasImages = false;
22349
- try {
22350
- const operatorList = await page.getOperatorList();
22351
- hasImages = operatorList.fnArray.some((op) => op === 85 || op === 86);
22352
- } catch {
22353
- }
22958
+ const hasImages = pageOpList?.fnArray.some((op) => op === 85 || op === 86) ?? false;
22354
22959
  pages.push({
22355
22960
  pageNumber: pageNum,
22356
22961
  textContent: rendered.textContent,
@@ -24689,8 +25294,8 @@ import { readFileSync as readFileSync4 } from "fs";
24689
25294
  import { resolve, dirname as dirname3 } from "path";
24690
25295
  import { fileURLToPath } from "url";
24691
25296
  var getVersion = () => {
24692
- if ("1.0.13") {
24693
- return "1.0.13";
25297
+ if ("1.0.14") {
25298
+ return "1.0.14";
24694
25299
  }
24695
25300
  try {
24696
25301
  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.13",
3
+ "version": "1.0.14",
4
4
  "description": "FormatFerry CLI — Convert HTML, DOCX, PDF, XLSX, CSV, JSON, XML, PPTX to Markdown",
5
5
  "keywords": [
6
6
  "markdown",