formatferry 1.0.39 → 1.0.41

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 +354 -156
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -58011,6 +58011,153 @@ import DOMPurifyImport from "dompurify";
58011
58011
  // ../../src/shared/constants.ts
58012
58012
  var MAX_INTERMEDIATE_HTML_CHARS = 256 * 1024 * 1024;
58013
58013
 
58014
+ // ../../src/shared/linkedom-sanitizer.ts
58015
+ var FORBID_CONTENTS = /* @__PURE__ */ new Set([
58016
+ "annotation-xml",
58017
+ "audio",
58018
+ "colgroup",
58019
+ "desc",
58020
+ "foreignobject",
58021
+ "head",
58022
+ "iframe",
58023
+ "math",
58024
+ "mi",
58025
+ "mn",
58026
+ "mo",
58027
+ "ms",
58028
+ "mtext",
58029
+ "noembed",
58030
+ "noframes",
58031
+ "noscript",
58032
+ "plaintext",
58033
+ "script",
58034
+ "selectedcontent",
58035
+ "style",
58036
+ "svg",
58037
+ "template",
58038
+ "thead",
58039
+ "title",
58040
+ "video",
58041
+ "xmp"
58042
+ ]);
58043
+ var BLOCK_UNWRAP_TAGS = /* @__PURE__ */ new Set([
58044
+ "address",
58045
+ "article",
58046
+ "aside",
58047
+ "center",
58048
+ "details",
58049
+ "dialog",
58050
+ "dir",
58051
+ "dd",
58052
+ "dl",
58053
+ "dt",
58054
+ "fieldset",
58055
+ "figcaption",
58056
+ "figure",
58057
+ "footer",
58058
+ "form",
58059
+ "header",
58060
+ "hgroup",
58061
+ "legend",
58062
+ "main",
58063
+ "menu",
58064
+ "nav",
58065
+ "search",
58066
+ "section",
58067
+ "summary"
58068
+ ]);
58069
+ var DATA_URI_TAGS = /* @__PURE__ */ new Set(["audio", "video", "img", "source", "image", "track"]);
58070
+ var URI_SAFE_ATTRIBUTES = /* @__PURE__ */ new Set([
58071
+ "alt",
58072
+ "class",
58073
+ "for",
58074
+ "id",
58075
+ "label",
58076
+ "name",
58077
+ "pattern",
58078
+ "placeholder",
58079
+ "role",
58080
+ "summary",
58081
+ "title",
58082
+ "value",
58083
+ "style",
58084
+ "xmlns"
58085
+ ]);
58086
+ var IS_ALLOWED_URI = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
58087
+ var ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g;
58088
+ var ELEMENT_NODE = 1;
58089
+ var COMMENT_NODE = 8;
58090
+ function toSet(list) {
58091
+ return new Set((list ?? []).map((s) => s.toLowerCase()));
58092
+ }
58093
+ function attrAllowed(name, allowed, wildcards) {
58094
+ return allowed.has(name) || wildcards.some((prefix) => name.startsWith(prefix));
58095
+ }
58096
+ function isSafeValue(tag, name, value) {
58097
+ if (URI_SAFE_ATTRIBUTES.has(name) || !value) return true;
58098
+ if (IS_ALLOWED_URI.test(value.replace(ATTR_WHITESPACE, ""))) return true;
58099
+ return (name === "src" || name === "href" || name === "xlink:href") && value.startsWith("data:") && DATA_URI_TAGS.has(tag);
58100
+ }
58101
+ function sanitizeWithLinkedom(html, config, parse) {
58102
+ if (!html) return "";
58103
+ const allowedTags = toSet(config.ALLOWED_TAGS);
58104
+ const forbidTags = toSet(config.FORBID_TAGS);
58105
+ const allowedAttrList = (config.ALLOWED_ATTR ?? []).map((s) => s.toLowerCase());
58106
+ const allowedAttrs = new Set(allowedAttrList.filter((a) => !a.endsWith("*")));
58107
+ const attrWildcards = allowedAttrList.filter((a) => a.endsWith("*")).map((a) => a.slice(0, -1));
58108
+ const forbidAttrs = toSet(config.FORBID_ATTR);
58109
+ const doc = parse(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`);
58110
+ const body = doc.body;
58111
+ if (!body) return "";
58112
+ const toUnwrap = [];
58113
+ const hadChildren = /* @__PURE__ */ new Set();
58114
+ const stack = [body];
58115
+ while (stack.length > 0) {
58116
+ const parent = stack.pop();
58117
+ for (const child of Array.from(parent.childNodes)) {
58118
+ if (child.nodeType === COMMENT_NODE) {
58119
+ parent.removeChild(child);
58120
+ continue;
58121
+ }
58122
+ if (child.nodeType !== ELEMENT_NODE) continue;
58123
+ const el = child;
58124
+ const tag = el.nodeName.toLowerCase();
58125
+ if (!allowedTags.has(tag) || forbidTags.has(tag)) {
58126
+ if (FORBID_CONTENTS.has(tag)) {
58127
+ parent.removeChild(el);
58128
+ } else {
58129
+ toUnwrap.push(el);
58130
+ if (el.firstChild) hadChildren.add(el);
58131
+ stack.push(el);
58132
+ }
58133
+ continue;
58134
+ }
58135
+ for (const attr of Array.from(el.attributes)) {
58136
+ const name = attr.name.toLowerCase();
58137
+ if (name.startsWith("on") || forbidAttrs.has(name) || !attrAllowed(name, allowedAttrs, attrWildcards) || !isSafeValue(tag, name, attr.value)) {
58138
+ el.removeAttribute(attr.name);
58139
+ }
58140
+ }
58141
+ stack.push(el);
58142
+ }
58143
+ }
58144
+ const wrapBlocks = allowedTags.has("div") && !forbidTags.has("div");
58145
+ for (let i = toUnwrap.length - 1; i >= 0; i--) {
58146
+ const el = toUnwrap[i];
58147
+ const parent = el.parentNode;
58148
+ if (!parent) continue;
58149
+ if (wrapBlocks && hadChildren.has(el) && BLOCK_UNWRAP_TAGS.has(el.nodeName.toLowerCase())) {
58150
+ const div = doc.createElement("div");
58151
+ while (el.firstChild) div.appendChild(el.firstChild);
58152
+ parent.replaceChild(div, el);
58153
+ continue;
58154
+ }
58155
+ while (el.firstChild) parent.insertBefore(el.firstChild, el);
58156
+ parent.removeChild(el);
58157
+ }
58158
+ return body.innerHTML;
58159
+ }
58160
+
58014
58161
  // ../../src/shared/input-sanitizer.ts
58015
58162
  function resolveDOMPurify() {
58016
58163
  const shimInstance = globalThis.__FF_DOMPurify;
@@ -58056,12 +58203,42 @@ var InputSanitizer = class {
58056
58203
  if (!html || typeof html !== "string") {
58057
58204
  return "";
58058
58205
  }
58059
- return this.domEnv.DOMPurify.sanitize(html, {
58060
- ALLOWED_TAGS: this.purifyConfig.ALLOWED_TAGS,
58061
- ALLOWED_ATTR: this.purifyConfig.ALLOWED_ATTR,
58062
- FORBID_TAGS: this.purifyConfig.FORBID_TAGS,
58063
- FORBID_ATTR: this.purifyConfig.FORBID_ATTR
58064
- });
58206
+ const purify = this.domEnv.DOMPurify;
58207
+ const hook = this.blockUnwrapHook();
58208
+ const useHook = hook && purify.addHook && purify.removeHook;
58209
+ if (useHook) purify.addHook("uponSanitizeElement", hook);
58210
+ try {
58211
+ return purify.sanitize(html, {
58212
+ ALLOWED_TAGS: this.purifyConfig.ALLOWED_TAGS,
58213
+ ALLOWED_ATTR: this.purifyConfig.ALLOWED_ATTR,
58214
+ FORBID_TAGS: this.purifyConfig.FORBID_TAGS,
58215
+ FORBID_ATTR: this.purifyConfig.FORBID_ATTR
58216
+ });
58217
+ } finally {
58218
+ if (useHook) purify.removeHook("uponSanitizeElement", hook);
58219
+ }
58220
+ }
58221
+ /**
58222
+ * DOMPurify unwraps a disallowed element by moving its children up, so
58223
+ * `<header>Head</header><nav>Nav</nav>` came out as "HeadNav". For a
58224
+ * block-level element, move its children into a <div> first: DOMPurify then
58225
+ * hoists that <div>, and Turndown gives the content its own paragraph. Same
58226
+ * rule as the linkedom sanitiser (BLOCK_UNWRAP_TAGS, #177).
58227
+ */
58228
+ blockUnwrapHook() {
58229
+ const allowed = new Set((this.purifyConfig.ALLOWED_TAGS ?? []).map((t) => t.toLowerCase()));
58230
+ const forbidden = new Set((this.purifyConfig.FORBID_TAGS ?? []).map((t) => t.toLowerCase()));
58231
+ if (!allowed.has("div") || forbidden.has("div")) return void 0;
58232
+ return (node, data) => {
58233
+ const tag = data.tagName.toLowerCase();
58234
+ if (!BLOCK_UNWRAP_TAGS.has(tag) || allowed.has(tag) && !forbidden.has(tag)) return;
58235
+ const el = node;
58236
+ const doc = el.ownerDocument;
58237
+ if (!doc || !el.firstChild) return;
58238
+ const div = doc.createElement("div");
58239
+ while (el.firstChild) div.appendChild(el.firstChild);
58240
+ el.appendChild(div);
58241
+ };
58065
58242
  }
58066
58243
  /**
58067
58244
  * Sanitize HTML content to prevent XSS attacks
@@ -58238,6 +58415,13 @@ var InputSanitizer = class {
58238
58415
  "th",
58239
58416
  "figure",
58240
58417
  "figcaption",
58418
+ // Definition lists and disclosure widgets (#177): unwrapped, their
58419
+ // text ran together ("TermDefinition", "MoreHidden").
58420
+ "dl",
58421
+ "dt",
58422
+ "dd",
58423
+ "details",
58424
+ "summary",
58241
58425
  "a",
58242
58426
  "img",
58243
58427
  "sup",
@@ -58942,6 +59126,12 @@ h${level}. ${trimmed}
58942
59126
  }
58943
59127
 
58944
59128
  // ../../src/shared/conversion-engine.ts
59129
+ var TableMarkdownLimitError = class extends Error {
59130
+ constructor() {
59131
+ super("Table Markdown exceeds the conversion limit");
59132
+ this.name = "TableMarkdownLimitError";
59133
+ }
59134
+ };
58945
59135
  var ConversionEngine = class {
58946
59136
  githubService;
58947
59137
  commonmarkService;
@@ -58953,6 +59143,10 @@ var ConversionEngine = class {
58953
59143
  confluenceService;
58954
59144
  dom;
58955
59145
  domSanitizer;
59146
+ /** Scoped to one synchronous Turndown pass; cloned cells do not change while table rules read them. */
59147
+ tableCellCache;
59148
+ /** Table Markdown characters this conversion may still emit (maxTableMarkdownChars). */
59149
+ tableMarkdownBudget = Number.POSITIVE_INFINITY;
58956
59150
  constructor(options = {}) {
58957
59151
  this.dom = options.dom;
58958
59152
  this.initializeServices();
@@ -58967,7 +59161,7 @@ var ConversionEngine = class {
58967
59161
  this.slackService,
58968
59162
  this.confluenceService
58969
59163
  ]) {
58970
- this.parseWithInjectedDom(service, this.dom);
59164
+ this.parseWithInjectedDom(service, this.dom, service === this.githubService);
58971
59165
  }
58972
59166
  }
58973
59167
  }
@@ -58978,7 +59172,7 @@ var ConversionEngine = class {
58978
59172
  * passes the element, which Turndown accepts in place of a string. Covers
58979
59173
  * the nested `service.turndown(el.outerHTML)` calls in the list rules too.
58980
59174
  */
58981
- parseWithInjectedDom(service, dom) {
59175
+ parseWithInjectedDom(service, dom, batchParagraphs) {
58982
59176
  const turndown = service.turndown.bind(service);
58983
59177
  service.turndown = (input) => {
58984
59178
  if (typeof input !== "string") return turndown(input);
@@ -58989,9 +59183,64 @@ var ConversionEngine = class {
58989
59183
  const root = doc.getElementById("turndown-root");
58990
59184
  root?.normalize();
58991
59185
  if (!root) throw new Error("Injected DOM did not produce the turndown root element");
58992
- return turndown(root);
59186
+ if (batchParagraphs) {
59187
+ this.batchParagraphSiblings(root);
59188
+ this.batchTableRows(root);
59189
+ }
59190
+ return turndown(this.ownedDomInput(root));
58993
59191
  };
58994
59192
  }
59193
+ /** Turndown consumes a fresh private tree synchronously; caller-owned DOM never uses this adapter. */
59194
+ ownedDomInput(root) {
59195
+ return { nodeType: 1, cloneNode: () => root };
59196
+ }
59197
+ /**
59198
+ * GFM paragraphs and generic divs both contribute two separating newlines. Group long runs in the
59199
+ * private parsed DOM so Turndown joins a few hundred outputs at a time, instead of flattening a growing
59200
+ * string tens of thousands of times (#180). Keep DOM-sensitive table/list/quote/pre rules untouched.
59201
+ */
59202
+ batchParagraphSiblings(root) {
59203
+ const containers = [root, ...Array.from(root.querySelectorAll("body,main,article,section,div"))];
59204
+ for (const container of containers) {
59205
+ if (container.childNodes.length < 512 || container.closest("table,ul,ol,pre,blockquote")) continue;
59206
+ const children = Array.from(container.childNodes);
59207
+ if (!children.every((child) => child.nodeType === 1 ? child.nodeName === "P" : child.nodeType === 8 || child.nodeType === 3 && !child.textContent?.trim())) continue;
59208
+ let group;
59209
+ let count = 0;
59210
+ for (const child of children) {
59211
+ if (!group) {
59212
+ group = container.ownerDocument.createElement("div");
59213
+ container.insertBefore(group, child);
59214
+ }
59215
+ group.appendChild(child);
59216
+ if (child.nodeType === 1 && ++count === 256) {
59217
+ group = void 0;
59218
+ count = 0;
59219
+ }
59220
+ }
59221
+ }
59222
+ }
59223
+ /** The table rule discards preliminary row Markdown and queries all descendant rows itself. */
59224
+ batchTableRows(root) {
59225
+ for (const container of Array.from(root.querySelectorAll("table,thead,tbody,tfoot"))) {
59226
+ if (container.childNodes.length < 512) continue;
59227
+ const children = Array.from(container.childNodes);
59228
+ if (!children.every((child) => child.nodeType === 1 ? child.nodeName === "TR" : child.nodeType === 8 || child.nodeType === 3 && !child.textContent?.trim())) continue;
59229
+ let group;
59230
+ let count = 0;
59231
+ for (const child of children) {
59232
+ if (!group) {
59233
+ group = container.ownerDocument.createElement("tbody");
59234
+ container.insertBefore(group, child);
59235
+ }
59236
+ group.appendChild(child);
59237
+ if (child.nodeType === 1 && ++count === 256) {
59238
+ group = void 0;
59239
+ count = 0;
59240
+ }
59241
+ }
59242
+ }
59243
+ }
58995
59244
  initializeServices() {
58996
59245
  this.githubService = new import_turndown2.default({
58997
59246
  headingStyle: "atx",
@@ -59063,7 +59312,21 @@ var ConversionEngine = class {
59063
59312
  this.rmarkdownService,
59064
59313
  this.slackService,
59065
59314
  this.confluenceService
59066
- ].forEach((service) => this.addGoogleDocsWrapperRule(service));
59315
+ ].forEach((service) => {
59316
+ this.addGoogleDocsWrapperRule(service);
59317
+ this.addDisclosureBlockRule(service);
59318
+ });
59319
+ }
59320
+ /**
59321
+ * Turndown's block list has no DETAILS or SUMMARY, so it treated them as
59322
+ * inline and `<details><summary>More</summary>Hidden</details>` came out as
59323
+ * "MoreHidden". Give them block spacing like a <div> (#177).
59324
+ */
59325
+ addDisclosureBlockRule(service) {
59326
+ service.addRule("disclosureBlock", {
59327
+ filter: ["details", "summary"],
59328
+ replacement: (content) => "\n\n" + content + "\n\n"
59329
+ });
59067
59330
  }
59068
59331
  /**
59069
59332
  * Google Docs wraps every copied selection in
@@ -59627,10 +59890,22 @@ ${plainText}
59627
59890
  if (rows.length === 0) return content;
59628
59891
  let markdown = "\n";
59629
59892
  let headerProcessed = false;
59893
+ let tableLength = 0;
59630
59894
  rows.forEach((row, index) => {
59631
59895
  const cells = Array.from(row.querySelectorAll("th, td"));
59632
- const cellContents = cells.map((cell) => this.escapeTableCell(this.renderTableCell(cell)));
59896
+ const cellContents = cells.map((cell) => {
59897
+ let rendered = this.tableCellCache?.get(cell);
59898
+ if (rendered === void 0) {
59899
+ rendered = this.escapeTableCell(this.renderTableCell(cell));
59900
+ this.chargeTableMarkdown(rendered.length);
59901
+ this.tableCellCache?.set(cell, rendered);
59902
+ }
59903
+ return rendered;
59904
+ });
59633
59905
  if (cellContents.length > 0) {
59906
+ tableLength += 1 + 3 * cellContents.length;
59907
+ for (const value of cellContents) tableLength += value.length;
59908
+ if (tableLength > this.tableMarkdownBudget) throw new TableMarkdownLimitError();
59634
59909
  markdown += "| " + cellContents.join(" | ") + " |\n";
59635
59910
  if (!headerProcessed && (row.querySelector("th") || index === 0)) {
59636
59911
  const separator = cellContents.map(() => "---").join(" | ");
@@ -59643,6 +59918,11 @@ ${plainText}
59643
59918
  }
59644
59919
  });
59645
59920
  }
59921
+ /** Deduct a rendered cell, which the per-conversion cache keeps alive until the conversion ends. */
59922
+ chargeTableMarkdown(chars) {
59923
+ this.tableMarkdownBudget -= chars;
59924
+ if (this.tableMarkdownBudget < 0) throw new TableMarkdownLimitError();
59925
+ }
59646
59926
  renderTableCell(cell) {
59647
59927
  const ELEMENT_NODE2 = 1;
59648
59928
  const blocks = [];
@@ -59700,8 +59980,17 @@ ${plainText}
59700
59980
  }
59701
59981
  renderCodeSpan(content) {
59702
59982
  if (!content.includes("`")) return `\`${content}\``;
59703
- const runs = content.match(/`+/g) || ["`"];
59704
- const delimiter = "`".repeat(Math.max(...runs.map((run) => run.length)) + 1);
59983
+ let longest = 0;
59984
+ let current = 0;
59985
+ for (let i = 0; i < content.length; i++) {
59986
+ if (content.charCodeAt(i) === 96) {
59987
+ current++;
59988
+ if (current > longest) longest = current;
59989
+ } else {
59990
+ current = 0;
59991
+ }
59992
+ }
59993
+ const delimiter = "`".repeat(longest + 1);
59705
59994
  return `${delimiter} ${content} ${delimiter}`;
59706
59995
  }
59707
59996
  escapeMarkdownLinkText(text) {
@@ -59711,7 +60000,8 @@ ${plainText}
59711
60000
  return href.replace(/</g, "%3C").replace(/>/g, "%3E");
59712
60001
  }
59713
60002
  escapeTableCell(content) {
59714
- return content.replace(/\|/g, "\\|").replace(/\s*<br>\s*/g, "<br>").trim();
60003
+ const escaped = content.replace(/\|/g, "\\|");
60004
+ return escaped.includes("<br>") ? escaped.split("<br>").map((part) => part.trim()).join("<br>") : escaped.trim();
59715
60005
  }
59716
60006
  /** A space after a checkbox marker unless the label text already starts with one. */
59717
60007
  checkboxSpacer(node) {
@@ -59766,6 +60056,12 @@ ${plainText}
59766
60056
  });
59767
60057
  }
