formatferry 1.0.2 → 1.0.3
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.
- package/dist/index.js +4 -5
- package/package.json +1 -1
- package/dist/commands/auth.d.ts +0 -17
- package/dist/commands/auth.d.ts.map +0 -1
- package/dist/commands/auth.js +0 -102
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -10
- package/dist/commands/batch.d.ts.map +0 -1
- package/dist/commands/batch.js +0 -92
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/convert.d.ts +0 -26
- package/dist/commands/convert.d.ts.map +0 -1
- package/dist/commands/convert.js +0 -198
- package/dist/commands/convert.js.map +0 -1
- package/dist/config-store.d.ts +0 -27
- package/dist/config-store.d.ts.map +0 -1
- package/dist/config-store.js +0 -58
- package/dist/config-store.js.map +0 -1
- package/dist/entitlement.d.ts +0 -11
- package/dist/entitlement.d.ts.map +0 -1
- package/dist/entitlement.js +0 -91
- package/dist/entitlement.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/packages/cli/src/commands/auth.d.ts +0 -17
- package/dist/packages/cli/src/commands/auth.d.ts.map +0 -1
- package/dist/packages/cli/src/commands/auth.js +0 -102
- package/dist/packages/cli/src/commands/auth.js.map +0 -1
- package/dist/packages/cli/src/commands/batch.d.ts +0 -10
- package/dist/packages/cli/src/commands/batch.d.ts.map +0 -1
- package/dist/packages/cli/src/commands/batch.js +0 -92
- package/dist/packages/cli/src/commands/batch.js.map +0 -1
- package/dist/packages/cli/src/commands/convert.d.ts +0 -26
- package/dist/packages/cli/src/commands/convert.d.ts.map +0 -1
- package/dist/packages/cli/src/commands/convert.js +0 -198
- package/dist/packages/cli/src/commands/convert.js.map +0 -1
- package/dist/packages/cli/src/config-store.d.ts +0 -27
- package/dist/packages/cli/src/config-store.d.ts.map +0 -1
- package/dist/packages/cli/src/config-store.js +0 -58
- package/dist/packages/cli/src/config-store.js.map +0 -1
- package/dist/packages/cli/src/entitlement.d.ts +0 -11
- package/dist/packages/cli/src/entitlement.d.ts.map +0 -1
- package/dist/packages/cli/src/entitlement.js +0 -91
- package/dist/packages/cli/src/entitlement.js.map +0 -1
- package/dist/packages/cli/src/index.d.ts +0 -2
- package/dist/packages/cli/src/index.d.ts.map +0 -1
- package/dist/packages/cli/src/index.js +0 -28
- package/dist/packages/cli/src/index.js.map +0 -1
- package/dist/packages/cli/src/shim.d.ts +0 -2
- package/dist/packages/cli/src/shim.d.ts.map +0 -1
- package/dist/packages/cli/src/shim.js +0 -21
- package/dist/packages/cli/src/shim.js.map +0 -1
- package/dist/shim.d.ts +0 -2
- package/dist/shim.d.ts.map +0 -1
- package/dist/shim.js +0 -21
- package/dist/shim.js.map +0 -1
- package/dist/src/shared/adapters/docx-adapter.d.ts +0 -85
- package/dist/src/shared/adapters/docx-adapter.d.ts.map +0 -1
- package/dist/src/shared/adapters/docx-adapter.js +0 -164
- package/dist/src/shared/adapters/docx-adapter.js.map +0 -1
- package/dist/src/shared/adapters/pdf-adapter.d.ts +0 -94
- package/dist/src/shared/adapters/pdf-adapter.d.ts.map +0 -1
- package/dist/src/shared/adapters/pdf-adapter.js +0 -322
- package/dist/src/shared/adapters/pdf-adapter.js.map +0 -1
- package/dist/src/shared/adapters/spreadsheet-adapter.d.ts +0 -98
- package/dist/src/shared/adapters/spreadsheet-adapter.d.ts.map +0 -1
- package/dist/src/shared/adapters/spreadsheet-adapter.js +0 -424
- package/dist/src/shared/adapters/spreadsheet-adapter.js.map +0 -1
- package/dist/src/shared/atlassian-detector.d.ts +0 -6
- package/dist/src/shared/atlassian-detector.d.ts.map +0 -1
- package/dist/src/shared/atlassian-detector.js +0 -23
- package/dist/src/shared/atlassian-detector.js.map +0 -1
- package/dist/src/shared/conversion-engine.d.ts +0 -114
- package/dist/src/shared/conversion-engine.d.ts.map +0 -1
- package/dist/src/shared/conversion-engine.js +0 -790
- package/dist/src/shared/conversion-engine.js.map +0 -1
- package/dist/src/shared/entitlement-service.d.ts +0 -42
- package/dist/src/shared/entitlement-service.d.ts.map +0 -1
- package/dist/src/shared/entitlement-service.js +0 -38
- package/dist/src/shared/entitlement-service.js.map +0 -1
- package/dist/src/shared/input-sanitizer.d.ts +0 -93
- package/dist/src/shared/input-sanitizer.d.ts.map +0 -1
- package/dist/src/shared/input-sanitizer.js +0 -527
- package/dist/src/shared/input-sanitizer.js.map +0 -1
|
@@ -1,790 +0,0 @@
|
|
|
1
|
-
import TurndownService from 'turndown';
|
|
2
|
-
import { sanitizeHTML } from '../shared/input-sanitizer';
|
|
3
|
-
export class ConversionEngine {
|
|
4
|
-
githubService;
|
|
5
|
-
commonmarkService;
|
|
6
|
-
customService;
|
|
7
|
-
discordService;
|
|
8
|
-
redditService;
|
|
9
|
-
rmarkdownService;
|
|
10
|
-
constructor() {
|
|
11
|
-
this.initializeServices();
|
|
12
|
-
}
|
|
13
|
-
initializeServices() {
|
|
14
|
-
// GitHub Flavored Markdown configuration
|
|
15
|
-
this.githubService = new TurndownService({
|
|
16
|
-
headingStyle: 'atx',
|
|
17
|
-
hr: '---',
|
|
18
|
-
bulletListMarker: '-',
|
|
19
|
-
codeBlockStyle: 'fenced',
|
|
20
|
-
emDelimiter: '_',
|
|
21
|
-
});
|
|
22
|
-
// CommonMark configuration (more strict)
|
|
23
|
-
this.commonmarkService = new TurndownService({
|
|
24
|
-
headingStyle: 'atx',
|
|
25
|
-
hr: '***',
|
|
26
|
-
bulletListMarker: '*',
|
|
27
|
-
codeBlockStyle: 'indented',
|
|
28
|
-
emDelimiter: '*',
|
|
29
|
-
});
|
|
30
|
-
// Custom service (starts as GitHub, can be modified)
|
|
31
|
-
this.customService = new TurndownService({
|
|
32
|
-
headingStyle: 'atx',
|
|
33
|
-
hr: '---',
|
|
34
|
-
bulletListMarker: '-',
|
|
35
|
-
codeBlockStyle: 'fenced',
|
|
36
|
-
emDelimiter: '_',
|
|
37
|
-
});
|
|
38
|
-
// Add enhanced Google Docs formatting detection to all services
|
|
39
|
-
this.addGoogleDocsRules(this.githubService);
|
|
40
|
-
this.addGoogleDocsRules(this.commonmarkService);
|
|
41
|
-
this.addGoogleDocsRules(this.customService);
|
|
42
|
-
// Add table preservation rules
|
|
43
|
-
this.addTableRules(this.githubService);
|
|
44
|
-
this.addTableRules(this.commonmarkService);
|
|
45
|
-
this.addTableRules(this.customService);
|
|
46
|
-
// Add enhanced list rules
|
|
47
|
-
this.addListRules(this.githubService);
|
|
48
|
-
this.addListRules(this.commonmarkService);
|
|
49
|
-
this.addListRules(this.customService);
|
|
50
|
-
// Add PDF page marker rules (Req 3.5)
|
|
51
|
-
this.addPdfPageMarkerRules(this.githubService);
|
|
52
|
-
this.addPdfPageMarkerRules(this.commonmarkService);
|
|
53
|
-
this.addPdfPageMarkerRules(this.customService);
|
|
54
|
-
// Initialize platform-specific services
|
|
55
|
-
this.discordService = this.createDiscordService();
|
|
56
|
-
this.redditService = this.createRedditService();
|
|
57
|
-
this.rmarkdownService = this.createRMarkdownService();
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Create Discord-optimized Turndown service
|
|
61
|
-
* - Underline support (__text__)
|
|
62
|
-
* - Heading limit (H1-H3 only)
|
|
63
|
-
* - Tables converted to code blocks
|
|
64
|
-
* - Images converted to links
|
|
65
|
-
*/
|
|
66
|
-
createDiscordService() {
|
|
67
|
-
const service = new TurndownService({
|
|
68
|
-
headingStyle: 'atx',
|
|
69
|
-
hr: '---',
|
|
70
|
-
bulletListMarker: '-',
|
|
71
|
-
codeBlockStyle: 'fenced',
|
|
72
|
-
emDelimiter: '*',
|
|
73
|
-
});
|
|
74
|
-
// Apply shared rules
|
|
75
|
-
this.addGoogleDocsRules(service);
|
|
76
|
-
this.addListRules(service);
|
|
77
|
-
this.addPdfPageMarkerRules(service);
|
|
78
|
-
// Discord-specific: Underline support
|
|
79
|
-
service.addRule('discordUnderline', {
|
|
80
|
-
filter: ['u', 'ins'],
|
|
81
|
-
replacement: (content) => content.trim() ? `__${content}__` : content
|
|
82
|
-
});
|
|
83
|
-
// Discord-specific: Limit headings to H3
|
|
84
|
-
service.addRule('discordHeadingLimit', {
|
|
85
|
-
filter: ['h4', 'h5', 'h6'],
|
|
86
|
-
replacement: (content) => `### ${content.trim()}\n\n`
|
|
87
|
-
});
|
|
88
|
-
// Discord-specific: Tables to code blocks
|
|
89
|
-
service.addRule('discordTable', {
|
|
90
|
-
filter: 'table',
|
|
91
|
-
replacement: (_content, node) => {
|
|
92
|
-
const plainText = this.tableToPlainText(node);
|
|
93
|
-
return `\`\`\`\n${plainText}\n\`\`\`\n\n`;
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
// Discord-specific: Images to links
|
|
97
|
-
service.addRule('discordImage', {
|
|
98
|
-
filter: 'img',
|
|
99
|
-
replacement: (_content, node) => {
|
|
100
|
-
const element = node;
|
|
101
|
-
const src = element.getAttribute('src') || '';
|
|
102
|
-
const alt = element.getAttribute('alt') || 'image';
|
|
103
|
-
return `[${alt}](${src})`;
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
// Discord-specific: Task list checkboxes to emoji (HTML input)
|
|
107
|
-
service.addRule('discordTaskList', {
|
|
108
|
-
filter: (node) => {
|
|
109
|
-
return node.nodeName === 'INPUT' &&
|
|
110
|
-
node.type === 'checkbox';
|
|
111
|
-
},
|
|
112
|
-
replacement: (_content, node) => {
|
|
113
|
-
const checked = node.checked;
|
|
114
|
-
return checked ? '☑ ' : '☐ ';
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
return service;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Apply format-specific post-processing for Unicode characters
|
|
121
|
-
* Handles checkboxes, superscript, and subscript Unicode characters
|
|
122
|
-
*/
|
|
123
|
-
applyFormatPostProcessing(markdown, format) {
|
|
124
|
-
switch (format) {
|
|
125
|
-
case 'github':
|
|
126
|
-
return this.normalizeGitHubCheckboxes(markdown);
|
|
127
|
-
case 'discord':
|
|
128
|
-
return this.normalizeDiscordCheckboxes(markdown);
|
|
129
|
-
case 'reddit':
|
|
130
|
-
return this.normalizeRedditUnicode(markdown);
|
|
131
|
-
case 'rmarkdown':
|
|
132
|
-
return this.normalizeRMarkdownUnicode(markdown);
|
|
133
|
-
default:
|
|
134
|
-
return markdown;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Normalize checkbox patterns for GitHub task list syntax
|
|
139
|
-
* Converts Unicode checkboxes to [x] and [ ]
|
|
140
|
-
*/
|
|
141
|
-
normalizeGitHubCheckboxes(markdown) {
|
|
142
|
-
let result = markdown;
|
|
143
|
-
// Convert checked Unicode checkboxes to [x]
|
|
144
|
-
result = result.replace(/^([-*]\s+)[☑✓✔■⬛]/gm, '$1[x]');
|
|
145
|
-
// Convert unchecked Unicode checkboxes to [ ]
|
|
146
|
-
result = result.replace(/^([-*]\s+)[☐✗✘□⬜]/gm, '$1[ ]');
|
|
147
|
-
return result;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Normalize checkbox patterns in markdown output for Discord
|
|
151
|
-
* Converts [x], [ ], and other checkbox patterns to emoji
|
|
152
|
-
*/
|
|
153
|
-
normalizeDiscordCheckboxes(markdown) {
|
|
154
|
-
// Normalize checked checkbox patterns to ☑
|
|
155
|
-
let result = markdown.replace(/^([-*]\s+)\[x\]/gmi, '$1☑');
|
|
156
|
-
result = result.replace(/^([-*]\s+)[✓✔■⬛]/gm, '$1☑');
|
|
157
|
-
// Normalize unchecked checkbox patterns to ☐
|
|
158
|
-
result = result.replace(/^([-*]\s+)\[\s?\]/gm, '$1☐');
|
|
159
|
-
result = result.replace(/^([-*]\s+)[✗✘□⬜]/gm, '$1☐');
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Normalize Unicode for Reddit format
|
|
164
|
-
* Converts Unicode superscript to ^text syntax
|
|
165
|
-
*/
|
|
166
|
-
normalizeRedditUnicode(markdown) {
|
|
167
|
-
let result = markdown;
|
|
168
|
-
// Convert Unicode superscript digits to Reddit syntax
|
|
169
|
-
const superscriptMap = {
|
|
170
|
-
'⁰': '0', '¹': '1', '²': '2', '³': '3', '⁴': '4',
|
|
171
|
-
'⁵': '5', '⁶': '6', '⁷': '7', '⁸': '8', '⁹': '9',
|
|
172
|
-
'ⁿ': 'n', 'ⁱ': 'i'
|
|
173
|
-
};
|
|
174
|
-
// Replace Unicode superscripts with ^n format
|
|
175
|
-
for (const [unicode, digit] of Object.entries(superscriptMap)) {
|
|
176
|
-
result = result.replace(new RegExp(unicode, 'g'), `^${digit}`);
|
|
177
|
-
}
|
|
178
|
-
// Convert Unicode subscript digits to [n] format
|
|
179
|
-
const subscriptMap = {
|
|
180
|
-
'₀': '0', '₁': '1', '₂': '2', '₃': '3', '₄': '4',
|
|
181
|
-
'₅': '5', '₆': '6', '₇': '7', '₈': '8', '₉': '9'
|
|
182
|
-
};
|
|
183
|
-
for (const [unicode, digit] of Object.entries(subscriptMap)) {
|
|
184
|
-
result = result.replace(new RegExp(unicode, 'g'), `[${digit}]`);
|
|
185
|
-
}
|
|
186
|
-
return result;
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Normalize Unicode for R Markdown format
|
|
190
|
-
* Converts Unicode superscript/subscript to Pandoc syntax
|
|
191
|
-
*/
|
|
192
|
-
normalizeRMarkdownUnicode(markdown) {
|
|
193
|
-
let result = markdown;
|
|
194
|
-
// Convert Unicode superscript digits to Pandoc ^n^ format
|
|
195
|
-
const superscriptMap = {
|
|
196
|
-
'⁰': '0', '¹': '1', '²': '2', '³': '3', '⁴': '4',
|
|
197
|
-
'⁵': '5', '⁶': '6', '⁷': '7', '⁸': '8', '⁹': '9',
|
|
198
|
-
'ⁿ': 'n', 'ⁱ': 'i'
|
|
199
|
-
};
|
|
200
|
-
for (const [unicode, digit] of Object.entries(superscriptMap)) {
|
|
201
|
-
result = result.replace(new RegExp(unicode, 'g'), `^${digit}^`);
|
|
202
|
-
}
|
|
203
|
-
// Convert Unicode subscript digits to Pandoc ~n~ format
|
|
204
|
-
const subscriptMap = {
|
|
205
|
-
'₀': '0', '₁': '1', '₂': '2', '₃': '3', '₄': '4',
|
|
206
|
-
'₅': '5', '₆': '6', '₇': '7', '₈': '8', '₉': '9'
|
|
207
|
-
};
|
|
208
|
-
for (const [unicode, digit] of Object.entries(subscriptMap)) {
|
|
209
|
-
result = result.replace(new RegExp(unicode, 'g'), `~${digit}~`);
|
|
210
|
-
}
|
|
211
|
-
return result;
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Create Reddit-optimized Turndown service
|
|
215
|
-
* - Superscript support (^text or ^(text with spaces))
|
|
216
|
-
* - Subscript to bracketed text
|
|
217
|
-
* - Fenced code blocks (new Reddit)
|
|
218
|
-
*/
|
|
219
|
-
createRedditService() {
|
|
220
|
-
const service = new TurndownService({
|
|
221
|
-
headingStyle: 'atx',
|
|
222
|
-
hr: '---',
|
|
223
|
-
bulletListMarker: '-',
|
|
224
|
-
codeBlockStyle: 'fenced',
|
|
225
|
-
emDelimiter: '*',
|
|
226
|
-
});
|
|
227
|
-
// Apply all shared rules
|
|
228
|
-
this.addGoogleDocsRules(service);
|
|
229
|
-
this.addTableRules(service);
|
|
230
|
-
this.addListRules(service);
|
|
231
|
-
this.addPdfPageMarkerRules(service);
|
|
232
|
-
// Reddit-specific: Superscript (semantic tag)
|
|
233
|
-
service.addRule('redditSuperscript', {
|
|
234
|
-
filter: (node) => node.nodeName === 'SUP',
|
|
235
|
-
replacement: (content) => {
|
|
236
|
-
const trimmed = content.trim();
|
|
237
|
-
if (!trimmed)
|
|
238
|
-
return '';
|
|
239
|
-
return trimmed.includes(' ') ? `^(${trimmed})` : `^${trimmed}`;
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
// Reddit-specific: Superscript (CSS vertical-align: super)
|
|
243
|
-
service.addRule('redditSuperscriptStyle', {
|
|
244
|
-
filter: (node) => {
|
|
245
|
-
if (node.nodeName !== 'SPAN')
|
|
246
|
-
return false;
|
|
247
|
-
const style = node.getAttribute('style') || '';
|
|
248
|
-
return style.includes('vertical-align: super') ||
|
|
249
|
-
style.includes('vertical-align:super');
|
|
250
|
-
},
|
|
251
|
-
replacement: (content) => {
|
|
252
|
-
const trimmed = content.trim();
|
|
253
|
-
if (!trimmed)
|
|
254
|
-
return '';
|
|
255
|
-
return trimmed.includes(' ') ? `^(${trimmed})` : `^${trimmed}`;
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
// Reddit-specific: Subscript to brackets (semantic tag)
|
|
259
|
-
service.addRule('redditSubscript', {
|
|
260
|
-
filter: (node) => node.nodeName === 'SUB',
|
|
261
|
-
replacement: (content) => {
|
|
262
|
-
const trimmed = content.trim();
|
|
263
|
-
return trimmed ? `[${trimmed}]` : '';
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
// Reddit-specific: Subscript to brackets (CSS vertical-align: sub)
|
|
267
|
-
service.addRule('redditSubscriptStyle', {
|
|
268
|
-
filter: (node) => {
|
|
269
|
-
if (node.nodeName !== 'SPAN')
|
|
270
|
-
return false;
|
|
271
|
-
const style = node.getAttribute('style') || '';
|
|
272
|
-
return style.includes('vertical-align: sub') ||
|
|
273
|
-
style.includes('vertical-align:sub');
|
|
274
|
-
},
|
|
275
|
-
replacement: (content) => {
|
|
276
|
-
const trimmed = content.trim();
|
|
277
|
-
return trimmed ? `[${trimmed}]` : '';
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
return service;
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Create R Markdown-optimized Turndown service
|
|
284
|
-
* - Pandoc superscript (^text^)
|
|
285
|
-
* - Pandoc subscript (~text~)
|
|
286
|
-
* - Math expression preservation
|
|
287
|
-
*/
|
|
288
|
-
createRMarkdownService() {
|
|
289
|
-
const service = new TurndownService({
|
|
290
|
-
headingStyle: 'atx',
|
|
291
|
-
hr: '---',
|
|
292
|
-
bulletListMarker: '-',
|
|
293
|
-
codeBlockStyle: 'fenced',
|
|
294
|
-
emDelimiter: '*',
|
|
295
|
-
});
|
|
296
|
-
// Apply all shared rules
|
|
297
|
-
this.addGoogleDocsRules(service);
|
|
298
|
-
this.addTableRules(service);
|
|
299
|
-
this.addListRules(service);
|
|
300
|
-
this.addPdfPageMarkerRules(service);
|
|
301
|
-
// R Markdown-specific: Pandoc superscript (semantic tag)
|
|
302
|
-
service.addRule('rmdSuperscript', {
|
|
303
|
-
filter: (node) => node.nodeName === 'SUP',
|
|
304
|
-
replacement: (content) => {
|
|
305
|
-
const trimmed = content.trim();
|
|
306
|
-
return trimmed ? `^${trimmed}^` : '';
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
// R Markdown-specific: Pandoc superscript (CSS vertical-align: super)
|
|
310
|
-
service.addRule('rmdSuperscriptStyle', {
|
|
311
|
-
filter: (node) => {
|
|
312
|
-
if (node.nodeName !== 'SPAN')
|
|
313
|
-
return false;
|
|
314
|
-
const style = node.getAttribute('style') || '';
|
|
315
|
-
return style.includes('vertical-align: super') ||
|
|
316
|
-
style.includes('vertical-align:super');
|
|
317
|
-
},
|
|
318
|
-
replacement: (content) => {
|
|
319
|
-
const trimmed = content.trim();
|
|
320
|
-
return trimmed ? `^${trimmed}^` : '';
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
// R Markdown-specific: Pandoc subscript (semantic tag)
|
|
324
|
-
service.addRule('rmdSubscript', {
|
|
325
|
-
filter: (node) => node.nodeName === 'SUB',
|
|
326
|
-
replacement: (content) => {
|
|
327
|
-
const trimmed = content.trim();
|
|
328
|
-
return trimmed ? `~${trimmed}~` : '';
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
// R Markdown-specific: Pandoc subscript (CSS vertical-align: sub)
|
|
332
|
-
service.addRule('rmdSubscriptStyle', {
|
|
333
|
-
filter: (node) => {
|
|
334
|
-
if (node.nodeName !== 'SPAN')
|
|
335
|
-
return false;
|
|
336
|
-
const style = node.getAttribute('style') || '';
|
|
337
|
-
return style.includes('vertical-align: sub') ||
|
|
338
|
-
style.includes('vertical-align:sub');
|
|
339
|
-
},
|
|
340
|
-
replacement: (content) => {
|
|
341
|
-
const trimmed = content.trim();
|
|
342
|
-
return trimmed ? `~${trimmed}~` : '';
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
// R Markdown-specific: Preserve math expressions
|
|
346
|
-
service.addRule('rmdMath', {
|
|
347
|
-
filter: (node) => {
|
|
348
|
-
if (node.nodeName !== 'SPAN')
|
|
349
|
-
return false;
|
|
350
|
-
return node.classList.contains('math') ||
|
|
351
|
-
node.classList.contains('MathJax') ||
|
|
352
|
-
node.hasAttribute('data-math');
|
|
353
|
-
},
|
|
354
|
-
replacement: (content, node) => {
|
|
355
|
-
const element = node;
|
|
356
|
-
const isDisplay = element.classList.contains('display');
|
|
357
|
-
const math = element.getAttribute('data-math') || content;
|
|
358
|
-
return isDisplay ? `$$${math}$$\n\n` : `$${math}$`;
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
return service;
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Convert HTML table to plain text for Discord code blocks
|
|
365
|
-
*/
|
|
366
|
-
tableToPlainText(table) {
|
|
367
|
-
const rows = Array.from(table.querySelectorAll('tr'));
|
|
368
|
-
if (rows.length === 0)
|
|
369
|
-
return '';
|
|
370
|
-
// Calculate column widths
|
|
371
|
-
const colWidths = [];
|
|
372
|
-
rows.forEach(row => {
|
|
373
|
-
const cells = Array.from(row.querySelectorAll('th, td'));
|
|
374
|
-
cells.forEach((cell, i) => {
|
|
375
|
-
const text = (cell.textContent || '').trim();
|
|
376
|
-
colWidths[i] = Math.max(colWidths[i] || 0, text.length);
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
// Build plain text table
|
|
380
|
-
const lines = [];
|
|
381
|
-
rows.forEach(row => {
|
|
382
|
-
const cells = Array.from(row.querySelectorAll('th, td'));
|
|
383
|
-
const cellTexts = cells.map((cell, i) => {
|
|
384
|
-
const text = (cell.textContent || '').trim();
|
|
385
|
-
return text.padEnd(colWidths[i] || 0);
|
|
386
|
-
});
|
|
387
|
-
lines.push(cellTexts.join(' '));
|
|
388
|
-
});
|
|
389
|
-
return lines.join('\n');
|
|
390
|
-
}
|
|
391
|
-
addGoogleDocsRules(service) {
|
|
392
|
-
// Enhanced Google Docs bold text detection
|
|
393
|
-
service.addRule('googleDocsBold', {
|
|
394
|
-
filter: (node) => {
|
|
395
|
-
if (node.nodeName !== 'SPAN')
|
|
396
|
-
return false;
|
|
397
|
-
const style = node.getAttribute('style') || '';
|
|
398
|
-
// Check for font-weight: bold in the style
|
|
399
|
-
if (style.includes('font-weight: bold') || style.includes('font-weight:bold')) {
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
402
|
-
// Check for bold font family names (Arial-BoldMT, etc)
|
|
403
|
-
const fontFamilyMatch = style.match(/font-family:\s*['"]*([^'"]+)['"]*[;]?/);
|
|
404
|
-
if (fontFamilyMatch && fontFamilyMatch[1].toLowerCase().includes('bold')) {
|
|
405
|
-
return true;
|
|
406
|
-
}
|
|
407
|
-
// Check for font-weight numeric values >= 600
|
|
408
|
-
const fontWeightMatch = style.match(/font-weight:\s*(\d+)/);
|
|
409
|
-
if (fontWeightMatch && parseInt(fontWeightMatch[1]) >= 600) {
|
|
410
|
-
return true;
|
|
411
|
-
}
|
|
412
|
-
return false;
|
|
413
|
-
},
|
|
414
|
-
replacement: (content) => {
|
|
415
|
-
return content.trim() ? `**${content}**` : content;
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
// Enhanced Google Docs italic text detection
|
|
419
|
-
service.addRule('googleDocsItalic', {
|
|
420
|
-
filter: (node) => {
|
|
421
|
-
if (node.nodeName !== 'SPAN')
|
|
422
|
-
return false;
|
|
423
|
-
const style = node.getAttribute('style') || '';
|
|
424
|
-
// Check for font-style: italic
|
|
425
|
-
if (style.includes('font-style: italic') || style.includes('font-style:italic')) {
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
428
|
-
// Check for italic font family names
|
|
429
|
-
const fontFamilyMatch = style.match(/font-family:\s*['"]*([^'"]+)['"]*[;]?/);
|
|
430
|
-
if (fontFamilyMatch && fontFamilyMatch[1].toLowerCase().includes('italic')) {
|
|
431
|
-
return true;
|
|
432
|
-
}
|
|
433
|
-
return false;
|
|
434
|
-
},
|
|
435
|
-
replacement: (content) => {
|
|
436
|
-
return content.trim() ? `*${content}*` : content;
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
// Enhanced Google Docs headings detection
|
|
440
|
-
service.addRule('googleDocsHeadings', {
|
|
441
|
-
filter: (node) => {
|
|
442
|
-
if (node.nodeName === 'P' || node.nodeName === 'DIV') {
|
|
443
|
-
const style = node.getAttribute('style') || '';
|
|
444
|
-
// Check for large font sizes
|
|
445
|
-
const fontSizeMatch = style.match(/font-size:\s*(\d+(?:\.\d+)?)px/);
|
|
446
|
-
if (fontSizeMatch) {
|
|
447
|
-
const fontSize = parseFloat(fontSizeMatch[1]);
|
|
448
|
-
return fontSize > 16;
|
|
449
|
-
}
|
|
450
|
-
// Check for font-weight bold combined with larger text
|
|
451
|
-
if (style.includes('font-weight: bold') || style.includes('font-weight:bold')) {
|
|
452
|
-
const textContent = node.textContent || '';
|
|
453
|
-
// If it's short and bold, likely a heading
|
|
454
|
-
return textContent.length < 100 && textContent.trim().length > 0;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
return false;
|
|
458
|
-
},
|
|
459
|
-
replacement: (content, node) => {
|
|
460
|
-
const style = node?.getAttribute('style') || '';
|
|
461
|
-
const fontSizeMatch = style.match(/font-size:\s*(\d+(?:\.\d+)?)px/);
|
|
462
|
-
const fontSize = fontSizeMatch ? parseFloat(fontSizeMatch[1]) : 0;
|
|
463
|
-
// Determine heading level based on font size
|
|
464
|
-
let level = 3; // default to h3
|
|
465
|
-
if (fontSize >= 28)
|
|
466
|
-
level = 1;
|
|
467
|
-
else if (fontSize >= 24)
|
|
468
|
-
level = 2;
|
|
469
|
-
else if (fontSize >= 20)
|
|
470
|
-
level = 3;
|
|
471
|
-
else if (fontSize >= 18)
|
|
472
|
-
level = 4;
|
|
473
|
-
else
|
|
474
|
-
level = 5;
|
|
475
|
-
const trimmedContent = content.trim();
|
|
476
|
-
return trimmedContent ? '\n' + '#'.repeat(level) + ' ' + trimmedContent + '\n\n' : content;
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
// Google Docs underline detection
|
|
480
|
-
service.addRule('googleDocsUnderline', {
|
|
481
|
-
filter: (node) => {
|
|
482
|
-
if (node.nodeName === 'SPAN') {
|
|
483
|
-
const style = node.getAttribute('style') || '';
|
|
484
|
-
return style.includes('text-decoration: underline') || style.includes('text-decoration:underline');
|
|
485
|
-
}
|
|
486
|
-
return false;
|
|
487
|
-
},
|
|
488
|
-
replacement: (content) => {
|
|
489
|
-
return content.trim() ? `<u>${content}</u>` : content;
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
// Strikethrough detection - semantic tags
|
|
493
|
-
service.addRule('strikethroughTags', {
|
|
494
|
-
filter: ['del', 's'],
|
|
495
|
-
replacement: (content) => {
|
|
496
|
-
return content.trim() ? `~~${content}~~` : content;
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
// Strikethrough detection - deprecated <strike> tag (filter as node check for TS compatibility)
|
|
500
|
-
service.addRule('strikethroughStrikeTag', {
|
|
501
|
-
filter: (node) => node.nodeName === 'STRIKE',
|
|
502
|
-
replacement: (content) => {
|
|
503
|
-
return content.trim() ? `~~${content}~~` : content;
|
|
504
|
-
}
|
|
505
|
-
});
|
|
506
|
-
// Strikethrough detection - CSS text-decoration: line-through
|
|
507
|
-
service.addRule('strikethroughStyle', {
|
|
508
|
-
filter: (node) => {
|
|
509
|
-
if (node.nodeName !== 'SPAN')
|
|
510
|
-
return false;
|
|
511
|
-
const style = node.getAttribute('style') || '';
|
|
512
|
-
return style.includes('text-decoration: line-through') ||
|
|
513
|
-
style.includes('text-decoration:line-through') ||
|
|
514
|
-
style.includes('text-decoration-line: line-through') ||
|
|
515
|
-
style.includes('text-decoration-line:line-through');
|
|
516
|
-
},
|
|
517
|
-
replacement: (content) => {
|
|
518
|
-
return content.trim() ? `~~${content}~~` : content;
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
// Inline code detection - CSS monospace font-family
|
|
522
|
-
service.addRule('monospaceCode', {
|
|
523
|
-
filter: (node) => {
|
|
524
|
-
if (node.nodeName !== 'SPAN')
|
|
525
|
-
return false;
|
|
526
|
-
const style = node.getAttribute('style') || '';
|
|
527
|
-
const fontFamily = style.toLowerCase();
|
|
528
|
-
// Check for common monospace font indicators
|
|
529
|
-
return fontFamily.includes('monospace') ||
|
|
530
|
-
fontFamily.includes('courier') ||
|
|
531
|
-
fontFamily.includes('consolas') ||
|
|
532
|
-
fontFamily.includes('monaco') ||
|
|
533
|
-
fontFamily.includes('menlo') ||
|
|
534
|
-
fontFamily.includes('source code') ||
|
|
535
|
-
fontFamily.includes('fira code') ||
|
|
536
|
-
fontFamily.includes('roboto mono') ||
|
|
537
|
-
fontFamily.includes('ubuntu mono');
|
|
538
|
-
},
|
|
539
|
-
replacement: (content) => {
|
|
540
|
-
const trimmed = content.trim();
|
|
541
|
-
if (!trimmed)
|
|
542
|
-
return content;
|
|
543
|
-
// Use backticks for inline code
|
|
544
|
-
// If content contains backticks, use double backticks
|
|
545
|
-
if (trimmed.includes('`')) {
|
|
546
|
-
return `\`\` ${trimmed} \`\``;
|
|
547
|
-
}
|
|
548
|
-
return `\`${trimmed}\``;
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
addTableRules(service) {
|
|
553
|
-
// Enhanced table handling
|
|
554
|
-
service.addRule('enhancedTable', {
|
|
555
|
-
filter: 'table',
|
|
556
|
-
replacement: (content, node) => {
|
|
557
|
-
const table = node;
|
|
558
|
-
const rows = Array.from(table.querySelectorAll('tr'));
|
|
559
|
-
if (rows.length === 0)
|
|
560
|
-
return content;
|
|
561
|
-
let markdown = '\n';
|
|
562
|
-
let headerProcessed = false;
|
|
563
|
-
rows.forEach((row, index) => {
|
|
564
|
-
const cells = Array.from(row.querySelectorAll('th, td'));
|
|
565
|
-
const cellContents = cells.map(cell => {
|
|
566
|
-
const cellContent = cell.textContent || '';
|
|
567
|
-
return cellContent.trim().replace(/\|/g, '\\|'); // Escape pipes
|
|
568
|
-
});
|
|
569
|
-
if (cellContents.length > 0) {
|
|
570
|
-
markdown += '| ' + cellContents.join(' | ') + ' |\n';
|
|
571
|
-
// Add header separator after first row if it contains th elements
|
|
572
|
-
if (!headerProcessed && (row.querySelector('th') || index === 0)) {
|
|
573
|
-
const separator = cellContents.map(() => '---').join(' | ');
|
|
574
|
-
markdown += '| ' + separator + ' |\n';
|
|
575
|
-
headerProcessed = true;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
return markdown + '\n';
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
addListRules(service) {
|
|
584
|
-
// Enhanced nested list handling
|
|
585
|
-
service.addRule('enhancedList', {
|
|
586
|
-
filter: ['ul', 'ol'],
|
|
587
|
-
replacement: (content, node) => {
|
|
588
|
-
const listType = node?.nodeName.toLowerCase();
|
|
589
|
-
const items = Array.from(node.children).filter(child => child.nodeName.toLowerCase() === 'li');
|
|
590
|
-
if (items.length === 0)
|
|
591
|
-
return content;
|
|
592
|
-
let markdown = '\n';
|
|
593
|
-
const isOrdered = listType === 'ol';
|
|
594
|
-
const marker = isOrdered ? '1.' : (service.options.bulletListMarker || '-');
|
|
595
|
-
items.forEach((item, index) => {
|
|
596
|
-
const itemContent = this.processListItem(item, service);
|
|
597
|
-
const actualMarker = isOrdered ? `${index + 1}.` : marker;
|
|
598
|
-
markdown += `${actualMarker} ${itemContent}\n`;
|
|
599
|
-
});
|
|
600
|
-
return markdown + '\n';
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
processListItem(item, service, depth = 0) {
|
|
605
|
-
let content = '';
|
|
606
|
-
// Use numeric constants instead of Node.TEXT_NODE / Node.ELEMENT_NODE
|
|
607
|
-
// to avoid direct browser-global reference (Req 14.3)
|
|
608
|
-
const TEXT_NODE = 3;
|
|
609
|
-
const ELEMENT_NODE = 1;
|
|
610
|
-
Array.from(item.childNodes).forEach(child => {
|
|
611
|
-
if (child.nodeType === TEXT_NODE) {
|
|
612
|
-
content += child.textContent || '';
|
|
613
|
-
}
|
|
614
|
-
else if (child.nodeType === ELEMENT_NODE) {
|
|
615
|
-
const element = child;
|
|
616
|
-
if (element.nodeName.toLowerCase() === 'ul' || element.nodeName.toLowerCase() === 'ol') {
|
|
617
|
-
// Handle nested lists, incrementing depth so indentation grows correctly
|
|
618
|
-
const nestedContent = this.processNestedList(element, service, depth + 1);
|
|
619
|
-
content += '\n' + nestedContent;
|
|
620
|
-
}
|
|
621
|
-
else {
|
|
622
|
-
content += service.turndown(element.outerHTML);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
});
|
|
626
|
-
return content.trim();
|
|
627
|
-
}
|
|
628
|
-
processNestedList(list, service, depth) {
|
|
629
|
-
const items = Array.from(list.children).filter(child => child.nodeName.toLowerCase() === 'li');
|
|
630
|
-
if (items.length === 0)
|
|
631
|
-
return '';
|
|
632
|
-
let markdown = '';
|
|
633
|
-
const isOrdered = list.nodeName.toLowerCase() === 'ol';
|
|
634
|
-
const indent = ' '.repeat(depth);
|
|
635
|
-
const marker = isOrdered ? '1.' : (service.options.bulletListMarker || '-');
|
|
636
|
-
items.forEach((item, index) => {
|
|
637
|
-
const itemContent = this.processListItem(item, service, depth);
|
|
638
|
-
const actualMarker = isOrdered ? `${index + 1}.` : marker;
|
|
639
|
-
markdown += `${indent}${actualMarker} ${itemContent}\n`;
|
|
640
|
-
});
|
|
641
|
-
return markdown;
|
|
642
|
-
}
|
|
643
|
-
/**
|
|
644
|
-
* Add PDF page marker rules to convert data-page-marker divs to Markdown comments
|
|
645
|
-
* This preserves page boundaries from PDF documents (Req 3.5)
|
|
646
|
-
*/
|
|
647
|
-
addPdfPageMarkerRules(service) {
|
|
648
|
-
service.addRule('pdfPageMarker', {
|
|
649
|
-
filter: (node) => {
|
|
650
|
-
if (node.nodeName !== 'DIV')
|
|
651
|
-
return false;
|
|
652
|
-
return node.hasAttribute('data-page-marker');
|
|
653
|
-
},
|
|
654
|
-
replacement: (_content, node) => {
|
|
655
|
-
const pageNumber = node.getAttribute('data-page-marker');
|
|
656
|
-
// Output as a Markdown comment-style marker that's visible in the output
|
|
657
|
-
return `\n\n---\n\n**[Page ${pageNumber}]**\n\n`;
|
|
658
|
-
}
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
async convert(html, options = {
|
|
662
|
-
format: 'github',
|
|
663
|
-
preserveTables: true,
|
|
664
|
-
preserveLists: true,
|
|
665
|
-
customRules: []
|
|
666
|
-
}) {
|
|
667
|
-
const startTime = performance.now();
|
|
668
|
-
const warnings = [];
|
|
669
|
-
const rulesApplied = [];
|
|
670
|
-
// Sanitize input
|
|
671
|
-
const sanitizedHtml = this.sanitize(html);
|
|
672
|
-
if (sanitizedHtml !== html) {
|
|
673
|
-
warnings.push('HTML content was sanitized for security');
|
|
674
|
-
}
|
|
675
|
-
// Validate input
|
|
676
|
-
const validation = this.validateInput(sanitizedHtml);
|
|
677
|
-
if (!validation.isValid) {
|
|
678
|
-
throw new Error(`Invalid input: ${validation.errors.join(', ')}`);
|
|
679
|
-
}
|
|
680
|
-
warnings.push(...validation.warnings);
|
|
681
|
-
// Select appropriate service
|
|
682
|
-
let service;
|
|
683
|
-
switch (options.format) {
|
|
684
|
-
case 'commonmark':
|
|
685
|
-
service = this.commonmarkService;
|
|
686
|
-
break;
|
|
687
|
-
case 'custom':
|
|
688
|
-
service = this.customService;
|
|
689
|
-
// Apply custom rules
|
|
690
|
-
options.customRules.forEach(rule => {
|
|
691
|
-
service.addRule(rule.name, {
|
|
692
|
-
filter: rule.filter,
|
|
693
|
-
replacement: rule.replacement
|
|
694
|
-
});
|
|
695
|
-
rulesApplied.push(rule.name);
|
|
696
|
-
});
|
|
697
|
-
break;
|
|
698
|
-
case 'discord':
|
|
699
|
-
service = this.discordService;
|
|
700
|
-
break;
|
|
701
|
-
case 'reddit':
|
|
702
|
-
service = this.redditService;
|
|
703
|
-
break;
|
|
704
|
-
case 'rmarkdown':
|
|
705
|
-
service = this.rmarkdownService;
|
|
706
|
-
break;
|
|
707
|
-
case 'github':
|
|
708
|
-
default:
|
|
709
|
-
service = this.githubService;
|
|
710
|
-
break;
|
|
711
|
-
}
|
|
712
|
-
// Perform conversion
|
|
713
|
-
let markdown = service.turndown(sanitizedHtml);
|
|
714
|
-
// Apply format-specific post-processing for Unicode characters
|
|
715
|
-
markdown = this.applyFormatPostProcessing(markdown, options.format);
|
|
716
|
-
const endTime = performance.now();
|
|
717
|
-
const metadata = {
|
|
718
|
-
originalLength: html.length,
|
|
719
|
-
convertedLength: markdown.length,
|
|
720
|
-
processingTime: endTime - startTime,
|
|
721
|
-
formatUsed: options.format,
|
|
722
|
-
rulesApplied
|
|
723
|
-
};
|
|
724
|
-
return {
|
|
725
|
-
markdown,
|
|
726
|
-
warnings,
|
|
727
|
-
metadata
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
sanitize(html) {
|
|
731
|
-
// Use the centralized sanitization from input-sanitizer
|
|
732
|
-
return sanitizeHTML(html, {
|
|
733
|
-
level: 'moderate' // Use moderate level for conversion
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
validateInput(input) {
|
|
737
|
-
const errors = [];
|
|
738
|
-
const warnings = [];
|
|
739
|
-
if (!input || input.trim().length === 0) {
|
|
740
|
-
errors.push('Input is empty');
|
|
741
|
-
}
|
|
742
|
-
if (input.length > 1024 * 1024) { // 1MB limit
|
|
743
|
-
errors.push('Input exceeds maximum size limit');
|
|
744
|
-
}
|
|
745
|
-
if (input.length > 100 * 1024) { // 100KB warning
|
|
746
|
-
warnings.push('Large input detected, processing may take longer');
|
|
747
|
-
}
|
|
748
|
-
// Check for potentially problematic content
|
|
749
|
-
if (input.includes('<script')) {
|
|
750
|
-
warnings.push('Script tags detected and will be removed');
|
|
751
|
-
}
|
|
752
|
-
if (input.includes('javascript:')) {
|
|
753
|
-
warnings.push('JavaScript URLs detected and will be sanitized');
|
|
754
|
-
}
|
|
755
|
-
return {
|
|
756
|
-
isValid: errors.length === 0,
|
|
757
|
-
errors,
|
|
758
|
-
warnings
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
// Method to get available format options
|
|
762
|
-
getAvailableFormats() {
|
|
763
|
-
return [
|
|
764
|
-
{
|
|
765
|
-
value: 'github',
|
|
766
|
-
label: 'GitHub Flavored Markdown',
|
|
767
|
-
description: 'Optimized for GitHub with fenced code blocks and dashes for lists'
|
|
768
|
-
},
|
|
769
|
-
{
|
|
770
|
-
value: 'commonmark',
|
|
771
|
-
label: 'CommonMark',
|
|
772
|
-
description: 'Strict CommonMark specification with indented code blocks'
|
|
773
|
-
},
|
|
774
|
-
{
|
|
775
|
-
value: 'custom',
|
|
776
|
-
label: 'Custom Rules',
|
|
777
|
-
description: 'GitHub base with additional custom conversion rules'
|
|
778
|
-
}
|
|
779
|
-
];
|
|
780
|
-
}
|
|
781
|
-
// Method to create custom rules easily
|
|
782
|
-
createCustomRule(name, selector, replacement) {
|
|
783
|
-
return {
|
|
784
|
-
name,
|
|
785
|
-
filter: selector,
|
|
786
|
-
replacement
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
//# sourceMappingURL=conversion-engine.js.map
|