formatferry 1.0.34 → 1.0.35

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 +105 -49
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -58205,6 +58205,7 @@ var InputSanitizer = class {
58205
58205
  ALLOWED_TAGS: [
58206
58206
  "p",
58207
58207
  "br",
58208
+ "hr",
58208
58209
  "div",
58209
58210
  "span",
58210
58211
  "h1",
@@ -58255,6 +58256,7 @@ var InputSanitizer = class {
58255
58256
  ALLOWED_TAGS: [
58256
58257
  "p",
58257
58258
  "br",
58259
+ "hr",
58258
58260
  "div",
58259
58261
  "span",
58260
58262
  "h1",
@@ -59013,6 +59015,37 @@ var ConversionEngine = class {
59013
59015
  this.confluenceService = createConfluenceService(deps);
59014
59016
  this.addSlideMarkerRules(this.slackService);
59015
59017
  this.addSlideMarkerRules(this.confluenceService);
59018
+ [
59019
+ this.githubService,
59020
+ this.commonmarkService,
59021
+ this.customService,
59022
+ this.discordService,
59023
+ this.redditService,
59024
+ this.rmarkdownService,
59025
+ this.slackService,
59026
+ this.confluenceService
59027
+ ].forEach((service) => this.addGoogleDocsWrapperRule(service));
59028
+ }
59029
+ /**
59030
+ * Google Docs wraps every copied selection in
59031
+ * <b style="font-weight:normal" id="docs-internal-guid-...">. Treating that
59032
+ * as bold puts a stray pair of bold markers around the whole document, so a
59033
+ * <b>/<strong> whose inline style explicitly sets a non-bold weight is
59034
+ * rendered as its plain content.
59035
+ */
59036
+ addGoogleDocsWrapperRule(service) {
59037
+ service.addRule("googleDocsNonBoldWrapper", {
59038
+ filter: (node) => {
59039
+ if (node.nodeName !== "B" && node.nodeName !== "STRONG") return false;
59040
+ const style = (node.getAttribute("style") || "").toLowerCase();
59041
+ const matches = [...style.matchAll(/font-weight\s*:\s*([a-z0-9]+)/g)];
59042
+ if (matches.length === 0) return false;
59043
+ const weight = matches[matches.length - 1][1];
59044
+ if (weight === "normal" || weight === "lighter") return true;
59045
+ return /^\d+$/.test(weight) && parseInt(weight, 10) < 600;
59046
+ },
59047
+ replacement: (content) => content
59048
+ });
59016
59049
  }
59017
59050
  /**
59018
59051
  * Create Discord-optimized Turndown service
@@ -59069,7 +59102,7 @@ ${plainText}
59069
59102
  },
59070
59103
  replacement: (_content, node) => {
59071
59104
  const checked = node.checked || node.hasAttribute("checked");
59072
- return checked ? "\u2611 " : "\u2610 ";
59105
+ return (checked ? "\u2611" : "\u2610") + this.checkboxSpacer(node);
59073
59106
  }
59074
59107
  });
59075
59108
  return service;
@@ -59097,9 +59130,9 @@ ${plainText}
59097
59130
  * Converts Unicode checkboxes to [x] and [ ]
59098
59131
  */
59099
59132
  normalizeGitHubCheckboxes(markdown) {
59100
- let result = markdown;
59101
- result = result.replace(/^([-*]\s+)[☑✓✔■⬛]/gm, "$1[x]");
59102
- result = result.replace(/^([-*]\s+)[☐✗✘□⬜]/gm, "$1[ ]");
59133
+ let result = markdown.replace(/^([ \t]*(?:[-*+]|\d+[.)])[ \t]+)\\\[([ xX])\\\]/gm, "$1[$2]");
59134
+ result = result.replace(/^([ \t]*(?:[-*+]|\d+[.)])[ \t]+)[☑✓✔■⬛]/gm, "$1[x]");
59135
+ result = result.replace(/^([ \t]*(?:[-*+]|\d+[.)])[ \t]+)[☐✗✘□⬜]/gm, "$1[ ]");
59103
59136
  return result;
59104
59137
  }
59105
59138
  /**
@@ -59107,10 +59140,10 @@ ${plainText}
59107
59140
  * Converts [x], [ ], and other checkbox patterns to emoji
59108
59141
  */
59109
59142
  normalizeDiscordCheckboxes(markdown) {
59110
- let result = markdown.replace(/^([-*]\s+)\[x\]/gmi, "$1\u2611");
59111
- result = result.replace(/^([-*]\s+)[✓✔■⬛]/gm, "$1\u2611");
59112
- result = result.replace(/^([-*]\s+)\[\s?\]/gm, "$1\u2610");
59113
- result = result.replace(/^([-*]\s+)[✗✘□⬜]/gm, "$1\u2610");
59143
+ let result = markdown.replace(/^([ \t]*(?:[-*+]|\d+[.)])[ \t]+)\\?\[x\\?\]/gmi, "$1\u2611");
59144
+ result = result.replace(/^([ \t]*(?:[-*+]|\d+[.)])[ \t]+)[✓✔■⬛]/gm, "$1\u2611");
59145
+ result = result.replace(/^([ \t]*(?:[-*+]|\d+[.)])[ \t]+)\\?\[\s?\\?\]/gm, "$1\u2610");
59146
+ result = result.replace(/^([ \t]*(?:[-*+]|\d+[.)])[ \t]+)[✗✘□⬜]/gm, "$1\u2610");
59114
59147
  return result;