59768
60058
  addListRules(service) {
60059
+ if (this.dom && service === this.githubService) {
60060
+ service.addRule("discardPreliminaryListItems", {
60061
+ filter: (node) => node.nodeName === "LI" && (node.parentNode?.nodeName === "UL" || node.parentNode?.nodeName === "OL"),
60062
+ replacement: () => ""
60063
+ });
60064
+ }
59769
60065
  service.addRule("enhancedList", {
59770
60066
  filter: ["ul", "ol"],
59771
60067
  replacement: (content, node) => {
@@ -59792,10 +60088,12 @@ ${plainText}
59792
60088
  let markdown = "";
59793
60089
  items.forEach((item, index) => {
59794
60090
  const marker = isOrdered ? `${index + 1}.` : bullet;
59795
- const indent = " ".repeat(marker.length + 1);
59796
60091
  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");
60092
+ let indented = body;
60093
+ if (body.includes("\n")) {
60094
+ const indent = " ".repeat(marker.length + 1);
60095
+ indented = body.split("\n").map((line, i) => i === 0 || line === "" ? line : indent + line).join("\n");
60096
+ }
59799
60097
  markdown += `${marker} ${indented}
59800
60098
  `;
59801
60099
  });
@@ -59808,13 +60106,21 @@ ${plainText}
59808
60106
  * multiple paragraphs stay separated.
59809
60107
  */
59810
60108
  processListItem(item, service) {
60109
+ if (this.dom && service === this.githubService && service.options.linkStyle !== "referenced") {
60110
+ const child = item.firstChild;
60111
+ if (child?.nodeType === 3 && child.nextSibling === null) {
60112
+ const text = child.textContent ?? "";
60113
+ if (!/\s/.test(text)) return service.escape(text).trim();
60114
+ }
60115
+ }
59811
60116
  const parts = [];
59812
60117
  const ELEMENT_NODE2 = 1;
59813
60118
  const doc = item.ownerDocument;
59814
60119
  let buffer = doc.createElement("div");
59815
60120
  const flush = () => {
59816
60121
  if (buffer.childNodes.length > 0) {
59817
- const md = service.turndown(buffer).trim();
60122
+ const input = this.dom && service === this.githubService ? this.ownedDomInput(buffer) : buffer;
60123
+ const md = service.turndown(input).trim();
59818
60124
  if (md) parts.push(md);
59819
60125
  }
59820
60126
  buffer = doc.createElement("div");
@@ -59936,7 +60242,17 @@ ${plainText}
59936
60242
  break;
59937
60243
  }
59938
60244
  const htmlForConversion = options.preserveTables ? sanitizedHtml : this.flattenTables(sanitizedHtml);
59939
- let markdown = service.turndown(htmlForConversion);
60245
+ const previousCache = this.tableCellCache;
60246
+ const previousBudget = this.tableMarkdownBudget;
60247
+ this.tableCellCache = /* @__PURE__ */ new WeakMap();
60248
+ this.tableMarkdownBudget = options.maxTableMarkdownChars ?? Number.POSITIVE_INFINITY;
60249
+ let markdown;
60250
+ try {
60251
+ markdown = service.turndown(htmlForConversion);
60252
+ } finally {
60253
+ this.tableCellCache = previousCache;
60254
+ this.tableMarkdownBudget = previousBudget;
60255
+ }
59940
60256
  markdown = this.applyFormatPostProcessing(markdown, options.format);
59941
60257
  const endTime = performance.now();
59942
60258
  const metadata = {
@@ -63696,118 +64012,6 @@ function buildCachedProvider(config, lastValidated) {
63696
64012
  return new CachedEntitlementProvider(cachedState);
63697
64013
  }
63698
64014
 
63699
- // ../../src/shared/linkedom-sanitizer.ts
63700
- var FORBID_CONTENTS = /* @__PURE__ */ new Set([
63701
- "annotation-xml",
63702
- "audio",
63703
- "colgroup",
63704
- "desc",
63705
- "foreignobject",
63706
- "head",
63707
- "iframe",
63708
- "math",
63709
- "mi",
63710
- "mn",
63711
- "mo",
63712
- "ms",
63713
- "mtext",
63714
- "noembed",
63715
- "noframes",
63716
- "noscript",
63717
- "plaintext",
63718
- "script",
63719
- "selectedcontent",
63720
- "style",
63721
- "svg",
63722
- "template",
63723
- "thead",
63724
- "title",
63725
- "video",
63726
- "xmp"
63727
- ]);
63728
- var DATA_URI_TAGS = /* @__PURE__ */ new Set(["audio", "video", "img", "source", "image", "track"]);
63729
- var URI_SAFE_ATTRIBUTES = /* @__PURE__ */ new Set([
63730
- "alt",
63731
- "class",
63732
- "for",
63733
- "id",
63734
- "label",
63735
- "name",
63736
- "pattern",
63737
- "placeholder",
63738
- "role",
63739
- "summary",
63740
- "title",
63741
- "value",
63742
- "style",
63743
- "xmlns"
63744
- ]);
63745
- var IS_ALLOWED_URI = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
63746
- var ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g;
63747
- var ELEMENT_NODE = 1;
63748
- var COMMENT_NODE = 8;
63749
- function toSet(list) {
63750
- return new Set((list ?? []).map((s) => s.toLowerCase()));
63751
- }
63752
- function attrAllowed(name, allowed, wildcards) {
63753
- return allowed.has(name) || wildcards.some((prefix) => name.startsWith(prefix));
63754
- }
63755
- function isSafeValue(tag, name, value) {
63756
- if (URI_SAFE_ATTRIBUTES.has(name) || !value) return true;
63757
- if (IS_ALLOWED_URI.test(value.replace(ATTR_WHITESPACE, ""))) return true;
63758
- return (name === "src" || name === "href" || name === "xlink:href") && value.startsWith("data:") && DATA_URI_TAGS.has(tag);
63759
- }
63760
- function sanitizeWithLinkedom(html, config, parse) {
63761
- if (!html) return "";
63762
- const allowedTags = toSet(config.ALLOWED_TAGS);
63763
- const forbidTags = toSet(config.FORBID_TAGS);
63764
- const allowedAttrList = (config.ALLOWED_ATTR ?? []).map((s) => s.toLowerCase());
63765
- const allowedAttrs = new Set(allowedAttrList.filter((a) => !a.endsWith("*")));
63766
- const attrWildcards = allowedAttrList.filter((a) => a.endsWith("*")).map((a) => a.slice(0, -1));
63767
- const forbidAttrs = toSet(config.FORBID_ATTR);
63768
- const doc = parse(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`);
63769
- const body = doc.body;
63770
- if (!body) return "";
63771
- const toUnwrap = [];
63772
- const stack = [body];
63773
- while (stack.length > 0) {
63774
- const parent = stack.pop();
63775
- for (const child of Array.from(parent.childNodes)) {
63776
- if (child.nodeType === COMMENT_NODE) {
63777
- parent.removeChild(child);
63778
- continue;
63779
- }
63780
- if (child.nodeType !== ELEMENT_NODE) continue;
63781
- const el = child;
63782
- const tag = el.nodeName.toLowerCase();
63783
- if (!allowedTags.has(tag) || forbidTags.has(tag)) {
63784
- if (FORBID_CONTENTS.has(tag)) {
63785
- parent.removeChild(el);
63786
- } else {
63787
- toUnwrap.push(el);
63788
- stack.push(el);
63789
- }
63790
- continue;
63791
- }
63792
- for (const attr of Array.from(el.attributes)) {
63793
- const name = attr.name.toLowerCase();
63794
- if (name.startsWith("on") || forbidAttrs.has(name) || !attrAllowed(name, allowedAttrs, attrWildcards) || !isSafeValue(tag, name, attr.value)) {
63795
- el.removeAttribute(attr.name);
63796
- }
63797
- }
63798
- stack.push(el);
63799
- }
63800
- }
63801
- for (let i = toUnwrap.length - 1; i >= 0; i--) {
63802
- const el = toUnwrap[i];
63803
- const parent = el.parentNode;
63804
- if (!parent) continue;
63805
- while (el.firstChild) parent.insertBefore(el.firstChild, el);
63806
- parent.removeChild(el);
63807
- }
63808
- return body.innerHTML;
63809
- }
63810
-
63811
64015
  // ../../src/shared/token-estimator.ts
63812
64016
  var CJK_REGEX3 = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g;
63813
64017
  var MAX_INPUT_LENGTH = 1e6;
@@ -64272,7 +64476,7 @@ function readStdin() {
64272
64476
  }
64273
64477
  async function htmlToMarkdown(html, format, applyAtlassianNormalization = false) {
64274
64478
  const processedHtml = applyAtlassianNormalization ? normalizeAtlassianHTML(html) : html;
64275
- const engine = new ConversionEngine();
64479
+ const engine = new ConversionEngine({ dom: linkedomDomEnvironment() });
64276
64480
  const result = await engine.convert(processedHtml, {
64277
64481
  format,
64278
64482
  preserveTables: true,
@@ -64286,12 +64490,11 @@ async function binaryToMarkdown(buffer, type, format) {
64286
64490
  buffer.byteOffset,
64287
64491
  buffer.byteOffset + buffer.byteLength
64288
64492
  );
64289
- const engine = new ConversionEngine();
64493
+ const engine = new ConversionEngine({ dom: linkedomDomEnvironment() });
64290
64494
  switch (type) {
64291
64495
  case "docx": {
64292
64496
  const result = await docxAdapter.parseToHtml(buffer);
64293
- const sanitized = sanitizeHTML(result.html);
64294
- const converted = await engine.convert(sanitized, {
64497
+ const converted = await engine.convert(result.html, {
64295
64498
  format,
64296
64499
  preserveTables: true,
64297
64500
  preserveLists: true,
@@ -64301,8 +64504,7 @@ async function binaryToMarkdown(buffer, type, format) {
64301
64504
  }
64302
64505
  case "pdf": {
64303
64506
  const result = await pdfAdapter.parseToText(toArrayBuffer());
64304
- const sanitized = sanitizeHTML(result.html);
64305
- const converted = await engine.convert(sanitized, {
64507
+ const converted = await engine.convert(result.html, {
64306
64508
  format,
64307
64509
  preserveTables: true,
64308
64510
  preserveLists: true,
@@ -64313,8 +64515,7 @@ async function binaryToMarkdown(buffer, type, format) {
64313
64515
  case "xlsx":
64314
64516
  case "csv": {
64315
64517
  const result = await spreadsheetAdapter.parseToHtml(toArrayBuffer(), type, 0);
64316
- const sanitized = sanitizeHTML(result.html);
64317
- const converted = await engine.convert(sanitized, {
64518
+ const converted = await engine.convert(result.html, {
64318
64519
  format,
64319
64520
  preserveTables: true,
64320
64521
  preserveLists: true,
@@ -64324,8 +64525,7 @@ async function binaryToMarkdown(buffer, type, format) {
64324
64525
  }
64325
64526
  case "json": {
64326
64527
  const result = await dataAdapter.parseJsonToHtml(toArrayBuffer());
64327
- const sanitized = sanitizeHTML(result.html);
64328
- const converted = await engine.convert(sanitized, {
64528
+ const converted = await engine.convert(result.html, {
64329
64529
  format,
64330
64530
  preserveTables: true,
64331
64531
  preserveLists: true,
@@ -64335,8 +64535,7 @@ async function binaryToMarkdown(buffer, type, format) {
64335
64535
  }
64336
64536
  case "xml": {
64337
64537
  const result = await dataAdapter.parseXmlToHtml(toArrayBuffer());
64338
- const sanitized = sanitizeHTML(result.html);
64339
- const converted = await engine.convert(sanitized, {
64538
+ const converted = await engine.convert(result.html, {
64340
64539
  format,
64341
64540
  preserveTables: true,
64342
64541
  preserveLists: true,
@@ -64346,8 +64545,7 @@ async function binaryToMarkdown(buffer, type, format) {
64346
64545
  }
64347
64546
  case "pptx": {
64348
64547
  const result = await pptxAdapter.parseToHtml(toArrayBuffer());
64349
- const sanitized = sanitizeHTML(result.html);
64350
- const converted = await engine.convert(sanitized, {
64548
+ const converted = await engine.convert(result.html, {
64351
64549
  format,
64352
64550
  preserveTables: true,
64353
64551
  preserveLists: true,
@@ -64359,7 +64557,7 @@ async function binaryToMarkdown(buffer, type, format) {
64359
64557
  }
64360
64558
  function linkedomDomEnvironment() {
64361
64559
  const parseHTML2 = globalThis.__FF_parseHTML;
64362
- if (!parseHTML2) throw new Error("The CLI shim must be loaded before converting a URL");
64560
+ if (!parseHTML2) throw new Error("The CLI shim must be loaded before converting");
64363
64561
  const parseDocument = (html) => parseHTML2(html).document;
64364
64562
  const { window: window3 } = parseHTML2("<!DOCTYPE html><html><head></head><body></body></html>");
64365
64563
  return {
@@ -64567,9 +64765,9 @@ async function convertFile(filePath, format) {
64567
64765
  const ext = extname2(filePath).toLowerCase();
64568
64766
  if (!SUPPORTED_EXTENSIONS.has(ext)) throw new Error(`Unsupported file type: ${ext}`);
64569
64767
  const buffer = readFileSync4(filePath);
64570
- const engine = new ConversionEngine();
64768
+ const engine = new ConversionEngine({ dom: linkedomDomEnvironment() });
64571
64769
  if (ext === ".html" || ext === ".htm") {
64572
- const result = await engine.convert(sanitizeHTML(buffer.toString("utf8")), {
64770
+ const result = await engine.convert(buffer.toString("utf8"), {
64573
64771
  format,
64574
64772
  preserveTables: true,
64575
64773
  preserveLists: true,
@@ -64580,32 +64778,32 @@ async function convertFile(filePath, format) {
64580
64778
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
64581
64779
  if (ext === ".docx") {
64582
64780
  const r = await docxAdapter.parseToHtml(arrayBuffer);
64583
- const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
64781
+ const c = await engine.convert(r.html, { format, preserveTables: true, preserveLists: true, customRules: [] });
64584
64782
  return { markdown: c.markdown, warnings: r.warnings ?? [] };
64585
64783
  }
64586
64784
  if (ext === ".pdf") {
64587
64785
  const r = await pdfAdapter.parseToText(arrayBuffer);
64588
- const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
64786
+ const c = await engine.convert(r.html, { format, preserveTables: true, preserveLists: true, customRules: [] });
64589
64787
  return { markdown: c.markdown, warnings: r.warnings ?? [] };
64590
64788
  }
64591
64789
  if (ext === ".xlsx" || ext === ".csv") {
64592
64790
  const r = await spreadsheetAdapter.parseToHtml(arrayBuffer, ext.slice(1), 0);
64593
- const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
64791
+ const c = await engine.convert(r.html, { format, preserveTables: true, preserveLists: true, customRules: [] });
64594
64792
  return { markdown: c.markdown, warnings: r.warnings ?? [] };
64595
64793
  }
64596
64794
  if (ext === ".json") {
64597
64795
  const r = await dataAdapter.parseJsonToHtml(arrayBuffer);
64598
- const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
64796
+ const c = await engine.convert(r.html, { format, preserveTables: true, preserveLists: true, customRules: [] });
64599
64797
  return { markdown: c.markdown, warnings: r.warnings ?? [] };
64600
64798
  }
64601
64799
  if (ext === ".xml") {
64602
64800
  const r = await dataAdapter.parseXmlToHtml(arrayBuffer);
64603
- const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
64801
+ const c = await engine.convert(r.html, { format, preserveTables: true, preserveLists: true, customRules: [] });
64604
64802
  return { markdown: c.markdown, warnings: r.warnings ?? [] };
64605
64803
  }
64606
64804
  if (ext === ".pptx") {
64607
64805
  const r = await pptxAdapter.parseToHtml(arrayBuffer);
64608
- const c = await engine.convert(sanitizeHTML(r.html), { format, preserveTables: true, preserveLists: true, customRules: [] });
64806
+ const c = await engine.convert(r.html, { format, preserveTables: true, preserveLists: true, customRules: [] });
64609
64807
  return { markdown: c.markdown, warnings: r.warnings ?? [] };
64610
64808
  }
64611
64809
  throw new Error(`Unsupported file type: ${ext}`);
@@ -64909,8 +65107,8 @@ import { readFileSync as readFileSync5 } from "fs";
64909
65107
  import { resolve, dirname as dirname3 } from "path";
64910
65108
  import { fileURLToPath } from "url";
64911
65109
  var getVersion = () => {
64912
- if ("1.0.39") {
64913
- return "1.0.39";
65110
+ if ("1.0.41") {
65111
+ return "1.0.41";
64914
65112
  }
64915
65113
  try {
64916
65114
  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.39",
3
+ "version": "1.0.41",
4
4
  "description": "Format Ferry CLI — Convert HTML, DOCX, PDF, XLSX, CSV, JSON, XML, PPTX to Markdown",
5
5
  "keywords": [
6
6
  "markdown",