formatferry 1.0.38 → 1.0.40

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 +137 -19
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -58942,6 +58942,12 @@ h${level}. ${trimmed}
58942
58942
  }
58943
58943
 
58944
58944
  // ../../src/shared/conversion-engine.ts
58945
+ var TableMarkdownLimitError = class extends Error {
58946
+ constructor() {
58947
+ super("Table Markdown exceeds the conversion limit");
58948
+ this.name = "TableMarkdownLimitError";
58949
+ }
58950
+ };
58945
58951
  var ConversionEngine = class {
58946
58952
  githubService;
58947
58953
  commonmarkService;
@@ -58953,6 +58959,10 @@ var ConversionEngine = class {
58953
58959
  confluenceService;
58954
58960
  dom;
58955
58961
  domSanitizer;
58962
+ /** Scoped to one synchronous Turndown pass; cloned cells do not change while table rules read them. */
58963
+ tableCellCache;
58964
+ /** Table Markdown characters this conversion may still emit (maxTableMarkdownChars). */
58965
+ tableMarkdownBudget = Number.POSITIVE_INFINITY;
58956
58966
  constructor(options = {}) {
58957
58967
  this.dom = options.dom;
58958
58968
  this.initializeServices();
@@ -58967,7 +58977,7 @@ var ConversionEngine = class {
58967
58977
  this.slackService,
58968
58978
  this.confluenceService
58969
58979
  ]) {
58970
- this.parseWithInjectedDom(service, this.dom);
58980
+ this.parseWithInjectedDom(service, this.dom, service === this.githubService);
58971
58981
  }
58972
58982
  }
58973
58983
  }