59115
59148
  }
59116
59149
  /**
@@ -59580,6 +59613,11 @@ ${plainText}
59580
59613
  escapeTableCell(content) {
59581
59614
  return content.replace(/\|/g, "\\|").replace(/\s*<br>\s*/g, "<br>").trim();
59582
59615
  }
59616
+ /** A space after a checkbox marker unless the label text already starts with one. */
59617
+ checkboxSpacer(node) {
59618
+ const next = node.nextSibling;
59619
+ return next && /^\s/.test(next.textContent || "") ? "" : " ";
59620
+ }
59583
59621
  addTaskListRules(service) {
59584
59622
  service.addRule("gfmTaskListCheckbox", {
59585
59623
  filter: (node) => {
@@ -59587,7 +59625,7 @@ ${plainText}
59587
59625
  },
59588
59626
  replacement: (_content, node) => {
59589
59627
  const checked = node.checked || node.hasAttribute("checked");
59590
- return checked ? "[x] " : "[ ] ";
59628
+ return (checked ? "[x]" : "[ ]") + this.checkboxSpacer(node);
59591
59629
  }
59592
59630
  });
59593
59631
  }
@@ -59653,60 +59691,78 @@ ${plainText}
59653
59691
  service.addRule("enhancedList", {
59654
59692
  filter: ["ul", "ol"],
59655
59693
  replacement: (content, node) => {
59656
- const listType = node?.nodeName.toLowerCase();
59657
59694
  const items = Array.from(node.children).filter(
59658
59695
  (child) => child.nodeName.toLowerCase() === "li"
59659
59696
  );
59660
59697
  if (items.length === 0) return content;
59661
- let markdown = "\n";
59662
- const isOrdered = listType === "ol";
59663
- const marker = isOrdered ? "1." : service.options.bulletListMarker || "-";
59664
- items.forEach((item, index) => {
59665
- const itemContent = this.processListItem(item, service);
59666
- const actualMarker = isOrdered ? `${index + 1}.` : marker;
59667
- markdown += `${actualMarker} ${itemContent}
59668
- `;
59669
- });
59670
- return markdown + "\n";
59698
+ return "\n" + this.renderList(node, service) + "\n";
59671
59699
  }
59672
59700
  });
59673
59701
  }
