formatferry 1.0.35 → 1.0.37

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 +229 -151
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -58515,6 +58515,49 @@ function normalizeAtlassianHTML(html, domParser) {
58515
58515
 
58516
58516
  // ../../src/shared/confluence-slack-services.ts
58517
58517
  var import_turndown = __toESM(require_turndown_cjs(), 1);
58518
+
58519
+ // ../../src/shared/text-decoration.ts
58520
+ var DECLARATION = /(?:^|;)\s*text-decoration(?:-line)?\s*:([^;]*)/gi;
58521
+ function styleDecorations(style) {
58522
+ const result = { underline: false, strike: false };
58523
+ if (!style) return result;
58524
+ DECLARATION.lastIndex = 0;
58525
+ let match;
58526
+ while ((match = DECLARATION.exec(style)) !== null) {
58527
+ const value = match[1].toLowerCase();
58528
+ result.underline = /\bunderline\b/.test(value);
58529
+ result.strike = /\bline-through\b/.test(value);
58530
+ }
58531
+ return result;
58532
+ }
58533
+ function nodeStyle(node) {
58534
+ const el = node;
58535
+ return typeof el.getAttribute === "function" ? el.getAttribute("style") || "" : "";
58536
+ }
58537
+ function nodeDecorations(node) {
58538
+ const fromStyle = styleDecorations(nodeStyle(node));
58539
+ const name = node.nodeName;
58540
+ return {
58541
+ underline: fromStyle.underline || name === "U" || name === "INS",
58542
+ strike: fromStyle.strike || name === "DEL" || name === "S" || name === "STRIKE"
58543
+ };
58544
+ }
58545
+ function isUnderlineSpan(node) {
58546
+ return node.nodeName === "SPAN" && styleDecorations(nodeStyle(node)).underline;
58547
+ }
58548
+ function isStrikeSpan(node) {
58549
+ return node.nodeName === "SPAN" && styleDecorations(nodeStyle(node)).strike;
58550
+ }
58551
+ function decorate(content, node, syntax) {
58552
+ if (!content.trim()) return content;
58553
+ const d = nodeDecorations(node);
58554
+ let out = content;
58555
+ if (d.underline && syntax.underline) out = syntax.underline[0] + out + syntax.underline[1];
58556
+ if (d.strike && syntax.strike) out = syntax.strike[0] + out + syntax.strike[1];
58557
+ return out;
58558
+ }
58559
+
58560
+ // ../../src/shared/confluence-slack-services.ts
58518
58561
  function escapeSlackEntities(text) {
58519
58562
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
58520
58563
  }
@@ -58577,13 +58620,9 @@ function createSlackService(deps) {
58577
58620
  service.addRule("slackStrikethrough", {
58578
58621
  filter: (node) => {
58579
58622
  if (["DEL", "S", "STRIKE"].includes(node.nodeName)) return true;
58580
- if (node.nodeName === "SPAN") {
58581
- const style = node.getAttribute("style") || "";
58582
- return style.includes("text-decoration: line-through") || style.includes("text-decoration:line-through") || style.includes("text-decoration-line: line-through") || style.includes("text-decoration-line:line-through");
58583
- }
58584
- return false;
58623
+ return isStrikeSpan(node);
58585
58624
  },
58586
- replacement: (content) => content.trim() ? `~${content}~` : content
58625
+ replacement: (content, node) => decorate(content, node, { underline: null, strike: ["~", "~"] })
58587
58626
  });
58588
58627
  service.addRule("slackMonospaceCode", {
58589
58628
  filter: (node) => {
@@ -58644,13 +58683,10 @@ ${escapeSlackEntities(code)}
58644
58683
  service.addRule("slackUnderline", {
58645
58684
  filter: (node) => {
58646
58685
  if (node.nodeName === "U" || node.nodeName === "INS") return true;
58647
- if (node.nodeName === "SPAN") {
58648
- const style = node.getAttribute("style") || "";
58649
- return style.includes("text-decoration: underline") || style.includes("text-decoration:underline");
58650
- }
58651
- return false;
58686
+ return isUnderlineSpan(node);
58652
58687
  },
58653
- replacement: (content) => content
58688
+ // Slack has no underline; keep a strikethrough set on the same span
58689
+ replacement: (content, node) => decorate(content, node, { underline: null, strike: ["~", "~"] })
58654
58690
  });
58655
58691
  service.addRule("slackLink", {
58656
58692
  filter: (node) => node.nodeName === "A" && !!node.getAttribute("href"),
@@ -58682,34 +58718,38 @@ ${plainText}
58682
58718
  });
58683
58719
  return service;
58684
58720
  }
58685
- function processConfluenceList(list, service, depth) {
58686
- const isOrdered = list.nodeName === "OL";
58687
- const marker = (isOrdered ? "#" : "*").repeat(depth);
58721
+ function processConfluenceList(list, service, parentPrefix) {
58722
+ const prefix = parentPrefix + (list.nodeName === "OL" ? "#" : "*");
58688
58723
  const items = Array.from(list.children).filter((c) => c.nodeName === "LI");
58689
- let result = depth === 1 ? "\n" : "";
58724
+ const isTop = parentPrefix === "";
58725
+ let result = isTop ? "\n" : "";
58690
58726
  items.forEach((item) => {
58691
- const itemContent = processConfluenceListItem(item, service, depth);
58692
- result += `${marker} ${itemContent}
58727
+ const itemContent = processConfluenceListItem(item, service, prefix);
58728
+ result += `${prefix} ${itemContent}
58693
58729
  `;
58694
58730
  });
58695
- return depth === 1 ? result + "\n" : result;
58731
+ return isTop ? result + "\n" : result;
58696
58732
  }
58697
- function processConfluenceListItem(item, service, depth) {
58698
- let content = "";
58699
- const TEXT_NODE = 3;
58733
+ function processConfluenceListItem(item, service, prefix) {
58700
58734
  const ELEMENT_NODE2 = 1;
58735
+ const doc = item.ownerDocument;
58736
+ let content = "";
58737
+ let buffer = doc.createElement("div");
58738
+ const flush = () => {
58739
+ if (buffer.childNodes.length > 0) {
58740
+ content += service.turndown(buffer).replace(/[ \t]*\n(?:[ \t]*\n)+[ \t]*/g, " \\\\ ");
58741
+ }
58742
+ buffer = doc.createElement("div");
58743
+ };
58701
58744
  Array.from(item.childNodes).forEach((child) => {
58702
- if (child.nodeType === TEXT_NODE) {
58703
- content += child.textContent || "";
58704
- } else if (child.nodeType === ELEMENT_NODE2) {
58705
- const el = child;
58706
- if (el.nodeName === "UL" || el.nodeName === "OL") {
58707
- content += "\n" + processConfluenceList(el, service, depth + 1);
58708
- } else {
58709
- content += service.turndown(el.outerHTML);
58710
- }
58745
+ if (child.nodeType === ELEMENT_NODE2 && (child.nodeName === "UL" || child.nodeName === "OL")) {
58746
+ flush();
58747
+ content = content.trimEnd() + "\n" + processConfluenceList(child, service, prefix);
58748
+ } else {
58749
+ buffer.appendChild(child.cloneNode(true));
58711
58750
  }
58712
58751
  });
58752
+ flush();
58713
58753
  return content.trim();
58714
58754
  }
58715
58755
  function createConfluenceService(deps) {
@@ -58785,24 +58825,16 @@ ${codeContent}
58785
58825
  service.addRule("confluenceUnderline", {
58786
58826
  filter: (node) => {
58787
58827
  if (node.nodeName === "U" || node.nodeName === "INS") return true;
58788
- if (node.nodeName === "SPAN") {
58789
- const style = node.getAttribute("style") || "";
58790
- return style.includes("text-decoration: underline") || style.includes("text-decoration:underline");
58791
- }
58792
- return false;
58828
+ return isUnderlineSpan(node);
58793
58829
  },
58794
- replacement: (content) => content.trim() ? `+${content}+` : content
58830
+ replacement: (content, node) => decorate(content, node, { underline: ["+", "+"], strike: ["-", "-"] })
58795
58831
  });
58796
58832
  service.addRule("confluenceStrikethrough", {
58797
58833
  filter: (node) => {
58798
58834
  if (["DEL", "S", "STRIKE"].includes(node.nodeName)) return true;
58799
- if (node.nodeName === "SPAN") {
58800
- const style = node.getAttribute("style") || "";
58801
- return style.includes("text-decoration: line-through") || style.includes("text-decoration:line-through") || style.includes("text-decoration-line: line-through") || style.includes("text-decoration-line:line-through");
58802
- }
58803
- return false;
58835
+ return isStrikeSpan(node);
58804
58836
  },
58805
- replacement: (content) => content.trim() ? `-${content}-` : content
58837
+ replacement: (content, node) => decorate(content, node, { underline: ["+", "+"], strike: ["-", "-"] })
58806
58838
  });
58807
58839
  service.addRule("confluenceMonospaceCode", {
58808
58840
  filter: (node) => {
@@ -58886,7 +58918,7 @@ h${level}. ${trimmed}
58886
58918
  service.addRule("confluenceList", {
58887
58919
  filter: ["ul", "ol"],
58888
58920
  replacement: (_content, node) => {
58889
- return processConfluenceList(node, service, 1);
58921
+ return processConfluenceList(node, service, "");
58890
58922
  }
58891
58923
  });
58892
58924
  service.addRule("confluenceTable", {
@@ -58900,7 +58932,7 @@ h${level}. ${trimmed}
58900
58932
  const cells = Array.from(row.querySelectorAll("th, td"));
58901
58933
  const isHeaderRow = cells.some((c) => c.nodeName === "TH");
58902
58934
  const delim = isHeaderRow ? "||" : "|";
58903
- const cellTexts = cells.map((c) => (c.textContent || "").trim());
58935
+ const cellTexts = cells.map((c) => (c.textContent || "").trim().replace(/\|/g, "\\|"));
58904
58936
  result += delim + cellTexts.join(delim) + delim + "\n";
58905
58937
  });
58906
58938
  return result + "\n";
@@ -58983,7 +59015,7 @@ var ConversionEngine = class {
58983
59015
  emDelimiter: "_"
58984
59016
  });
58985
59017
  this.addGoogleDocsRules(this.githubService);
58986
- this.addGoogleDocsRules(this.commonmarkService);
59018
+ this.addGoogleDocsRules(this.commonmarkService, ["<del>", "</del>"]);
58987
59019
  this.addGoogleDocsRules(this.customService);
58988
59020
  this.addTableRules(this.githubService);
58989
59021
  this.addTableRules(this.commonmarkService);
@@ -58997,6 +59029,13 @@ var ConversionEngine = class {
58997
59029
  this.addSupSubMathRules(this.githubService);
58998
59030
  this.addSupSubMathRules(this.commonmarkService);
58999
59031
  this.addSupSubMathRules(this.customService);
59032
+ this.commonmarkService.addRule("commonmarkStrikethrough", {
59033
+ filter: (node) => {
59034
+ if (["DEL", "S", "STRIKE"].includes(node.nodeName)) return true;
59035
+ return isStrikeSpan(node);
59036
+ },
59037
+ replacement: (content, node) => decorate(content, node, { underline: ["<u>", "</u>"], strike: ["<del>", "</del>"] })
59038
+ });
59000
59039
  this.addPdfPageMarkerRules(this.githubService);
59001
59040
  this.addPdfPageMarkerRules(this.commonmarkService);
59002
59041
  this.addPdfPageMarkerRules(this.customService);
@@ -59067,8 +59106,10 @@ var ConversionEngine = class {
59067
59106
  this.addPdfPageMarkerRules(service);
59068
59107
  this.addSlideMarkerRules(service);
59069
59108
  service.addRule("discordUnderline", {
59070
- filter: ["u", "ins"],
59071
- replacement: (content) => content.trim() ? `__${content}__` : content
59109
+ // Includes Google Docs CSS underline, which the shared rules would
59110
+ // otherwise emit as <u>…</u> (shown literally in Discord)
59111
+ filter: (node) => node.nodeName === "U" || node.nodeName === "INS" || isUnderlineSpan(node),
59112
+ replacement: (content, node) => decorate(content, node, { underline: ["__", "__"], strike: ["~~", "~~"] })
59072
59113
  });
59073
59114
  service.addRule("discordHeadingLimit", {
59074
59115
  filter: ["h4", "h5", "h6"],
@@ -59151,7 +59192,6 @@ ${plainText}
59151
59192
  * Converts Unicode superscript to ^text syntax
59152
59193
  */
59153
59194
  normalizeRedditUnicode(markdown) {
59154
- let result = markdown;
59155
59195
  const superscriptMap = {
59156
59196
  "\u2070": "0",
59157
59197
  "\xB9": "1",
@@ -59166,9 +59206,6 @@ ${plainText}
59166
59206
  "\u207F": "n",
59167
59207
  "\u2071": "i"
59168
59208
  };
59169
- for (const [unicode, digit] of Object.entries(superscriptMap)) {
59170
- result = result.replace(new RegExp(unicode, "g"), `^${digit}`);
59171
- }
59172
59209
  const subscriptMap = {
59173
59210
  "\u2080": "0",
59174
59211
  "\u2081": "1",
@@ -59181,17 +59218,13 @@ ${plainText}
59181
59218
  "\u2088": "8",
59182
59219
  "\u2089": "9"
59183
59220
  };
59184
- for (const [unicode, digit] of Object.entries(subscriptMap)) {
59185
- result = result.replace(new RegExp(unicode, "g"), `[${digit}]`);
59186
- }
59187
- return result;
59221
+ return this.replaceOutsideCode(markdown, (text) => text.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹ⁿⁱ]+/g, (run) => `^(${Array.from(run).map((c) => superscriptMap[c]).join("")})`).replace(/[₀₁₂₃₄₅₆₇₈₉]+/g, (run) => `[${Array.from(run).map((c) => subscriptMap[c]).join("")}]`));
59188
59222
  }
59189
59223
  /**
59190
59224
  * Normalize Unicode for R Markdown format
59191
59225
  * Converts Unicode superscript/subscript to Pandoc syntax
59192
59226
  */
59193
59227
  normalizeRMarkdownUnicode(markdown) {
59194
- let result = markdown;
59195
59228
  const superscriptMap = {
59196
59229
  "\u2070": "0",
59197
59230
  "\xB9": "1",
@@ -59206,9 +59239,6 @@ ${plainText}
59206
59239
  "\u207F": "n",
59207
59240
  "\u2071": "i"
59208
59241
  };
59209
- for (const [unicode, digit] of Object.entries(superscriptMap)) {
59210
- result = result.replace(new RegExp(unicode, "g"), `^${digit}^`);
59211
- }
59212
59242
  const subscriptMap = {
59213
59243
  "\u2080": "0",
59214
59244
  "\u2081": "1",
@@ -59221,10 +59251,7 @@ ${plainText}
59221
59251
  "\u2088": "8",
59222
59252
  "\u2089": "9"
59223
59253
  };
59224
- for (const [unicode, digit] of Object.entries(subscriptMap)) {
59225
- result = result.replace(new RegExp(unicode, "g"), `~${digit}~`);
59226
- }
59227
- return result;
59254
+ return this.replaceOutsideCode(markdown, (text) => text.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹ⁿⁱ]+/g, (run) => `^${Array.from(run).map((c) => superscriptMap[c]).join("")}^`).replace(/[₀₁₂₃₄₅₆₇₈₉]+/g, (run) => `~${Array.from(run).map((c) => subscriptMap[c]).join("")}~`));
59228
59255
  }
59229
59256
  /**
59230
59257
  * Create Reddit-optimized Turndown service
@@ -59232,6 +59259,104 @@ ${plainText}
59232
59259
  * - Subscript to bracketed text
59233
59260
  * - Fenced code blocks (new Reddit)
59234
59261
  */
59262
+ /**
59263
+ * Reddit's bare ^word superscript runs to the next space, so "x^2y" would
59264
+ * superscript "2y". The parenthesised form ^(…) always ends where it should.
59265
+ */
59266
+ redditSuperscript(content) {
59267
+ const trimmed = content.trim();
59268
+ return trimmed ? `^(${trimmed.replace(/\)/g, "\\)")})` : "";
59269
+ }
59270
+ /**
59271
+ * Pandoc ^x^ / ~x~ cannot contain bare spaces (they must be backslash-escaped),
59272
+ * and a bare delimiter inside the content would close the span early.
59273
+ */
59274
+ pandocScript(content, delimiter) {
59275
+ const trimmed = content.trim();
59276
+ if (!trimmed) return "";
59277
+ const escaped = trimmed.replace(delimiter === "^" ? /(?<!\\)\^/g : /(?<!\\)~/g, `\\${delimiter}`).replace(/ /g, "\\ ");
59278
+ return `${delimiter}${escaped}${delimiter}`;
59279
+ }
59280
+ /**
59281
+ * Apply a text transform everywhere except fenced code blocks and inline code spans.
59282
+ *
59283
+ * Fences may use ``` or ~~~ of any length (3+) and may sit behind list
59284
+ * indentation or blockquote markers; a fence closes only on a run of the same
59285
+ * character at least as long as the opener. Inline code spans close on a
59286
+ * backtick run of exactly the opening length (CommonMark), so `` a`b `` is
59287
+ * protected whole.
59288
+ */
59289
+ replaceOutsideCode(markdown, transform) {
59290
+ const fenceOpen = /^((?:[ \t]*>)*[ \t]*(?:(?:[-*+]|\d+[.)])[ \t]+)?[ \t]*)(`{3,}|~{3,})/;
59291
+ const lines = markdown.split(/(?<=\n)/);
59292
+ const segments = [];
59293
+ const push = (text, code) => {
59294
+ const last = segments[segments.length - 1];
59295
+ if (last && last.code === code) last.text += text;
59296
+ else segments.push({ text, code });
59297
+ };
59298
+ let fence = null;
59299
+ for (const line of lines) {
59300
+ if (fence) {
59301
+ push(line, true);
59302
+ const body = line.replace(/^(?:[ \t]*>)*[ \t]*/, "");
59303
+ const close = body.match(/^(`{3,}|~{3,})[ \t]*\r?\n?$/);
59304
+ if (close && close[1][0] === fence.char && close[1].length >= fence.len) fence = null;
59305
+ continue;
59306
+ }
59307
+ const open = line.match(fenceOpen);
59308
+ if (open && !(open[2][0] === "`" && line.slice(open[0].length).includes("`"))) {
59309
+ fence = { char: open[2][0], len: open[2].length };
59310
+ push(line, true);
59311
+ continue;
59312
+ }
59313
+ push(line, false);
59314
+ }
59315
+ const out = [];
59316
+ for (const seg of segments) {
59317
+ if (seg.code) {
59318
+ out.push(seg.text);
59319
+ continue;
59320
+ }
59321
+ const text = seg.text;
59322
+ let plainStart = 0;
59323
+ let i = 0;
59324
+ while (i < text.length) {
59325
+ if (text[i] !== "`") {
59326
+ i++;
59327
+ continue;
59328
+ }
59329
+ let j = i;
59330
+ while (j < text.length && text[j] === "`") j++;
59331
+ const runLen = j - i;
59332
+ let k = j;
59333
+ let closeAt = -1;
59334
+ while (k < text.length) {
59335
+ if (text[k] !== "`") {
59336
+ k++;
59337
+ continue;
59338
+ }
59339
+ let m = k;
59340
+ while (m < text.length && text[m] === "`") m++;
59341
+ if (m - k === runLen) {
59342
+ closeAt = m;
59343
+ break;
59344
+ }
59345
+ k = m;
59346
+ }
59347
+ if (closeAt === -1) {
59348
+ i = j;
59349
+ continue;
59350
+ }
59351
+ out.push(transform(text.slice(plainStart, i)));
59352
+ out.push(text.slice(i, closeAt));
59353
+ plainStart = closeAt;
59354
+ i = closeAt;
59355
+ }
59356
+ out.push(transform(text.slice(plainStart)));
59357
+ }
59358
+ return out.join("");
59359
+ }
59235
59360
  createRedditService() {
59236
59361
  const service = new import_turndown2.default({
59237
59362
  headingStyle: "atx",
@@ -59245,13 +59370,15 @@ ${plainText}
59245
59370
  this.addListRules(service);
59246
59371
  this.addPdfPageMarkerRules(service);
59247
59372
  this.addSlideMarkerRules(service);
59373
+ service.addRule("redditUnderline", {
59374
+ filter: (node) => node.nodeName === "U" || node.nodeName === "INS" || isUnderlineSpan(node),
59375
+ // Keep a strikethrough set on the same span
59376
+ replacement: (content, node) => decorate(content, node, { underline: null, strike: ["~~", "~~"] })
59377
+ });
59378
+ this.addTaskListRules(service);
59248
59379
  service.addRule("redditSuperscript", {
59249
59380
  filter: (node) => node.nodeName === "SUP",
59250
- replacement: (content) => {
59251
- const trimmed = content.trim();
59252
- if (!trimmed) return "";
59253
- return trimmed.includes(" ") ? `^(${trimmed})` : `^${trimmed}`;
59254
- }
59381
+ replacement: (content) => this.redditSuperscript(content)
59255
59382
  });
59256
59383
  service.addRule("redditSuperscriptStyle", {
59257
59384
  filter: (node) => {
@@ -59259,11 +59386,7 @@ ${plainText}
59259
59386
  const style = node.getAttribute("style") || "";
59260
59387
  return style.includes("vertical-align: super") || style.includes("vertical-align:super");
59261
59388
  },
59262
- replacement: (content) => {
59263
- const trimmed = content.trim();
59264
- if (!trimmed) return "";
59265
- return trimmed.includes(" ") ? `^(${trimmed})` : `^${trimmed}`;
59266
- }
59389
+ replacement: (content) => this.redditSuperscript(content)
59267
59390
  });
59268
59391
  service.addRule("redditSubscript", {
59269
59392
  filter: (node) => node.nodeName === "SUB",
@@ -59304,12 +59427,10 @@ ${plainText}
59304
59427
  this.addListRules(service);
59305
59428
  this.addPdfPageMarkerRules(service);
59306
59429
  this.addSlideMarkerRules(service);
59430
+ this.addTaskListRules(service);
59307
59431
  service.addRule("rmdSuperscript", {
59308
59432
  filter: (node) => node.nodeName === "SUP",
59309
- replacement: (content) => {
59310
- const trimmed = content.trim();
59311
- return trimmed ? `^${trimmed}^` : "";
59312
- }
59433
+ replacement: (content) => this.pandocScript(content, "^")
59313
59434
  });
59314
59435
  service.addRule("rmdSuperscriptStyle", {
59315
59436
  filter: (node) => {
@@ -59317,17 +59438,11 @@ ${plainText}
59317
59438
  const style = node.getAttribute("style") || "";
59318
59439
  return style.includes("vertical-align: super") || style.includes("vertical-align:super");
59319
59440
  },
59320
- replacement: (content) => {
59321
- const trimmed = content.trim();
59322
- return trimmed ? `^${trimmed}^` : "";
59323
- }
59441
+ replacement: (content) => this.pandocScript(content, "^")
59324
59442
  });
59325
59443
  service.addRule("rmdSubscript", {
59326
59444
  filter: (node) => node.nodeName === "SUB",
59327
- replacement: (content) => {
59328
- const trimmed = content.trim();
59329
- return trimmed ? `~${trimmed}~` : "";
59330
- }
59445
+ replacement: (content) => this.pandocScript(content, "~")
59331
59446
  });
59332
59447
  service.addRule("rmdSubscriptStyle", {
59333
59448
  filter: (node) => {
@@ -59335,10 +59450,7 @@ ${plainText}
59335
59450
  const style = node.getAttribute("style") || "";
59336
59451
  return style.includes("vertical-align: sub") || style.includes("vertical-align:sub");
59337
59452
  },
59338
- replacement: (content) => {
59339
- const trimmed = content.trim();
59340
- return trimmed ? `~${trimmed}~` : "";
59341
- }
59453
+ replacement: (content) => this.pandocScript(content, "~")
59342
59454
  });
59343
59455
  service.addRule("rmdMath", {
59344
59456
  filter: (node) => {
@@ -59398,7 +59510,7 @@ ${plainText}
59398
59510
  });
59399
59511
  return doc.body.firstElementChild?.innerHTML ?? html;
59400
59512
  }
59401
- addGoogleDocsRules(service) {
59513
+ addGoogleDocsRules(service, strike = ["~~", "~~"]) {
59402
59514
  service.addRule("googleDocsBold", {
59403
59515
  filter: (node) => {
59404
59516
  if (node.nodeName !== "SPAN") return false;
@@ -59468,16 +59580,9 @@ ${plainText}
59468
59580
  }
59469
59581
  });
59470
59582
  service.addRule("googleDocsUnderline", {
59471
- filter: (node) => {
59472
- if (node.nodeName === "SPAN") {
59473
- const style = node.getAttribute("style") || "";
59474
- return style.includes("text-decoration: underline") || style.includes("text-decoration:underline");
59475
- }
59476
- return false;
59477
- },
59478
- replacement: (content) => {
59479
- return content.trim() ? `<u>${content}</u>` : content;
59480
- }
59583
+ filter: (node) => isUnderlineSpan(node),
59584
+ // Also applies a line-through set on the same span
59585
+ replacement: (content, node) => decorate(content, node, { underline: ["<u>", "</u>"], strike })
59481
59586
  });
59482
59587
  service.addRule("strikethroughTags", {
59483
59588
  filter: ["del", "s"],
@@ -59492,14 +59597,9 @@ ${plainText}
59492
59597
  }
59493
59598
  });
59494
59599
  service.addRule("strikethroughStyle", {
59495
- filter: (node) => {
59496
- if (node.nodeName !== "SPAN") return false;
59497
- const style = node.getAttribute("style") || "";
59498
- return style.includes("text-decoration: line-through") || style.includes("text-decoration:line-through") || style.includes("text-decoration-line: line-through") || style.includes("text-decoration-line:line-through");
59499
- },
59500
- replacement: (content) => {
59501
- return content.trim() ? `~~${content}~~` : content;
59502
- }
59600
+ filter: (node) => isStrikeSpan(node),
59601
+ // Also applies an underline set on the same span
59602
+ replacement: (content, node) => decorate(content, node, { underline: ["<u>", "</u>"], strike })
59503
59603
  });
59504
59604
  service.addRule("monospaceCode", {
59505
59605
  filter: (node) => {
@@ -59630,48 +59730,26 @@ ${plainText}
59630
59730
  });
59631
59731
  }
59632
59732
  /**
59633
- * FF-IMP-004: Add superscript, subscript, and math span rules for
59634
- * github/commonmark flavours. Uses the same Pandoc-style syntax as
59635
- * rmarkdown (^text^ for sup, ~text~ for sub, $math$ for inline math)
59636
- * which GitHub renders correctly in Markdown files.
59733
+ * FF-IMP-004: Superscript, subscript and math span rules for the
59734
+ * github/commonmark/gdocs flavours. Neither GFM nor CommonMark has
59735
+ * superscript/subscript syntax: Pandoc-style ^x^ shows literally, and GitHub
59736
+ * renders ~x~ as strikethrough. Both allow inline HTML and GitHub keeps
59737
+ * <sup>/<sub>, so those are emitted instead.
59637
59738
  */
59638
59739
  addSupSubMathRules(service) {
59639
- service.addRule("githubSuperscript", {
59640
- filter: (node) => node.nodeName === "SUP",
59641
- replacement: (content) => {
59642
- const trimmed = content.trim();
59643
- return trimmed ? `^${trimmed}^` : "";
59644
- }
59645
- });
59646
- service.addRule("githubSuperscriptStyle", {
59647
- filter: (node) => {
59648
- if (node.nodeName !== "SPAN") return false;
59649
- const style = node.getAttribute("style") || "";
59650
- return style.includes("vertical-align: super") || style.includes("vertical-align:super");
59651
- },
59652
- replacement: (content) => {
59653
- const trimmed = content.trim();
59654
- return trimmed ? `^${trimmed}^` : "";
59655
- }
59656
- });
59657
- service.addRule("githubSubscript", {
59658
- filter: (node) => node.nodeName === "SUB",
59659
- replacement: (content) => {
59660
- const trimmed = content.trim();
59661
- return trimmed ? `~${trimmed}~` : "";
59662
- }
59663
- });
59664
- service.addRule("githubSubscriptStyle", {
59665
- filter: (node) => {
59666
- if (node.nodeName !== "SPAN") return false;
59667
- const style = node.getAttribute("style") || "";
59668
- return style.includes("vertical-align: sub") || style.includes("vertical-align:sub");
59669
- },
59670
- replacement: (content) => {
59671
- const trimmed = content.trim();
59672
- return trimmed ? `~${trimmed}~` : "";
59673
- }
59674
- });
59740
+ const isStyle = (node, value) => {
59741
+ if (node.nodeName !== "SPAN") return false;
59742
+ const style = node.getAttribute("style") || "";
59743
+ return style.includes(`vertical-align: ${value}`) || style.includes(`vertical-align:${value}`);
59744
+ };
59745
+ const wrap = (tag) => (content) => {
59746
+ const trimmed = content.trim();
59747
+ return trimmed ? `<${tag}>${trimmed}</${tag}>` : "";
59748
+ };
59749
+ service.addRule("githubSuperscript", { filter: (node) => node.nodeName === "SUP", replacement: wrap("sup") });
59750
+ service.addRule("githubSuperscriptStyle", { filter: (node) => isStyle(node, "super"), replacement: wrap("sup") });
59751
+ service.addRule("githubSubscript", { filter: (node) => node.nodeName === "SUB", replacement: wrap("sub") });
59752
+ service.addRule("githubSubscriptStyle", { filter: (node) => isStyle(node, "sub"), replacement: wrap("sub") });
59675
59753
  service.addRule("githubMath", {
59676
59754
  filter: (node) => {
59677
59755
  if (node.nodeName !== "SPAN") return false;
@@ -64831,8 +64909,8 @@ import { readFileSync as readFileSync5 } from "fs";
64831
64909
  import { resolve, dirname as dirname3 } from "path";
64832
64910
  import { fileURLToPath } from "url";
64833
64911
  var getVersion = () => {
64834
- if ("1.0.35") {
64835
- return "1.0.35";
64912
+ if ("1.0.37") {
64913
+ return "1.0.37";
64836
64914
  }
64837
64915
  try {
64838
64916
  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.35",
3
+ "version": "1.0.37",
4
4
  "description": "Format Ferry CLI — Convert HTML, DOCX, PDF, XLSX, CSV, JSON, XML, PPTX to Markdown",
5
5
  "keywords": [
6
6
  "markdown",