@@ -58978,7 +58988,7 @@ var ConversionEngine = class {
58978
58988
  * passes the element, which Turndown accepts in place of a string. Covers
58979
58989
  * the nested `service.turndown(el.outerHTML)` calls in the list rules too.
58980
58990
  */
58981
- parseWithInjectedDom(service, dom) {
58991
+ parseWithInjectedDom(service, dom, batchParagraphs) {
58982
58992
  const turndown = service.turndown.bind(service);
58983
58993
  service.turndown = (input) => {
58984
58994
  if (typeof input !== "string") return turndown(input);
@@ -58989,9 +58999,64 @@ var ConversionEngine = class {
58989
58999
  const root = doc.getElementById("turndown-root");
58990
59000
  root?.normalize();
58991
59001
  if (!root) throw new Error("Injected DOM did not produce the turndown root element");
58992
- return turndown(root);
59002
+ if (batchParagraphs) {
59003
+ this.batchParagraphSiblings(root);
59004
+ this.batchTableRows(root);
59005
+ }
59006
+ return turndown(this.ownedDomInput(root));
58993
59007
  };
58994
59008
  }
59009
+ /** Turndown consumes a fresh private tree synchronously; caller-owned DOM never uses this adapter. */
59010
+ ownedDomInput(root) {
59011
+ return { nodeType: 1, cloneNode: () => root };
59012
+ }
59013
+ /**
59014
+ * GFM paragraphs and generic divs both contribute two separating newlines. Group long runs in the
59015
+ * private parsed DOM so Turndown joins a few hundred outputs at a time, instead of flattening a growing
59016
+ * string tens of thousands of times (#180). Keep DOM-sensitive table/list/quote/pre rules untouched.
59017
+ */
59018
+ batchParagraphSiblings(root) {
59019
+ const containers = [root, ...Array.from(root.querySelectorAll("body,main,article,section,div"))];
59020
+ for (const container of containers) {
59021
+ if (container.childNodes.length < 512 || container.closest("table,ul,ol,pre,blockquote")) continue;
59022
+ const children = Array.from(container.childNodes);
59023
+ if (!children.every((child) => child.nodeType === 1 ? child.nodeName === "P" : child.nodeType === 8 || child.nodeType === 3 && !child.textContent?.trim())) continue;
59024
+ let group;
59025
+ let count = 0;
59026
+ for (const child of children) {
59027
+ if (!group) {
59028
+ group = container.ownerDocument.createElement("div");
59029
+ container.insertBefore(group, child);
59030
+ }
59031
+ group.appendChild(child);
59032
+ if (child.nodeType === 1 && ++count === 256) {
59033
+ group = void 0;
59034
+ count = 0;
59035
+ }
59036
+ }
59037
+ }
59038
+ }
59039
+ /** The table rule discards preliminary row Markdown and queries all descendant rows itself. */
59040
+ batchTableRows(root) {
59041
+ for (const container of Array.from(root.querySelectorAll("table,thead,tbody,tfoot"))) {
59042
+ if (container.childNodes.length < 512) continue;
59043
+ const children = Array.from(container.childNodes);
59044
+ if (!children.every((child) => child.nodeType === 1 ? child.nodeName === "TR" : child.nodeType === 8 || child.nodeType === 3 && !child.textContent?.trim())) continue;
59045
+ let group;
59046
+ let count = 0;
59047
+ for (const child of children) {
59048
+ if (!group) {
59049
+ group = container.ownerDocument.createElement("tbody");
59050
+ container.insertBefore(group, child);
59051
+ }
59052
+ group.appendChild(child);
59053
+ if (child.nodeType === 1 && ++count === 256) {
59054
+ group = void 0;
59055
+ count = 0;
59056
+ }
59057
+ }
59058
+ }
59059
+ }
58995
59060
  initializeServices() {
58996
59061
  this.githubService = new import_turndown2.default({
58997
59062
  headingStyle: "atx",
@@ -59627,10 +59692,22 @@ ${plainText}
59627
59692
  if (rows.length === 0) return content;
59628
59693
  let markdown = "\n";
59629
59694
  let headerProcessed = false;
59695
+ let tableLength = 0;
59630
59696
  rows.forEach((row, index) => {
59631
59697
  const cells = Array.from(row.querySelectorAll("th, td"));
59632
- const cellContents = cells.map((cell) => this.escapeTableCell(this.renderTableCell(cell)));
59698
+ const cellContents = cells.map((cell) => {
59699
+ let rendered = this.tableCellCache?.get(cell);
59700
+ if (rendered === void 0) {
59701
+ rendered = this.escapeTableCell(this.renderTableCell(cell));
59702
+ this.chargeTableMarkdown(rendered.length);
59703
+ this.tableCellCache?.set(cell, rendered);
59704
+ }
59705
+ return rendered;
59706
+ });
59633
59707
  if (cellContents.length > 0) {
59708
+ tableLength += 1 + 3 * cellContents.length;
59709
+ for (const value of cellContents) tableLength += value.length;
59710
+ if (tableLength > this.tableMarkdownBudget) throw new TableMarkdownLimitError();
59634
59711
  markdown += "| " + cellContents.join(" | ") + " |\n";
59635
59712
  if (!headerProcessed && (row.querySelector("th") || index === 0)) {
59636
59713
  const separator = cellContents.map(() => "---").join(" | ");
@@ -59643,6 +59720,11 @@ ${plainText}
59643
59720
  }
59644
59721
  });
59645
59722
  }
59723
+ /** Deduct a rendered cell, which the per-conversion cache keeps alive until the conversion ends. */
59724
+ chargeTableMarkdown(chars) {
59725
+ this.tableMarkdownBudget -= chars;
59726
+ if (this.tableMarkdownBudget < 0) throw new TableMarkdownLimitError();
59727
+ }
59646
59728
  renderTableCell(cell) {
59647
59729
  const ELEMENT_NODE2 = 1;
59648
59730
  const blocks = [];
@@ -59700,8 +59782,17 @@ ${plainText}
59700
59782
  }
59701
59783
  renderCodeSpan(content) {
59702
59784
  if (!content.includes("`")) return `\`${content}\``;
59703
- const runs = content.match(/`+/g) || ["`"];
59704
- const delimiter = "`".repeat(Math.max(...runs.map((run) => run.length)) + 1);
59785
+ let longest = 0;
59786
+ let current = 0;
59787
+ for (let i = 0; i < content.length; i++) {
59788
+ if (content.charCodeAt(i) === 96) {
59789
+ current++;
59790
+ if (current > longest) longest = current;
59791
+ } else {
59792
+ current = 0;
59793
+ }
59794
+ }
59795
+ const delimiter = "`".repeat(longest + 1);
59705
59796
  return `${delimiter} ${content} ${delimiter}`;
59706
59797
  }
59707
59798
  escapeMarkdownLinkText(text) {
@@ -59711,7 +59802,8 @@ ${plainText}
59711
59802
  return href.replace(/</g, "%3C").replace(/>/g, "%3E");
59712
59803
  }
59713
59804
  escapeTableCell(content) {
59714
- return content.replace(/\|/g, "\\|").replace(/\s*<br>\s*/g, "<br>").trim();
59805
+ const escaped = content.replace(/\|/g, "\\|");
59806
+ return escaped.includes("<br>") ? escaped.split("<br>").map((part) => part.trim()).join("<br>") : escaped.trim();
59715
59807
  }
59716
59808
  /** A space after a checkbox marker unless the label text already starts with one. */
59717
59809
  checkboxSpacer(node) {
@@ -59766,6 +59858,12 @@ ${plainText}
59766
59858
  });
59767
59859
  }
59768
59860
  addListRules(service) {
59861
+ if (this.dom && service === this.githubService) {
59862
+ service.addRule("discardPreliminaryListItems", {
59863
+ filter: (node) => node.nodeName === "LI" && (node.parentNode?.nodeName === "UL" || node.parentNode?.nodeName === "OL"),
59864
+ replacement: () => ""
59865
+ });
59866
+ }
59769
59867
  service.addRule("enhancedList", {
59770
59868
  filter: ["ul", "ol"],
59771
59869
  replacement: (content, node) => {
@@ -59792,10 +59890,12 @@ ${plainText}
59792
59890
  let markdown = "";
59793
59891
  items.forEach((item, index) => {
59794
59892
  const marker = isOrdered ? `${index + 1}.` : bullet;
59795
- const indent = " ".repeat(marker.length + 1);
59796
59893
  const body = this.processListItem(item, service);
59797
- const lines = body.split("\n");
59798
- const indented = lines.map((line, i) => i === 0 || line === "" ? line : indent + line).join("\n");
59894
+ let indented = body;
59895
+ if (body.includes("\n")) {
59896
+ const indent = " ".repeat(marker.length + 1);
59897
+ indented = body.split("\n").map((line, i) => i === 0 || line === "" ? line : indent + line).join("\n");
59898
+ }
59799
59899
  markdown += `${marker} ${indented}
59800
59900
  `;
59801
59901
  });
@@ -59808,13 +59908,21 @@ ${plainText}
59808
59908
  * multiple paragraphs stay separated.
59809
59909
  */
59810
59910
  processListItem(item, service) {
59911
+ if (this.dom && service === this.githubService && service.options.linkStyle !== "referenced") {
59912
+ const child = item.firstChild;
59913
+ if (child?.nodeType === 3 && child.nextSibling === null) {
59914
+ const text = child.textContent ?? "";
59915
+ if (!/\s/.test(text)) return service.escape(text).trim();
59916
+ }
59917
+ }
59811
59918
  const parts = [];
59812
59919
  const ELEMENT_NODE2 = 1;
59813
59920
  const doc = item.ownerDocument;
59814
59921
  let buffer = doc.createElement("div");
59815
59922
  const flush = () => {
59816
59923
  if (buffer.childNodes.length > 0) {
59817
- const md = service.turndown(buffer).trim();
59924
+ const input = this.dom && service === this.githubService ? this.ownedDomInput(buffer) : buffer;
59925
+ const md = service.turndown(input).trim();
59818
59926
  if (md) parts.push(md);
59819
59927
  }
59820
59928
  buffer = doc.createElement("div");
@@ -59936,7 +60044,17 @@ ${plainText}
59936
60044
  break;
59937
60045
  }
59938
60046
  const htmlForConversion = options.preserveTables ? sanitizedHtml : this.flattenTables(sanitizedHtml);
59939
- let markdown = service.turndown(htmlForConversion);
60047
+ const previousCache = this.tableCellCache;
60048
+ const previousBudget = this.tableMarkdownBudget;
60049
+ this.tableCellCache = /* @__PURE__ */ new WeakMap();
60050
+ this.tableMarkdownBudget = options.maxTableMarkdownChars ?? Number.POSITIVE_INFINITY;
60051
+ let markdown;
60052
+ try {
60053
+ markdown = service.turndown(htmlForConversion);
60054
+ } finally {
60055
+ this.tableCellCache = previousCache;
60056
+ this.tableMarkdownBudget = previousBudget;
60057
+ }
59940
60058
  markdown = this.applyFormatPostProcessing(markdown, options.format);
59941
60059
  const endTime = performance.now();
59942
60060
  const metadata = {
@@ -64297,7 +64415,7 @@ async function binaryToMarkdown(buffer, type, format) {
64297
64415
  preserveLists: true,
64298
64416
  customRules: []
64299
64417
  });
64300
- return { markdown: converted.markdown, warnings: result.warnings, warningCodes: result.warningCodes };
64418
+ return { markdown: converted.markdown, warnings: result.warnings };
64301
64419
  }
64302
64420
  case "pdf": {
64303
64421
  const result = await pdfAdapter.parseToText(toArrayBuffer());
@@ -64320,7 +64438,7 @@ async function binaryToMarkdown(buffer, type, format) {
64320
64438
  preserveLists: true,
64321
64439
  customRules: []
64322
64440
  });
64323
- return { markdown: converted.markdown, warnings: result.warnings, warningCodes: result.warningCodes };
64441
+ return { markdown: converted.markdown, warnings: result.warnings };
64324
64442
  }
64325
64443
  case "json": {
64326
64444
  const result = await dataAdapter.parseJsonToHtml(toArrayBuffer());
@@ -64331,7 +64449,7 @@ async function binaryToMarkdown(buffer, type, format) {
64331
64449
  preserveLists: true,
64332
64450
  customRules: []
64333
64451
  });
64334
- return { markdown: converted.markdown, warnings: result.warnings, warningCodes: result.warningCodes };
64452
+ return { markdown: converted.markdown, warnings: result.warnings };
64335
64453
  }
64336
64454
  case "xml": {
64337
64455
  const result = await dataAdapter.parseXmlToHtml(toArrayBuffer());
@@ -64342,7 +64460,7 @@ async function binaryToMarkdown(buffer, type, format) {
64342
64460
  preserveLists: true,
64343
64461
  customRules: []
64344
64462
  });
64345
- return { markdown: converted.markdown, warnings: result.warnings, warningCodes: result.warningCodes };
64463
+ return { markdown: converted.markdown, warnings: result.warnings };
64346
64464
  }
64347
64465
  case "pptx": {
64348
64466
  const result = await pptxAdapter.parseToHtml(toArrayBuffer());
@@ -64353,7 +64471,7 @@ async function binaryToMarkdown(buffer, type, format) {
64353
64471
  preserveLists: true,
64354
64472
  customRules: []
64355
64473
  });
64356
- return { markdown: converted.markdown, warnings: result.warnings, warningCodes: result.warningCodes };
64474
+ return { markdown: converted.markdown, warnings: result.warnings };
64357
64475
  }
64358
64476
  }
64359
64477
  }
@@ -64909,8 +65027,8 @@ import { readFileSync as readFileSync5 } from "fs";
64909
65027
  import { resolve, dirname as dirname3 } from "path";
64910
65028
  import { fileURLToPath } from "url";
64911
65029
  var getVersion = () => {
64912
- if ("1.0.38") {
64913
- return "1.0.38";
65030
+ if ("1.0.40") {
65031
+ return "1.0.40";
64914
65032
  }
64915
65033
  try {
64916
65034
  const __dirname2 = dirname3(fileURLToPath(import.meta.url));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formatferry",
3
- "version": "1.0.38",
3
+ "version": "1.0.40",
4
4
  "description": "Format Ferry CLI — Convert HTML, DOCX, PDF, XLSX, CSV, JSON, XML, PPTX to Markdown",
5
5
  "keywords": [
6
6
  "markdown",