formatferry 1.0.21 → 1.0.23

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 +90 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -19720,7 +19720,7 @@ var InputSanitizer = class {
19720
19720
  ],
19721
19721
  // Include data-page-marker for PDF page break markers (Req 3.5)
19722
19722
  // Include type and checked for task list checkbox support
19723
- ALLOWED_ATTR: ["href", "src", "alt", "title", "style", "class", "data-page-marker", "type", "checked", "data-math"]
19723
+ ALLOWED_ATTR: ["href", "src", "alt", "title", "style", "class", "data-page-marker", "data-docx-table-kind", "type", "checked", "data-math"]
19724
19724
  };
19725
19725
  switch (this.config.level) {
19726
19726
  case "strict":
@@ -20729,6 +20729,22 @@ ${plainText}
20729
20729
  });
20730
20730
  return lines.join("\n");
20731
20731
  }
20732
+ flattenTables(html) {
20733
+ if (typeof DOMParser === "undefined") return html;
20734
+ const parser = new DOMParser();
20735
+ const doc = parser.parseFromString(`<div>${html}</div>`, "text/html");
20736
+ Array.from(doc.querySelectorAll("table")).forEach((table) => {
20737
+ const replacement = doc.createElement("div");
20738
+ const lines = this.tableToPlainText(table).split("\n").filter(Boolean);
20739
+ lines.forEach((line) => {
20740
+ const paragraph = doc.createElement("p");
20741
+ paragraph.textContent = line;
20742
+ replacement.appendChild(paragraph);
20743
+ });
20744
+ table.replaceWith(replacement);
20745
+ });
20746
+ return doc.body.firstElementChild?.innerHTML ?? html;
20747
+ }
20732
20748
  addGoogleDocsRules(service) {
20733
20749
  service.addRule("googleDocsBold", {
20734
20750
  filter: (node) => {
@@ -20860,10 +20876,7 @@ ${plainText}
20860
20876
  let headerProcessed = false;
20861
20877
  rows.forEach((row, index) => {
20862
20878
  const cells = Array.from(row.querySelectorAll("th, td"));
20863
- const cellContents = cells.map((cell) => {
20864
- const cellContent = cell.textContent || "";
20865
- return cellContent.trim().replace(/\|/g, "\\|");
20866
- });
20879
+ const cellContents = cells.map((cell) => this.escapeTableCell(this.renderTableCell(cell)));
20867
20880
  if (cellContents.length > 0) {
20868
20881
  markdown += "| " + cellContents.join(" | ") + " |\n";
20869
20882
  if (!headerProcessed && (row.querySelector("th") || index === 0)) {
@@ -20877,6 +20890,73 @@ ${plainText}
20877
20890
  }
20878
20891
  });
20879
20892
  }
20893
+ renderTableCell(cell) {
20894
+ const blocks = [];
20895
+ let inlineBuffer = "";
20896
+ cell.childNodes.forEach((child) => {
20897
+ if (child.nodeType === Node.ELEMENT_NODE && this.isTableCellBlock(child)) {
20898
+ if (inlineBuffer.trim()) {
20899
+ blocks.push(inlineBuffer.trim());
20900
+ inlineBuffer = "";
20901
+ }
20902
+ blocks.push(this.renderTableBlock(child));
20903
+ return;
20904
+ }
20905
+ inlineBuffer += this.renderInlineNode(child);
20906
+ });
20907
+ if (inlineBuffer.trim()) blocks.push(inlineBuffer.trim());
20908
+ if (blocks.length === 0) return (cell.textContent || "").trim();
20909
+ return blocks.filter(Boolean).join("<br>");
20910
+ }
20911
+ isTableCellBlock(element) {
20912
+ return ["P", "DIV", "UL", "OL", "H1", "H2", "H3", "H4", "H5", "H6", "BLOCKQUOTE", "PRE"].includes(element.nodeName);
20913
+ }
20914
+ renderTableBlock(element) {
20915
+ if (element.nodeName === "UL" || element.nodeName === "OL") {
20916
+ return Array.from(element.children).filter((child) => child.nodeName === "LI").map((child) => `- ${this.renderInlineNode(child).trim()}`).join("<br>");
20917
+ }
20918
+ return this.renderInlineNode(element).trim();
20919
+ }
20920
+ renderInlineNode(node) {
20921
+ if (node.nodeType === Node.TEXT_NODE) return node.textContent || "";
20922
+ if (node.nodeType !== Node.ELEMENT_NODE) return "";
20923
+ const element = node;
20924
+ const content = Array.from(element.childNodes).map((child) => this.renderInlineNode(child)).join("");
20925
+ const trimmed = content.trim();
20926
+ switch (element.nodeName) {
20927
+ case "BR":
20928
+ return "<br>";
20929
+ case "STRONG":
20930
+ case "B":
20931
+ return trimmed ? `**${trimmed}**` : content;
20932
+ case "EM":
20933
+ case "I":
20934
+ return trimmed ? `_${trimmed}_` : content;
20935
+ case "CODE":
20936
+ return trimmed ? this.renderCodeSpan(trimmed) : content;
20937
+ case "A": {
20938
+ const href = element.getAttribute("href") || "";
20939
+ return href && trimmed ? `[${this.escapeMarkdownLinkText(trimmed)}](<${this.escapeMarkdownLinkHref(href)}>)` : content;
20940
+ }
20941
+ default:
20942
+ return content;
20943
+ }
20944
+ }
20945
+ renderCodeSpan(content) {
20946
+ if (!content.includes("`")) return `\`${content}\``;
20947
+ const runs = content.match(/`+/g) || ["`"];
20948
+ const delimiter = "`".repeat(Math.max(...runs.map((run) => run.length)) + 1);
20949
+ return `${delimiter} ${content} ${delimiter}`;
20950
+ }
20951
+ escapeMarkdownLinkText(text) {
20952
+ return text.replace(/\\/g, "\\\\").replace(/\[/g, "\\[").replace(/\]/g, "\\]");
20953
+ }
20954
+ escapeMarkdownLinkHref(href) {
20955
+ return href.replace(/</g, "%3C").replace(/>/g, "%3E");
20956
+ }
20957
+ escapeTableCell(content) {
20958
+ return content.replace(/\|/g, "\\|").replace(/\s*<br>\s*/g, "<br>").trim();
20959
+ }
20880
20960
  addListRules(service) {
20881
20961
  service.addRule("enhancedList", {
20882
20962
  filter: ["ul", "ol"],
@@ -21025,7 +21105,8 @@ ${plainText}
21025
21105
  service = this.githubService;
21026
21106
  break;
21027
21107
  }
21028
- let markdown = service.turndown(sanitizedHtml);
21108
+ const htmlForConversion = options.preserveTables ? sanitizedHtml : this.flattenTables(sanitizedHtml);
21109
+ let markdown = service.turndown(htmlForConversion);
21029
21110
  markdown = this.applyFormatPostProcessing(markdown, options.format);
21030
21111
  const endTime = performance.now();
21031
21112
  const metadata = {
@@ -23089,7 +23170,7 @@ var PdfAdapter = class {
23089
23170
  const hasMinimalTextWithImages = totalTextLength < MIN_TEXT_CHARS_THRESHOLD && hasAnyImages;
23090
23171
  if (hasNoText || hasMinimalTextWithImages) {
23091
23172
  throw new PdfParseError(
23092
- "This PDF contains only images. Text extraction requires a text layer. OCR is not supported.",
23173
+ "PDF contains only images \u2014 no text layer available.",
23093
23174
  "IMAGE_ONLY"
23094
23175
  );
23095
23176
  }
@@ -25419,8 +25500,8 @@ import { readFileSync as readFileSync4 } from "fs";
25419
25500
  import { resolve, dirname as dirname3 } from "path";
25420
25501
  import { fileURLToPath } from "url";
25421
25502
  var getVersion = () => {
25422
- if ("1.0.21") {
25423
- return "1.0.21";
25503
+ if ("1.0.23") {
25504
+ return "1.0.23";
25424
25505
  }
25425
25506
  try {
25426
25507
  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.21",
3
+ "version": "1.0.23",
4
4
  "description": "FormatFerry CLI — Convert HTML, DOCX, PDF, XLSX, CSV, JSON, XML, PPTX to Markdown",
5
5
  "keywords": [
6
6
  "markdown",