59674
- processListItem(item, service, depth = 0) {
59675
- let content = "";
59676
- const TEXT_NODE = 3;
59677
- const ELEMENT_NODE2 = 1;
59678
- Array.from(item.childNodes).forEach((child) => {
59679
- if (child.nodeType === TEXT_NODE) {
59680
- content += child.textContent || "";
59681
- } else if (child.nodeType === ELEMENT_NODE2) {
59682
- const element = child;
59683
- if (element.nodeName.toLowerCase() === "ul" || element.nodeName.toLowerCase() === "ol") {
59684
- const nestedContent = this.processNestedList(element, service, depth + 1);
59685
- content += "\n" + nestedContent;
59686
- } else {
59687
- content += service.turndown(element.outerHTML);
59688
- }
59689
- }
59690
- });
59691
- return content.trim();
59692
- }
59693
- processNestedList(list, service, depth) {
59702
+ /**
59703
+ * Render a list with markers at column 0. Continuation lines of each item
59704
+ * (extra paragraphs, nested lists) are indented to the item's content
59705
+ * column (marker width + 1), which is what CommonMark/GFM require for them
59706
+ * to stay inside the item: 2 spaces under "- ", 3 under "1. ", 4 under "10. ".
59707
+ */
59708
+ renderList(list, service) {
59694
59709
  const items = Array.from(list.children).filter(
59695
59710
  (child) => child.nodeName.toLowerCase() === "li"
59696
59711
  );
59697
- if (items.length === 0) return "";
59698
- let markdown = "";
59699
59712
  const isOrdered = list.nodeName.toLowerCase() === "ol";
59700
- const indent = " ".repeat(depth);
59701
- const marker = isOrdered ? "1." : service.options.bulletListMarker || "-";
59713
+ const bullet = service.options.bulletListMarker || "-";
59714
+ let markdown = "";
59702
59715
  items.forEach((item, index) => {
59703
- const itemContent = this.processListItem(item, service, depth);
59704
- const actualMarker = isOrdered ? `${index + 1}.` : marker;
59705
- markdown += `${indent}${actualMarker} ${itemContent}
59716
+ const marker = isOrdered ? `${index + 1}.` : bullet;
59717
+ const indent = " ".repeat(marker.length + 1);
59718
+ const body = this.processListItem(item, service);
59719
+ const lines = body.split("\n");
59720
+ const indented = lines.map((line, i) => i === 0 || line === "" ? line : indent + line).join("\n");
59721
+ markdown += `${marker} ${indented}
59706
59722
  `;
59707
59723
  });
59708
59724
  return markdown;
59709
59725
  }
59726
+ /**
59727
+ * Render one list item's content relative to its own content column.
59728
+ * Inline runs and block children are converted together, so whitespace
59729
+ * between sibling spans survives (Google Docs emits one span per run) and
59730
+ * multiple paragraphs stay separated.
59731
+ */
59732
+ processListItem(item, service) {
59733
+ const parts = [];
59734
+ const ELEMENT_NODE2 = 1;
59735
+ const doc = item.ownerDocument;
59736
+ let buffer = doc.createElement("div");
59737
+ const flush = () => {
59738
+ if (buffer.childNodes.length > 0) {
59739
+ const md = service.turndown(buffer).trim();
59740
+ if (md) parts.push(md);
59741
+ }
59742
+ buffer = doc.createElement("div");
59743
+ };
59744
+ Array.from(item.childNodes).forEach((child) => {
59745
+ const name = child.nodeType === ELEMENT_NODE2 ? child.nodeName.toLowerCase() : "";
59746
+ if (name === "ul" || name === "ol") {
59747
+ flush();
59748
+ const nested = this.renderList(child, service).replace(/\n+$/, "");
59749
+ if (nested) parts.push(nested);
59750
+ } else {
59751
+ buffer.appendChild(child.cloneNode(true));
59752
+ }
59753
+ });
59754
+ flush();
59755
+ let content = "";
59756
+ parts.forEach((part, i) => {
59757
+ if (i === 0) {
59758
+ content = part;
59759
+ return;
59760
+ }
59761
+ const isList = /^(?:[-*+]|\d+\.) /.test(part);
59762
+ content += (isList ? "\n" : "\n\n") + part;
59763
+ });
59764
+ return content;
59765
+ }
59710
59766
  addSlideMarkerRules(service) {
59711
59767
  service.addRule("pptxSlideMarker", {
59712
59768
  filter: (node) => node.nodeName === "DIV" && node.hasAttribute("data-slide-marker"),
@@ -64775,8 +64831,8 @@ import { readFileSync as readFileSync5 } from "fs";
64775
64831
  import { resolve, dirname as dirname3 } from "path";
64776
64832
  import { fileURLToPath } from "url";
64777
64833
  var getVersion = () => {
64778
- if ("1.0.34") {
64779
- return "1.0.34";
64834
+ if ("1.0.35") {
64835
+ return "1.0.35";
64780
64836
  }
64781
64837
  try {
64782
64838
  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.34",
3
+ "version": "1.0.35",
4
4
  "description": "Format Ferry CLI — Convert HTML, DOCX, PDF, XLSX, CSV, JSON, XML, PPTX to Markdown",
5
5
  "keywords": [
6
6
  "markdown",