formatferry 1.0.8 → 1.0.10

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 +425 -414
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17256,8 +17256,8 @@ var require_turndown_cjs = __commonJS({
17256
17256
  [/_/g, "\\_"],
17257
17257
  [/^(\d+)\. /g, "$1\\. "]
17258
17258
  ];
17259
- function TurndownService2(options) {
17260
- if (!(this instanceof TurndownService2)) return new TurndownService2(options);
17259
+ function TurndownService3(options) {
17260
+ if (!(this instanceof TurndownService3)) return new TurndownService3(options);
17261
17261
  var defaults = {
17262
17262
  rules,
17263
17263
  headingStyle: "setext",
@@ -17284,7 +17284,7 @@ var require_turndown_cjs = __commonJS({
17284
17284
  this.options = extend({}, defaults, options);
17285
17285
  this.rules = new Rules(this.options);
17286
17286
  }
17287
- TurndownService2.prototype = {
17287
+ TurndownService3.prototype = {
17288
17288
  /**
17289
17289
  * The entry point for converting a string or DOM node to Markdown
17290
17290
  * @public
@@ -17405,7 +17405,7 @@ var require_turndown_cjs = __commonJS({
17405
17405
  function canConvert(input) {
17406
17406
  return input != null && (typeof input === "string" || input.nodeType && (input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11));
17407
17407
  }
17408
- module.exports = TurndownService2;
17408
+ module.exports = TurndownService3;
17409
17409
  }
17410
17410
  });
17411
17411
 
@@ -19490,7 +19490,7 @@ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "
19490
19490
  import { extname } from "path";
19491
19491
 
19492
19492
  // ../../src/shared/conversion-engine.ts
19493
- var import_turndown = __toESM(require_turndown_cjs(), 1);
19493
+ var import_turndown2 = __toESM(require_turndown_cjs(), 1);
19494
19494
 
19495
19495
  // ../../src/shared/input-sanitizer.ts
19496
19496
  import DOMPurifyImport from "dompurify";
@@ -19978,6 +19978,357 @@ function normalizeAtlassianHTML(html, domParser) {
19978
19978
  return doc.body.innerHTML;
19979
19979
  }
19980
19980
 
19981
+ // ../../src/shared/confluence-slack-services.ts
19982
+ var import_turndown = __toESM(require_turndown_cjs(), 1);
19983
+ function createSlackService(deps) {
19984
+ const service = new import_turndown.default({
19985
+ headingStyle: "atx",
19986
+ hr: "---",
19987
+ bulletListMarker: "-",
19988
+ codeBlockStyle: "fenced",
19989
+ fence: "```",
19990
+ emDelimiter: "_"
19991
+ });
19992
+ deps.addListRules(service);
19993
+ deps.addPdfPageMarkerRules(service);
19994
+ service.addRule("slackBold", {
19995
+ filter: (node) => {
19996
+ if (node.nodeName === "STRONG" || node.nodeName === "B") return true;
19997
+ if (node.nodeName === "SPAN") {
19998
+ const style = node.getAttribute("style") || "";
19999
+ if (style.includes("font-weight: bold") || style.includes("font-weight:bold")) return true;
20000
+ const fwMatch = style.match(/font-weight:\s*(\d+)/);
20001
+ if (fwMatch && parseInt(fwMatch[1]) >= 600) return true;
20002
+ const ffMatch = style.match(/font-family:\s*['"]*([^'"]+)['"]*[;]?/);
20003
+ if (ffMatch && ffMatch[1].toLowerCase().includes("bold")) return true;
20004
+ }
20005
+ return false;
20006
+ },
20007
+ replacement: (content) => content.trim() ? `*${content}*` : content
20008
+ });
20009
+ service.addRule("slackItalic", {
20010
+ filter: (node) => {
20011
+ if (node.nodeName === "EM" || node.nodeName === "I") return true;
20012
+ if (node.nodeName === "SPAN") {
20013
+ const style = node.getAttribute("style") || "";
20014
+ if (style.includes("font-style: italic") || style.includes("font-style:italic")) return true;
20015
+ const ffMatch = style.match(/font-family:\s*['"]*([^'"]+)['"]*[;]?/);
20016
+ if (ffMatch && ffMatch[1].toLowerCase().includes("italic")) return true;
20017
+ }
20018
+ return false;
20019
+ },
20020
+ replacement: (content) => content.trim() ? `_${content}_` : content
20021
+ });
20022
+ service.addRule("slackStrikethrough", {
20023
+ filter: (node) => {
20024
+ if (["DEL", "S", "STRIKE"].includes(node.nodeName)) return true;
20025
+ if (node.nodeName === "SPAN") {
20026
+ const style = node.getAttribute("style") || "";
20027
+ 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");
20028
+ }
20029
+ return false;
20030
+ },
20031
+ replacement: (content) => content.trim() ? `~${content}~` : content
20032
+ });
20033
+ service.addRule("slackMonospaceCode", {
20034
+ filter: (node) => {
20035
+ if (node.nodeName !== "SPAN") return false;
20036
+ const style = node.getAttribute("style") || "";
20037
+ const fontFamily = style.toLowerCase();
20038
+ return fontFamily.includes("monospace") || fontFamily.includes("courier") || fontFamily.includes("consolas") || fontFamily.includes("monaco") || fontFamily.includes("menlo") || fontFamily.includes("source code") || fontFamily.includes("fira code") || fontFamily.includes("roboto mono") || fontFamily.includes("ubuntu mono");
20039
+ },
20040
+ replacement: (content) => {
20041
+ const trimmed = content.trim();
20042
+ if (!trimmed) return content;
20043
+ return trimmed.includes("`") ? `\`\` ${trimmed} \`\`` : `\`${trimmed}\``;
20044
+ }
20045
+ });
20046
+ service.addRule("slackHeading", {
20047
+ filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
20048
+ replacement: (content) => content.trim() ? `*${content.trim()}*
20049
+
20050
+ ` : content
20051
+ });
20052
+ service.addRule("slackGoogleDocsHeadings", {
20053
+ filter: (node) => {
20054
+ if (node.nodeName !== "P" && node.nodeName !== "DIV") return false;
20055
+ const style = node.getAttribute("style") || "";
20056
+ const fontSizeMatch = style.match(/font-size:\s*(\d+(?:\.\d+)?)px/);
20057
+ if (fontSizeMatch && parseFloat(fontSizeMatch[1]) > 16) return true;
20058
+ if (style.includes("font-weight: bold") || style.includes("font-weight:bold")) {
20059
+ const textContent = node.textContent || "";
20060
+ return textContent.length < 100 && textContent.trim().length > 0;
20061
+ }
20062
+ return false;
20063
+ },
20064
+ replacement: (content) => content.trim() ? `*${content.trim()}*
20065
+
20066
+ ` : content
20067
+ });
20068
+ service.addRule("slackUnderline", {
20069
+ filter: (node) => {
20070
+ if (node.nodeName === "U" || node.nodeName === "INS") return true;
20071
+ if (node.nodeName === "SPAN") {
20072
+ const style = node.getAttribute("style") || "";
20073
+ return style.includes("text-decoration: underline") || style.includes("text-decoration:underline");
20074
+ }
20075
+ return false;
20076
+ },
20077
+ replacement: (content) => content
20078
+ });
20079
+ service.addRule("slackLink", {
20080
+ filter: (node) => node.nodeName === "A" && !!node.getAttribute("href"),
20081
+ replacement: (content, node) => {
20082
+ const href = node.getAttribute("href") || "";
20083
+ const text = content.trim().replace(/\|/g, " ");
20084
+ if (!text || text === href) return `<${href}>`;
20085
+ return `<${href}|${text}>`;
20086
+ }
20087
+ });
20088
+ service.addRule("slackImage", {
20089
+ filter: "img",
20090
+ replacement: (_content, node) => {
20091
+ const src = node.getAttribute("src") || "";
20092
+ const alt = (node.getAttribute("alt") || "image").replace(/\|/g, " ");
20093
+ return `<${src}|${alt}>`;
20094
+ }
20095
+ });
20096
+ service.addRule("slackTable", {
20097
+ filter: "table",
20098
+ replacement: (_content, node) => {
20099
+ const plainText = deps.tableToPlainText(node);
20100
+ return `\`\`\`
20101
+ ${plainText}
20102
+ \`\`\`
20103
+
20104
+ `;
20105
+ }
20106
+ });
20107
+ return service;
20108
+ }
20109
+ function processConfluenceList(list, service, depth) {
20110
+ const isOrdered = list.nodeName === "OL";
20111
+ const marker = (isOrdered ? "#" : "*").repeat(depth);
20112
+ const items = Array.from(list.children).filter((c) => c.nodeName === "LI");
20113
+ let result = depth === 1 ? "\n" : "";
20114
+ items.forEach((item) => {
20115
+ const itemContent = processConfluenceListItem(item, service, depth);
20116
+ result += `${marker} ${itemContent}
20117
+ `;
20118
+ });
20119
+ return depth === 1 ? result + "\n" : result;
20120
+ }
20121
+ function processConfluenceListItem(item, service, depth) {
20122
+ let content = "";
20123
+ const TEXT_NODE = 3;
20124
+ const ELEMENT_NODE = 1;
20125
+ Array.from(item.childNodes).forEach((child) => {
20126
+ if (child.nodeType === TEXT_NODE) {
20127
+ content += child.textContent || "";
20128
+ } else if (child.nodeType === ELEMENT_NODE) {
20129
+ const el = child;
20130
+ if (el.nodeName === "UL" || el.nodeName === "OL") {
20131
+ content += "\n" + processConfluenceList(el, service, depth + 1);
20132
+ } else {
20133
+ content += service.turndown(el.outerHTML);
20134
+ }
20135
+ }
20136
+ });
20137
+ return content.trim();
20138
+ }
20139
+ function createConfluenceService(deps) {
20140
+ const service = new import_turndown.default({
20141
+ headingStyle: "atx",
20142
+ hr: "---",
20143
+ bulletListMarker: "-",
20144
+ codeBlockStyle: "fenced",
20145
+ emDelimiter: "_"
20146
+ });
20147
+ deps.addPdfPageMarkerRules(service);
20148
+ service.addRule("confluenceHeading", {
20149
+ filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
20150
+ replacement: (content, node) => {
20151
+ const level = parseInt(node.nodeName[1], 10);
20152
+ return `
20153
+
20154
+ h${level}. ${content.trim()}
20155
+
20156
+ `;
20157
+ }
20158
+ });
20159
+ service.addRule("confluenceCodeBlock", {
20160
+ filter: "pre",
20161
+ replacement: (_content, node) => {
20162
+ const codeEl = node.querySelector("code");
20163
+ const langMatch = codeEl?.className?.match(/language-(\w+)/);
20164
+ const lang = langMatch ? langMatch[1] : "";
20165
+ const codeContent = codeEl?.textContent || node.textContent || "";
20166
+ const langAttr = lang ? `:language=${lang}` : "";
20167
+ return `
20168
+
20169
+ {code${langAttr}}
20170
+ ${codeContent}
20171
+ {code}
20172
+
20173
+ `;
20174
+ }
20175
+ });
20176
+ service.addRule("confluenceInlineCode", {
20177
+ filter: (node) => node.nodeName === "CODE" && node.parentNode?.nodeName !== "PRE",
20178
+ replacement: (content) => content ? `{{${content.replace(/}+/g, (m) => m.split("").join(" "))}}}` : ""
20179
+ });
20180
+ service.addRule("confluenceBold", {
20181
+ filter: (node) => {
20182
+ if (node.nodeName === "STRONG" || node.nodeName === "B") return true;
20183
+ if (node.nodeName === "SPAN") {
20184
+ const style = node.getAttribute("style") || "";
20185
+ if (style.includes("font-weight: bold") || style.includes("font-weight:bold")) return true;
20186
+ const fwMatch = style.match(/font-weight:\s*(\d+)/);
20187
+ if (fwMatch && parseInt(fwMatch[1]) >= 600) return true;
20188
+ const ffMatch = style.match(/font-family:\s*['"]*([^'"]+)['"]*[;]?/);
20189
+ if (ffMatch && ffMatch[1].toLowerCase().includes("bold")) return true;
20190
+ }
20191
+ return false;
20192
+ },
20193
+ replacement: (content) => content.trim() ? `*${content}*` : content
20194
+ });
20195
+ service.addRule("confluenceItalic", {
20196
+ filter: (node) => {
20197
+ if (node.nodeName === "EM" || node.nodeName === "I") return true;
20198
+ if (node.nodeName === "SPAN") {
20199
+ const style = node.getAttribute("style") || "";
20200
+ if (style.includes("font-style: italic") || style.includes("font-style:italic")) return true;
20201
+ const ffMatch = style.match(/font-family:\s*['"]*([^'"]+)['"]*[;]?/);
20202
+ if (ffMatch && ffMatch[1].toLowerCase().includes("italic")) return true;
20203
+ }
20204
+ return false;
20205
+ },
20206
+ replacement: (content) => content.trim() ? `_${content}_` : content
20207
+ });
20208
+ service.addRule("confluenceUnderline", {
20209
+ filter: (node) => {
20210
+ if (node.nodeName === "U" || node.nodeName === "INS") return true;
20211
+ if (node.nodeName === "SPAN") {
20212
+ const style = node.getAttribute("style") || "";
20213
+ return style.includes("text-decoration: underline") || style.includes("text-decoration:underline");
20214
+ }
20215
+ return false;
20216
+ },
20217
+ replacement: (content) => content.trim() ? `+${content}+` : content
20218
+ });
20219
+ service.addRule("confluenceStrikethrough", {
20220
+ filter: (node) => {
20221
+ if (["DEL", "S", "STRIKE"].includes(node.nodeName)) return true;
20222
+ if (node.nodeName === "SPAN") {
20223
+ const style = node.getAttribute("style") || "";
20224
+ 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");
20225
+ }
20226
+ return false;
20227
+ },
20228
+ replacement: (content) => content.trim() ? `-${content}-` : content
20229
+ });
20230
+ service.addRule("confluenceMonospaceCode", {
20231
+ filter: (node) => {
20232
+ if (node.nodeName !== "SPAN") return false;
20233
+ const style = node.getAttribute("style") || "";
20234
+ const fontFamily = style.toLowerCase();
20235
+ return fontFamily.includes("monospace") || fontFamily.includes("courier") || fontFamily.includes("consolas") || fontFamily.includes("monaco") || fontFamily.includes("menlo") || fontFamily.includes("source code") || fontFamily.includes("fira code") || fontFamily.includes("roboto mono") || fontFamily.includes("ubuntu mono");
20236
+ },
20237
+ replacement: (content) => content.trim() ? `{{${content.trim().replace(/}+/g, (m) => m.split("").join(" "))}}}` : content
20238
+ });
20239
+ service.addRule("confluenceLink", {
20240
+ filter: (node) => node.nodeName === "A" && !!node.getAttribute("href"),
20241
+ replacement: (content, node) => {
20242
+ const href = node.getAttribute("href") || "";
20243
+ const text = content.trim().replace(/\|/g, " ");
20244
+ if (!text || text === href) return `[${href}]`;
20245
+ return `[${text}|${href}]`;
20246
+ }
20247
+ });
20248
+ service.addRule("confluenceImage", {
20249
+ filter: "img",
20250
+ replacement: (_content, node) => {
20251
+ const src = node.getAttribute("src") || "";
20252
+ return src ? `!${src}!` : "";
20253
+ }
20254
+ });
20255
+ service.addRule("confluenceHr", {
20256
+ filter: "hr",
20257
+ replacement: () => "\n\n----\n\n"
20258
+ });
20259
+ service.addRule("confluenceBlockquote", {
20260
+ filter: "blockquote",
20261
+ replacement: (content) => `
20262
+
20263
+ {quote}
20264
+ ${content.trim()}
20265
+ {quote}
20266
+
20267
+ `
20268
+ });
20269
+ service.addRule("confluenceGoogleDocsHeadings", {
20270
+ filter: (node) => {
20271
+ if (node.nodeName !== "P" && node.nodeName !== "DIV") return false;
20272
+ const style = node.getAttribute("style") || "";
20273
+ const fontSizeMatch = style.match(/font-size:\s*(\d+(?:\.\d+)?)px/);
20274
+ if (fontSizeMatch && parseFloat(fontSizeMatch[1]) > 16) return true;
20275
+ if (style.includes("font-weight: bold") || style.includes("font-weight:bold")) {
20276
+ const textContent = node.textContent || "";
20277
+ return textContent.length < 100 && textContent.trim().length > 0;
20278
+ }
20279
+ return false;
20280
+ },
20281
+ replacement: (content, node) => {
20282
+ const style = node?.getAttribute("style") || "";
20283
+ const fontSizeMatch = style.match(/font-size:\s*(\d+(?:\.\d+)?)px/);
20284
+ const trimmed = content.trim();
20285
+ if (!trimmed) return content;
20286
+ if (!fontSizeMatch) {
20287
+ return `
20288
+
20289
+ *${trimmed}*
20290
+
20291
+ `;
20292
+ }
20293
+ const fontSize = parseFloat(fontSizeMatch[1]);
20294
+ let level = 5;
20295
+ if (fontSize >= 28) level = 1;
20296
+ else if (fontSize >= 24) level = 2;
20297
+ else if (fontSize >= 20) level = 3;
20298
+ else if (fontSize >= 18) level = 4;
20299
+ return `
20300
+
20301
+ h${level}. ${trimmed}
20302
+
20303
+ `;
20304
+ }
20305
+ });
20306
+ service.addRule("confluenceList", {
20307
+ filter: ["ul", "ol"],
20308
+ replacement: (_content, node) => {
20309
+ return processConfluenceList(node, service, 1);
20310
+ }
20311
+ });
20312
+ service.addRule("confluenceTable", {
20313
+ filter: "table",
20314
+ replacement: (_content, node) => {
20315
+ const table = node;
20316
+ const rows = Array.from(table.querySelectorAll("tr"));
20317
+ if (rows.length === 0) return "";
20318
+ let result = "\n";
20319
+ rows.forEach((row) => {
20320
+ const cells = Array.from(row.querySelectorAll("th, td"));
20321
+ const isHeaderRow = cells.some((c) => c.nodeName === "TH");
20322
+ const delim = isHeaderRow ? "||" : "|";
20323
+ const cellTexts = cells.map((c) => (c.textContent || "").trim());
20324
+ result += delim + cellTexts.join(delim) + delim + "\n";
20325
+ });
20326
+ return result + "\n";
20327
+ }
20328
+ });
20329
+ return service;
20330
+ }
20331
+
19981
20332
  // ../../src/shared/conversion-engine.ts
19982
20333
  var ConversionEngine = class {
19983
20334
  githubService;
@@ -19992,21 +20343,21 @@ var ConversionEngine = class {
19992
20343
  this.initializeServices();
19993
20344
  }
19994
20345
  initializeServices() {
19995
- this.githubService = new import_turndown.default({
20346
+ this.githubService = new import_turndown2.default({
19996
20347
  headingStyle: "atx",
19997
20348
  hr: "---",
19998
20349
  bulletListMarker: "-",
19999
20350
  codeBlockStyle: "fenced",
20000
20351
  emDelimiter: "_"
20001
20352
  });
20002
- this.commonmarkService = new import_turndown.default({
20353
+ this.commonmarkService = new import_turndown2.default({
20003
20354
  headingStyle: "atx",
20004
20355
  hr: "***",
20005
20356
  bulletListMarker: "*",
20006
20357
  codeBlockStyle: "indented",
20007
20358
  emDelimiter: "*"
20008
20359
  });
20009
- this.customService = new import_turndown.default({
20360
+ this.customService = new import_turndown2.default({
20010
20361
  headingStyle: "atx",
20011
20362
  hr: "---",
20012
20363
  bulletListMarker: "-",
@@ -20028,8 +20379,13 @@ var ConversionEngine = class {
20028
20379
  this.discordService = this.createDiscordService();
20029
20380
  this.redditService = this.createRedditService();
20030
20381
  this.rmarkdownService = this.createRMarkdownService();
20031
- this.slackService = this.createSlackService();
20032
- this.confluenceService = this.createConfluenceService();
20382
+ const deps = {
20383
+ addListRules: this.addListRules.bind(this),
20384
+ addPdfPageMarkerRules: this.addPdfPageMarkerRules.bind(this),
20385
+ tableToPlainText: this.tableToPlainText.bind(this)
20386
+ };
20387
+ this.slackService = createSlackService(deps);
20388
+ this.confluenceService = createConfluenceService(deps);
20033
20389
  }
20034
20390
  /**
20035
20391
  * Create Discord-optimized Turndown service
@@ -20039,7 +20395,7 @@ var ConversionEngine = class {
20039
20395
  * - Images converted to links
20040
20396
  */
20041
20397
  createDiscordService() {
20042
- const service = new import_turndown.default({
20398
+ const service = new import_turndown2.default({
20043
20399
  headingStyle: "atx",
20044
20400
  hr: "---",
20045
20401
  bulletListMarker: "-",
@@ -20216,7 +20572,7 @@ ${plainText}
20216
20572
  * - Fenced code blocks (new Reddit)
20217
20573
  */
20218
20574
  createRedditService() {
20219
- const service = new import_turndown.default({
20575
+ const service = new import_turndown2.default({
20220
20576
  headingStyle: "atx",
20221
20577
  hr: "---",
20222
20578
  bulletListMarker: "-",
@@ -20274,7 +20630,7 @@ ${plainText}
20274
20630
  * - Math expression preservation
20275
20631
  */
20276
20632
  createRMarkdownService() {
20277
- const service = new import_turndown.default({
20633
+ const service = new import_turndown2.default({
20278
20634
  headingStyle: "atx",
20279
20635
  hr: "---",
20280
20636
  bulletListMarker: "-",
@@ -20337,377 +20693,6 @@ ${plainText}
20337
20693
  });
20338
20694
  return service;
20339
20695
  }
20340
- /**
20341
- * Create Slack mrkdwn-optimized Turndown service
20342
- * - Bold uses *text* (single asterisk, not **)
20343
- * - Italic uses _text_
20344
- * - Strikethrough uses ~text~ (single tilde)
20345
- * - Links use <url|text> format
20346
- * - Headers converted to bold (Slack has no heading support)
20347
- * - Tables converted to code blocks
20348
- */
20349
- createSlackService() {
20350
- const service = new import_turndown.default({
20351
- headingStyle: "atx",
20352
- hr: "---",
20353
- bulletListMarker: "-",
20354
- codeBlockStyle: "fenced",
20355
- fence: "```",
20356
- emDelimiter: "_"
20357
- });
20358
- this.addListRules(service);
20359
- this.addPdfPageMarkerRules(service);
20360
- service.addRule("slackBold", {
20361
- filter: (node) => {
20362
- if (node.nodeName === "STRONG" || node.nodeName === "B") return true;
20363
- if (node.nodeName === "SPAN") {
20364
- const style = node.getAttribute("style") || "";
20365
- if (style.includes("font-weight: bold") || style.includes("font-weight:bold")) return true;
20366
- const fwMatch = style.match(/font-weight:\s*(\d+)/);
20367
- if (fwMatch && parseInt(fwMatch[1]) >= 600) return true;
20368
- const ffMatch = style.match(/font-family:\s*['"]*([^'"]+)['"]*[;]?/);
20369
- if (ffMatch && ffMatch[1].toLowerCase().includes("bold")) return true;
20370
- }
20371
- return false;
20372
- },
20373
- replacement: (content) => content.trim() ? `*${content}*` : content
20374
- });
20375
- service.addRule("slackItalic", {
20376
- filter: (node) => {
20377
- if (node.nodeName === "EM" || node.nodeName === "I") return true;
20378
- if (node.nodeName === "SPAN") {
20379
- const style = node.getAttribute("style") || "";
20380
- if (style.includes("font-style: italic") || style.includes("font-style:italic")) return true;
20381
- const ffMatch = style.match(/font-family:\s*['"]*([^'"]+)['"]*[;]?/);
20382
- if (ffMatch && ffMatch[1].toLowerCase().includes("italic")) return true;
20383
- }
20384
- return false;
20385
- },
20386
- replacement: (content) => content.trim() ? `_${content}_` : content
20387
- });
20388
- service.addRule("slackStrikethrough", {
20389
- filter: (node) => {
20390
- if (["DEL", "S", "STRIKE"].includes(node.nodeName)) return true;
20391
- if (node.nodeName === "SPAN") {
20392
- const style = node.getAttribute("style") || "";
20393
- 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");
20394
- }
20395
- return false;
20396
- },
20397
- replacement: (content) => content.trim() ? `~${content}~` : content
20398
- });
20399
- service.addRule("slackMonospaceCode", {
20400
- filter: (node) => {
20401
- if (node.nodeName !== "SPAN") return false;
20402
- const style = node.getAttribute("style") || "";
20403
- const fontFamily = style.toLowerCase();
20404
- return fontFamily.includes("monospace") || fontFamily.includes("courier") || fontFamily.includes("consolas") || fontFamily.includes("monaco") || fontFamily.includes("menlo") || fontFamily.includes("source code") || fontFamily.includes("fira code") || fontFamily.includes("roboto mono") || fontFamily.includes("ubuntu mono");
20405
- },
20406
- replacement: (content) => {
20407
- const trimmed = content.trim();
20408
- if (!trimmed) return content;
20409
- return trimmed.includes("`") ? `\`\` ${trimmed} \`\`` : `\`${trimmed}\``;
20410
- }
20411
- });
20412
- service.addRule("slackHeading", {
20413
- filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
20414
- replacement: (content) => content.trim() ? `*${content.trim()}*
20415
-
20416
- ` : content
20417
- });
20418
- service.addRule("slackGoogleDocsHeadings", {
20419
- filter: (node) => {
20420
- if (node.nodeName !== "P" && node.nodeName !== "DIV") return false;
20421
- const style = node.getAttribute("style") || "";
20422
- const fontSizeMatch = style.match(/font-size:\s*(\d+(?:\.\d+)?)px/);
20423
- if (fontSizeMatch && parseFloat(fontSizeMatch[1]) > 16) return true;
20424
- if (style.includes("font-weight: bold") || style.includes("font-weight:bold")) {
20425
- const textContent = node.textContent || "";
20426
- return textContent.length < 100 && textContent.trim().length > 0;
20427
- }
20428
- return false;
20429
- },
20430
- replacement: (content) => content.trim() ? `*${content.trim()}*
20431
-
20432
- ` : content
20433
- });
20434
- service.addRule("slackUnderline", {
20435
- filter: (node) => {
20436
- if (node.nodeName === "U" || node.nodeName === "INS") return true;
20437
- if (node.nodeName === "SPAN") {
20438
- const style = node.getAttribute("style") || "";
20439
- return style.includes("text-decoration: underline") || style.includes("text-decoration:underline");
20440
- }
20441
- return false;
20442
- },
20443
- replacement: (content) => content
20444
- });
20445
- service.addRule("slackLink", {
20446
- filter: (node) => node.nodeName === "A" && !!node.getAttribute("href"),
20447
- replacement: (content, node) => {
20448
- const href = node.getAttribute("href") || "";
20449
- const text = content.trim().replace(/\|/g, " ");
20450
- if (!text || text === href) return `<${href}>`;
20451
- return `<${href}|${text}>`;
20452
- }
20453
- });
20454
- service.addRule("slackImage", {
20455
- filter: "img",
20456
- replacement: (_content, node) => {
20457
- const src = node.getAttribute("src") || "";
20458
- const alt = (node.getAttribute("alt") || "image").replace(/\|/g, " ");
20459
- return `<${src}|${alt}>`;
20460
- }
20461
- });
20462
- service.addRule("slackTable", {
20463
- filter: "table",
20464
- replacement: (_content, node) => {
20465
- const plainText = this.tableToPlainText(node);
20466
- return `\`\`\`
20467
- ${plainText}
20468
- \`\`\`
20469
-
20470
- `;
20471
- }
20472
- });
20473
- return service;
20474
- }
20475
- /**
20476
- * Create Confluence wiki markup Turndown service
20477
- * - Headings: h1. h2. etc.
20478
- * - Bold: *text*, Italic: _text_, Underline: +text+, Strikethrough: -text-
20479
- * - Inline code: {{text}}, Code blocks: {code:language=x}...{code}
20480
- * - Links: [text|url], Images: !url!
20481
- * - Tables: ||header|| / |cell| format
20482
- * - Lists: depth-aware * and # notation
20483
- * - Blockquote: {quote}...{quote}
20484
- */
20485
- createConfluenceService() {
20486
- const service = new import_turndown.default({
20487
- headingStyle: "atx",
20488
- hr: "---",
20489
- bulletListMarker: "-",
20490
- codeBlockStyle: "fenced",
20491
- emDelimiter: "_"
20492
- });
20493
- this.addPdfPageMarkerRules(service);
20494
- service.addRule("confluenceHeading", {
20495
- filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
20496
- replacement: (content, node) => {
20497
- const level = parseInt(node.nodeName[1], 10);
20498
- return `
20499
-
20500
- h${level}. ${content.trim()}
20501
-
20502
- `;
20503
- }
20504
- });
20505
- service.addRule("confluenceCodeBlock", {
20506
- filter: "pre",
20507
- replacement: (_content, node) => {
20508
- const codeEl = node.querySelector("code");
20509
- const langMatch = codeEl?.className?.match(/language-(\w+)/);
20510
- const lang = langMatch ? langMatch[1] : "";
20511
- const codeContent = codeEl?.textContent || node.textContent || "";
20512
- const langAttr = lang ? `:language=${lang}` : "";
20513
- return `
20514
-
20515
- {code${langAttr}}
20516
- ${codeContent}
20517
- {code}
20518
-
20519
- `;
20520
- }
20521
- });
20522
- service.addRule("confluenceInlineCode", {
20523
- filter: (node) => node.nodeName === "CODE" && node.parentNode?.nodeName !== "PRE",
20524
- replacement: (content) => content ? `{{${content.replace(/}+/g, (m) => m.split("").join(" "))}}}` : ""
20525
- });
20526
- service.addRule("confluenceBold", {
20527
- filter: (node) => {
20528
- if (node.nodeName === "STRONG" || node.nodeName === "B") return true;
20529
- if (node.nodeName === "SPAN") {
20530
- const style = node.getAttribute("style") || "";
20531
- if (style.includes("font-weight: bold") || style.includes("font-weight:bold")) return true;
20532
- const fwMatch = style.match(/font-weight:\s*(\d+)/);
20533
- if (fwMatch && parseInt(fwMatch[1]) >= 600) return true;
20534
- const ffMatch = style.match(/font-family:\s*['"]*([^'"]+)['"]*[;]?/);
20535
- if (ffMatch && ffMatch[1].toLowerCase().includes("bold")) return true;
20536
- }
20537
- return false;
20538
- },
20539
- replacement: (content) => content.trim() ? `*${content}*` : content
20540
- });
20541
- service.addRule("confluenceItalic", {
20542
- filter: (node) => {
20543
- if (node.nodeName === "EM" || node.nodeName === "I") return true;
20544
- if (node.nodeName === "SPAN") {
20545
- const style = node.getAttribute("style") || "";
20546
- if (style.includes("font-style: italic") || style.includes("font-style:italic")) return true;
20547
- const ffMatch = style.match(/font-family:\s*['"]*([^'"]+)['"]*[;]?/);
20548
- if (ffMatch && ffMatch[1].toLowerCase().includes("italic")) return true;
20549
- }
20550
- return false;
20551
- },
20552
- replacement: (content) => content.trim() ? `_${content}_` : content
20553
- });
20554
- service.addRule("confluenceUnderline", {
20555
- filter: (node) => {
20556
- if (node.nodeName === "U" || node.nodeName === "INS") return true;
20557
- if (node.nodeName === "SPAN") {
20558
- const style = node.getAttribute("style") || "";
20559
- return style.includes("text-decoration: underline") || style.includes("text-decoration:underline");
20560
- }
20561
- return false;
20562
- },
20563
- replacement: (content) => content.trim() ? `+${content}+` : content
20564
- });
20565
- service.addRule("confluenceStrikethrough", {
20566
- filter: (node) => {
20567
- if (["DEL", "S", "STRIKE"].includes(node.nodeName)) return true;
20568
- if (node.nodeName === "SPAN") {
20569
- const style = node.getAttribute("style") || "";
20570
- 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");
20571
- }
20572
- return false;
20573
- },
20574
- replacement: (content) => content.trim() ? `-${content}-` : content
20575
- });
20576
- service.addRule("confluenceMonospaceCode", {
20577
- filter: (node) => {
20578
- if (node.nodeName !== "SPAN") return false;
20579
- const style = node.getAttribute("style") || "";
20580
- const fontFamily = style.toLowerCase();
20581
- return fontFamily.includes("monospace") || fontFamily.includes("courier") || fontFamily.includes("consolas") || fontFamily.includes("monaco") || fontFamily.includes("menlo") || fontFamily.includes("source code") || fontFamily.includes("fira code") || fontFamily.includes("roboto mono") || fontFamily.includes("ubuntu mono");
20582
- },
20583
- replacement: (content) => content.trim() ? `{{${content.trim().replace(/}+/g, (m) => m.split("").join(" "))}}}` : content
20584
- });
20585
- service.addRule("confluenceLink", {
20586
- filter: (node) => node.nodeName === "A" && !!node.getAttribute("href"),
20587
- replacement: (content, node) => {
20588
- const href = node.getAttribute("href") || "";
20589
- const text = content.trim().replace(/\|/g, " ");
20590
- if (!text || text === href) return `[${href}]`;
20591
- return `[${text}|${href}]`;
20592
- }
20593
- });
20594
- service.addRule("confluenceImage", {
20595
- filter: "img",
20596
- replacement: (_content, node) => {
20597
- const src = node.getAttribute("src") || "";
20598
- return src ? `!${src}!` : "";
20599
- }
20600
- });
20601
- service.addRule("confluenceHr", {
20602
- filter: "hr",
20603
- replacement: () => "\n\n----\n\n"
20604
- });
20605
- service.addRule("confluenceBlockquote", {
20606
- filter: "blockquote",
20607
- replacement: (content) => `
20608
-
20609
- {quote}
20610
- ${content.trim()}
20611
- {quote}
20612
-
20613
- `
20614
- });
20615
- service.addRule("confluenceGoogleDocsHeadings", {
20616
- filter: (node) => {
20617
- if (node.nodeName !== "P" && node.nodeName !== "DIV") return false;
20618
- const style = node.getAttribute("style") || "";
20619
- const fontSizeMatch = style.match(/font-size:\s*(\d+(?:\.\d+)?)px/);
20620
- if (fontSizeMatch && parseFloat(fontSizeMatch[1]) > 16) return true;
20621
- if (style.includes("font-weight: bold") || style.includes("font-weight:bold")) {
20622
- const textContent = node.textContent || "";
20623
- return textContent.length < 100 && textContent.trim().length > 0;
20624
- }
20625
- return false;
20626
- },
20627
- replacement: (content, node) => {
20628
- const style = node?.getAttribute("style") || "";
20629
- const fontSizeMatch = style.match(/font-size:\s*(\d+(?:\.\d+)?)px/);
20630
- const trimmed = content.trim();
20631
- if (!trimmed) return content;
20632
- if (!fontSizeMatch) {
20633
- return `
20634
-
20635
- *${trimmed}*
20636
-
20637
- `;
20638
- }
20639
- const fontSize = parseFloat(fontSizeMatch[1]);
20640
- let level = 5;
20641
- if (fontSize >= 28) level = 1;
20642
- else if (fontSize >= 24) level = 2;
20643
- else if (fontSize >= 20) level = 3;
20644
- else if (fontSize >= 18) level = 4;
20645
- return `
20646
-
20647
- h${level}. ${trimmed}
20648
-
20649
- `;
20650
- }
20651
- });
20652
- service.addRule("confluenceList", {
20653
- filter: ["ul", "ol"],
20654
- replacement: (_content, node) => {
20655
- return this.processConfluenceList(node, service, 1);
20656
- }
20657
- });
20658
- service.addRule("confluenceTable", {
20659
- filter: "table",
20660
- replacement: (_content, node) => {
20661
- const table = node;
20662
- const rows = Array.from(table.querySelectorAll("tr"));
20663
- if (rows.length === 0) return "";
20664
- let result = "\n";
20665
- rows.forEach((row) => {
20666
- const cells = Array.from(row.querySelectorAll("th, td"));
20667
- const isHeaderRow = cells.some((c) => c.nodeName === "TH");
20668
- const delim = isHeaderRow ? "||" : "|";
20669
- const cellTexts = cells.map((c) => (c.textContent || "").trim());
20670
- result += delim + cellTexts.join(delim) + delim + "\n";
20671
- });
20672
- return result + "\n";
20673
- }
20674
- });
20675
- return service;
20676
- }
20677
- /**
20678
- * Process a Confluence list element into depth-aware wiki markup.
20679
- * Uses * for unordered and # for ordered, repeated for nesting depth.
20680
- */
20681
- processConfluenceList(list, service, depth) {
20682
- const isOrdered = list.nodeName === "OL";
20683
- const marker = (isOrdered ? "#" : "*").repeat(depth);
20684
- const items = Array.from(list.children).filter((c) => c.nodeName === "LI");
20685
- let result = depth === 1 ? "\n" : "";
20686
- items.forEach((item) => {
20687
- const itemContent = this.processConfluenceListItem(item, service, depth);
20688
- result += `${marker} ${itemContent}
20689
- `;
20690
- });
20691
- return depth === 1 ? result + "\n" : result;
20692
- }
20693
- processConfluenceListItem(item, service, depth) {
20694
- let content = "";
20695
- const TEXT_NODE = 3;
20696
- const ELEMENT_NODE = 1;
20697
- Array.from(item.childNodes).forEach((child) => {
20698
- if (child.nodeType === TEXT_NODE) {
20699
- content += child.textContent || "";
20700
- } else if (child.nodeType === ELEMENT_NODE) {
20701
- const el = child;
20702
- if (el.nodeName === "UL" || el.nodeName === "OL") {
20703
- content += "\n" + this.processConfluenceList(el, service, depth + 1);
20704
- } else {
20705
- content += service.turndown(el.outerHTML);
20706
- }
20707
- }
20708
- });
20709
- return content.trim();
20710
- }
20711
20696
  /**
20712
20697
  * Convert HTML table to plain text for Discord code blocks
20713
20698
  */
@@ -21122,37 +21107,51 @@ async function loadLibrary(format) {
21122
21107
  }
21123
21108
  }
21124
21109
  async function importMammoth() {
21125
- return import("mammoth");
21110
+ const mod = await import("mammoth");
21111
+ const modDefault = mod.default;
21112
+ if (mod && !mod.convertToHtml && modDefault && modDefault.convertToHtml) {
21113
+ return mod.default;
21114
+ }
21115
+ return mod;
21126
21116
  }
21127
21117
  async function importXlsx() {
21128
- return import("xlsx/dist/xlsx.mini.min.js");
21118
+ const mod = await import("xlsx");
21119
+ const modDefault = mod.default;
21120
+ if (mod && !mod.read && modDefault && modDefault.read) {
21121
+ return mod.default;
21122
+ }
21123
+ return mod;
21129
21124
  }
21130
21125
  async function importPdfjs() {
21131
21126
  if (typeof process !== "undefined") {
21132
- const proxyProcess = process;
21133
- if (typeof proxyProcess.getBuiltinModule !== "function") {
21134
- let requireWorker;
21127
+ const p = process;
21128
+ if (typeof p.getBuiltinModule !== "function") {
21135
21129
  try {
21136
- const moduleName = "module";
21137
- const mod = await import(
21138
- /* @vite-ignore */
21139
- moduleName
21140
- );
21141
- if (mod && typeof mod.createRequire === "function") {
21142
- requireWorker = mod.createRequire(import.meta.url);
21130
+ const mod2 = await import("node:module");
21131
+ const createRequire = mod2.createRequire;
21132
+ if (typeof createRequire === "function") {
21133
+ const req = createRequire(process.argv?.[1] || import.meta.url);
21134
+ p.getBuiltinModule = (name) => {
21135
+ try {
21136
+ return req(name);
21137
+ } catch {
21138
+ return void 0;
21139
+ }
21140
+ };
21141
+ } else {
21142
+ p.getBuiltinModule = () => void 0;
21143
21143
  }
21144
21144
  } catch {
21145
+ p.getBuiltinModule = () => void 0;
21145
21146
  }
21146
- proxyProcess.getBuiltinModule = (name) => {
21147
- try {
21148
- return requireWorker ? requireWorker(name) : void 0;
21149
- } catch {
21150
- return void 0;
21151
- }
21152
- };
21153
21147
  }
21154
21148
  }
21155
- return import("pdfjs-dist/legacy/build/pdf.mjs");
21149
+ const mod = await import("pdfjs-dist/legacy/build/pdf.mjs");
21150
+ const modDefault = mod.default;
21151
+ if (mod && !mod.getDocument && modDefault && modDefault.getDocument) {
21152
+ return mod.default;
21153
+ }
21154
+ return mod;
21156
21155
  }
21157
21156
  async function loadLibraryInternal(format) {
21158
21157
  try {
@@ -21969,6 +21968,9 @@ function readConfig() {
21969
21968
  return null;
21970
21969
  }
21971
21970
  }
21971
+ function getApiKey() {
21972
+ return process.env.FORMATFERRY_API_KEY || readConfig()?.apiKey;
21973
+ }
21972
21974
  function writeConfig(config) {
21973
21975
  mkdirSync(dirname(CONFIG_PATH), { recursive: true });
21974
21976
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
@@ -22731,11 +22733,10 @@ async function convertCommand(options) {
22731
22733
  process.stderr.write("Error: --url and --input are mutually exclusive. Use one or the other.\n");
22732
22734
  process.exit(1);
22733
22735
  }
22734
- const config = readConfig();
22735
- const apiKey = config?.apiKey;
22736
+ const apiKey = getApiKey();
22736
22737
  if (!apiKey) {
22737
22738
  process.stderr.write(
22738
- "Error: No SaaS API key configured. URL extraction requires a subscription.\nSet your API key with: formatferry auth --license-key <KEY>\n"
22739
+ "Error: No SaaS API key configured. URL extraction requires a subscription.\nSet your API key with: formatferry auth --api-key <KEY>\nOr via the FORMATFERRY_API_KEY environment variable.\n"
22739
22740
  );
22740
22741
  process.exit(1);
22741
22742
  }
@@ -23074,15 +23075,20 @@ async function handleLicenseKey(key) {
23074
23075
  }
23075
23076
  }
23076
23077
  function handleStatus() {
23078
+ const envKey = process.env.FORMATFERRY_API_KEY;
23077
23079
  const config = readConfig();
23078
- if (!config || !config.licenseKey && !config.apiKey) {
23080
+ if (envKey) {
23081
+ process.stdout.write(`Using API Key from FORMATFERRY_API_KEY environment variable.
23082
+ `);
23083
+ }
23084
+ if ((!config || !config.licenseKey && !config.apiKey) && !envKey) {
23079
23085
  process.stdout.write(`Tier: free
23080
23086
  `);
23081
23087
  process.stdout.write(`No license key or API key configured.
23082
23088
  `);
23083
23089
  return;
23084
23090
  }
23085
- if (config.apiKey) {
23091
+ if (config?.apiKey) {
23086
23092
  const plan = config.plan ?? "free";
23087
23093
  process.stdout.write(`Plan: ${formatPlanName(plan)}
23088
23094
  `);
@@ -23104,9 +23110,14 @@ function handleStatus() {
23104
23110
  }
23105
23111
  return;
23106
23112
  }
23107
- const tier = config.tier ?? "free";
23108
- const features = config.features ?? [];
23109
- const expiresAt = config.expiresAt ?? null;
23113
+ if (!config && envKey) {
23114
+ process.stdout.write(`Plan: API key configured via environment variable
23115
+ `);
23116
+ return;
23117
+ }
23118
+ const tier = config?.tier ?? "free";
23119
+ const features = config?.features ?? [];
23120
+ const expiresAt = config?.expiresAt ?? null;
23110
23121
  process.stdout.write(`Tier: ${tier}
23111
23122
  `);
23112
23123
  if (expiresAt) {
@@ -23129,8 +23140,8 @@ import { readFileSync as readFileSync4 } from "fs";
23129
23140
  import { resolve, dirname as dirname3 } from "path";
23130
23141
  import { fileURLToPath } from "url";
23131
23142
  var getVersion = () => {
23132
- if ("1.0.8") {
23133
- return "1.0.8";
23143
+ if ("1.0.10") {
23144
+ return "1.0.10";
23134
23145
  }
23135
23146
  try {
23136
23147
  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.8",
3
+ "version": "1.0.10",
4
4
  "description": "FormatFerry CLI — Convert HTML, DOCX, PDF, XLSX, CSV to Markdown",
5
5
  "keywords": [
6
6
  "markdown",