officeparser 7.2.3 → 7.3.0
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/README.md +148 -9
- package/dist/OfficeGenerator.js +4 -0
- package/dist/OfficeParser.d.ts +2 -0
- package/dist/OfficeParser.js +6 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -2
- package/dist/generators/BaseGenerator.d.ts +11 -0
- package/dist/generators/BaseGenerator.js +29 -0
- package/dist/generators/CsvGenerator.d.ts +9 -1
- package/dist/generators/CsvGenerator.js +24 -14
- package/dist/generators/EpubGenerator.d.ts +18 -0
- package/dist/generators/EpubGenerator.js +242 -0
- package/dist/generators/HtmlGenerator.d.ts +12 -0
- package/dist/generators/HtmlGenerator.js +266 -51
- package/dist/generators/MarkdownGenerator.d.ts +16 -0
- package/dist/generators/MarkdownGenerator.js +173 -24
- package/dist/generators/PdfGenerator.js +32 -0
- package/dist/generators/RtfGenerator.js +12 -15
- package/dist/generators/TextGenerator.js +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/officeparser.browser.d.ts +143 -6
- package/dist/officeparser.browser.iife.js +284 -188
- package/dist/officeparser.browser.mjs +284 -188
- package/dist/officeparser.browser.slim.d.ts +143 -6
- package/dist/officeparser.browser.slim.iife.js +284 -188
- package/dist/officeparser.browser.slim.mjs +284 -188
- package/dist/parsers/EpubParser.d.ts +8 -0
- package/dist/parsers/EpubParser.js +217 -0
- package/dist/parsers/HtmlParser.js +284 -20
- package/dist/parsers/MarkdownParser.js +424 -33
- package/dist/parsers/PdfParser.js +4 -1
- package/dist/sbom.cdx.json +1695 -0
- package/dist/types.d.ts +142 -6
- package/dist/types.js +2 -0
- package/dist/utils/errorUtils.js +3 -2
- package/dist/utils/sanitize.d.ts +99 -0
- package/dist/utils/sanitize.js +228 -0
- package/dist/utils/zipUtils.js +76 -26
- package/package.json +9 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MarkdownGenerator = void 0;
|
|
4
|
+
const sanitize_js_1 = require("../utils/sanitize.js");
|
|
4
5
|
const BaseGenerator_js_1 = require("./BaseGenerator.js");
|
|
5
6
|
/**
|
|
6
7
|
* Generates Markdown from an AST.
|
|
@@ -29,6 +30,7 @@ const BaseGenerator_js_1 = require("./BaseGenerator.js");
|
|
|
29
30
|
class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
30
31
|
isInsideTable = false;
|
|
31
32
|
hoistedContent = [];
|
|
33
|
+
collectedAbbreviations = new Map();
|
|
32
34
|
constructor(ast, config) {
|
|
33
35
|
super('md', ast, config);
|
|
34
36
|
}
|
|
@@ -41,6 +43,41 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
41
43
|
const ids = metadata?.anchorIds || [];
|
|
42
44
|
return ids.map((aid) => `<a id="${this.slugify(aid)}"></a>`).join('');
|
|
43
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Serializes a frontmatter array as a YAML flow sequence (e.g. `[a, b]`), matching
|
|
48
|
+
* MarkdownParser's frontmatter array handling. Plain strings are left bare; anything
|
|
49
|
+
* that would break flow-array syntax (or isn't a string) falls back to JSON encoding.
|
|
50
|
+
*/
|
|
51
|
+
serializeFrontmatterArray(arr) {
|
|
52
|
+
const items = arr.map(item => (typeof item === 'string' && item.trim() === item && !/[,[\]]/.test(item))
|
|
53
|
+
? item
|
|
54
|
+
: JSON.stringify(item));
|
|
55
|
+
return `[${items.join(', ')}]`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Renders a Pandoc-style attribute list (e.g. `{width=50% align=left}`) from
|
|
59
|
+
* ImageMetadata/TableMetadata's width/align fields - the canonical form is always
|
|
60
|
+
* `key=value`, matching MarkdownParser's own vocabulary (MARKDOWN_DIALECT.md §15).
|
|
61
|
+
*/
|
|
62
|
+
renderAttributeList(meta) {
|
|
63
|
+
if (!meta?.width && !meta?.align)
|
|
64
|
+
return '';
|
|
65
|
+
const parts = [];
|
|
66
|
+
// Strip characters that would break out of the `{...}` attribute list.
|
|
67
|
+
if (meta.width)
|
|
68
|
+
parts.push(`width=${String(meta.width).replace(/[{}\s]+/g, '')}`);
|
|
69
|
+
if (meta.align)
|
|
70
|
+
parts.push(`align=${String(meta.align).replace(/[{}\s]+/g, '')}`);
|
|
71
|
+
return `{${parts.join(' ')}}`;
|
|
72
|
+
}
|
|
73
|
+
/** Converts a document-supplied date to an ISO string, or '' if invalid
|
|
74
|
+
* (a malformed date would otherwise throw a RangeError and abort generation). */
|
|
75
|
+
toIsoDate(value) {
|
|
76
|
+
if (value === undefined || value === null || value === '')
|
|
77
|
+
return '';
|
|
78
|
+
const d = new Date(value);
|
|
79
|
+
return isNaN(d.getTime()) ? '' : d.toISOString();
|
|
80
|
+
}
|
|
44
81
|
/**
|
|
45
82
|
* Generates Markdown string from the provided AST.
|
|
46
83
|
*
|
|
@@ -51,19 +88,27 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
51
88
|
// Add Metadata (YAML Front Matter)
|
|
52
89
|
if (this.ast.metadata) {
|
|
53
90
|
output += '---\n';
|
|
91
|
+
// JSON-encode scalar values so a title/author/description containing a
|
|
92
|
+
// quote or newline can't break out of the YAML string and inject
|
|
93
|
+
// arbitrary front-matter keys. (JSON.stringify of a benign value yields
|
|
94
|
+
// the same `"..."` form as before, so normal output is unchanged.)
|
|
54
95
|
if (this.ast.metadata.title)
|
|
55
|
-
output += `title:
|
|
96
|
+
output += `title: ${JSON.stringify(this.ast.metadata.title)}\n`;
|
|
56
97
|
if (this.ast.metadata.author)
|
|
57
|
-
output += `author:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
98
|
+
output += `author: ${JSON.stringify(this.ast.metadata.author)}\n`;
|
|
99
|
+
const createdIso = this.toIsoDate(this.ast.metadata.created);
|
|
100
|
+
if (createdIso)
|
|
101
|
+
output += `created: ${createdIso}\n`;
|
|
102
|
+
const modifiedIso = this.toIsoDate(this.ast.metadata.modified);
|
|
103
|
+
if (modifiedIso)
|
|
104
|
+
output += `modified: ${modifiedIso}\n`;
|
|
62
105
|
if (this.ast.metadata.description)
|
|
63
|
-
output += `description:
|
|
106
|
+
output += `description: ${JSON.stringify(this.ast.metadata.description)}\n`;
|
|
64
107
|
if (this.ast.metadata.customProperties) {
|
|
65
108
|
for (const [key, val] of Object.entries(this.ast.metadata.customProperties)) {
|
|
66
|
-
|
|
109
|
+
// Strip newlines/colons from the key so it can't inject a new mapping.
|
|
110
|
+
const safeKey = String(key).replace(/[\r\n:]+/g, ' ').trim();
|
|
111
|
+
output += `${safeKey}: ${Array.isArray(val) ? this.serializeFrontmatterArray(val) : JSON.stringify(val)}\n`;
|
|
67
112
|
}
|
|
68
113
|
}
|
|
69
114
|
output += '---\n\n';
|
|
@@ -87,7 +132,9 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
87
132
|
}
|
|
88
133
|
switch (node.type) {
|
|
89
134
|
case 'text': {
|
|
90
|
-
|
|
135
|
+
// Entity-encode angle brackets so document text can't inject a raw
|
|
136
|
+
// HTML tag (e.g. <script>) when the Markdown is rendered to HTML.
|
|
137
|
+
let text = (0, sanitize_js_1.markdownEscapeText)(node.text || '');
|
|
91
138
|
if (this.config.includeFormatting && node.formatting) {
|
|
92
139
|
if (node.formatting.bold)
|
|
93
140
|
text = `**${text}**`;
|
|
@@ -106,7 +153,15 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
106
153
|
}
|
|
107
154
|
}
|
|
108
155
|
const meta = node.metadata;
|
|
109
|
-
if (meta?.
|
|
156
|
+
if (meta?.wikilink) {
|
|
157
|
+
// Obsidian syntax: bare page name, or page|alias when the display
|
|
158
|
+
// text differs from the page name. Strip the `[]|`/newline chars
|
|
159
|
+
// that would break out of the `[[...]]` wrapper.
|
|
160
|
+
const page = (meta.link || '').replace(/[[\]|\r\n]+/g, '');
|
|
161
|
+
const alias = (node.text || '').replace(/[[\]|\r\n]+/g, '');
|
|
162
|
+
text = (node.text && node.text !== (meta.link || '')) ? `[[${page}|${alias}]]` : `[[${page}]]`;
|
|
163
|
+
}
|
|
164
|
+
else if (meta?.link) {
|
|
110
165
|
const isInternal = meta.linkType !== 'external';
|
|
111
166
|
if (!this.config.ignoreInternalLinks || !isInternal) {
|
|
112
167
|
let link = meta.link;
|
|
@@ -115,9 +170,20 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
115
170
|
const target = link.substring(1);
|
|
116
171
|
link = '#' + this.slugify(target);
|
|
117
172
|
}
|
|
118
|
-
|
|
173
|
+
// Reject javascript:/data: schemes and encode `()`/whitespace so the
|
|
174
|
+
// URL can't break out of `](...)` or inject a script link.
|
|
175
|
+
text = `[${text}](${(0, sanitize_js_1.sanitizeMarkdownUrl)(link)})`;
|
|
119
176
|
}
|
|
120
177
|
}
|
|
178
|
+
if (meta?.abbreviationTitle) {
|
|
179
|
+
// Markdown Extra's abbreviation syntax has no inline marker - the bare
|
|
180
|
+
// word round-trips as-is, with its expansion collected at the document
|
|
181
|
+
// end via `*[abbr]: title`.
|
|
182
|
+
this.collectedAbbreviations.set(node.text || '', meta.abbreviationTitle);
|
|
183
|
+
}
|
|
184
|
+
if (meta?.citationKey) {
|
|
185
|
+
text = `[@${String(meta.citationKey).replace(/[[\]\r\n]+/g, '')}]`;
|
|
186
|
+
}
|
|
121
187
|
return text;
|
|
122
188
|
}
|
|
123
189
|
case 'heading': {
|
|
@@ -137,13 +203,13 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
137
203
|
id = ` {#${this.slugify(this.getNodeText(node))}}`;
|
|
138
204
|
}
|
|
139
205
|
const anchors = this.config.mdConfig.fallbackToHtml
|
|
140
|
-
? remainingAnchors.map(aid => `<a name="${aid}"></a>`).join('')
|
|
206
|
+
? remainingAnchors.map(aid => `<a name="${this.slugify(aid)}"></a>`).join('')
|
|
141
207
|
: '';
|
|
142
208
|
let content = `${prefix}${childrenOutput}${id}`;
|
|
143
209
|
// Alignment fallback via HTML div/p
|
|
144
210
|
if (this.config.mdConfig.fallbackToHtml && meta?.alignment && meta.alignment !== 'left') {
|
|
145
211
|
// Use extra newlines to ensure Markdown inside the div is parsed
|
|
146
|
-
content = `<div style="text-align: ${meta.alignment}">\n\n${content}\n\n</div>`;
|
|
212
|
+
content = `<div style="text-align: ${(0, sanitize_js_1.sanitizeCssValue)(meta.alignment)}">\n\n${content}\n\n</div>`;
|
|
147
213
|
}
|
|
148
214
|
return `${anchors}${anchors ? '\n' : ''}${content}\n\n`;
|
|
149
215
|
}
|
|
@@ -153,7 +219,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
153
219
|
let content = childrenOutput;
|
|
154
220
|
// Alignment fallback via HTML div/p
|
|
155
221
|
if (this.config.mdConfig.fallbackToHtml && meta?.alignment && meta.alignment !== 'left') {
|
|
156
|
-
content = `<div style="text-align: ${meta.alignment}">${content}</div>`;
|
|
222
|
+
content = `<div style="text-align: ${(0, sanitize_js_1.sanitizeCssValue)(meta.alignment)}">${content}</div>`;
|
|
157
223
|
}
|
|
158
224
|
return childrenOutput ? `${anchors}${content}\n\n` : '';
|
|
159
225
|
}
|
|
@@ -161,7 +227,9 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
161
227
|
const meta = node.metadata;
|
|
162
228
|
const indentSpaces = ' '.repeat(4);
|
|
163
229
|
const indent = indentSpaces.repeat(meta?.indentation || 0);
|
|
164
|
-
const marker = meta?.
|
|
230
|
+
const marker = meta?.isTask
|
|
231
|
+
? (meta.checked ? '- [x] ' : '- [ ] ')
|
|
232
|
+
: (meta?.listType === 'ordered' ? `${(meta.itemIndex ?? 0) + 1}. ` : '- ');
|
|
165
233
|
const anchors = this.renderAnchors(meta);
|
|
166
234
|
return `${indent}${marker}${anchors}${childrenOutput}\n`;
|
|
167
235
|
}
|
|
@@ -179,11 +247,24 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
179
247
|
}
|
|
180
248
|
}
|
|
181
249
|
const anchors = this.renderAnchors(meta);
|
|
182
|
-
|
|
250
|
+
// Strip `[]` from alt (would close the `![...]`) and neutralize the URL scheme.
|
|
251
|
+
const safeAlt = (0, sanitize_js_1.markdownEscapeText)(alt).replace(/[[\]]/g, '');
|
|
252
|
+
const safeSrc = (0, sanitize_js_1.sanitizeMarkdownUrl)(src, { allowDataImage: true });
|
|
253
|
+
return `${anchors}${anchors ? '\n' : ''}${this.renderAttributeList(meta)}`;
|
|
183
254
|
}
|
|
184
255
|
case 'table': {
|
|
185
256
|
const anchors = this.renderAnchors(node.metadata);
|
|
186
257
|
const tableOutput = await this.renderMarkdownTable(node, processor);
|
|
258
|
+
// The HTML-fallback path (merged cells/nested tables) already carries
|
|
259
|
+
// data-align on the <table> tag directly - only the plain pipe-table
|
|
260
|
+
// form needs the attribute-list syntax to express alignment.
|
|
261
|
+
const usedHtmlFallback = this.config.mdConfig.fallbackToHtml && (this.hasNestedTable(node) || this.hasColspanOrRowspan(node));
|
|
262
|
+
const attrList = usedHtmlFallback ? '' : this.renderAttributeList(node.metadata);
|
|
263
|
+
if (attrList) {
|
|
264
|
+
// Must glue directly below the last row with no blank line, or
|
|
265
|
+
// MarkdownParser's block splitter won't see it as part of the same block.
|
|
266
|
+
return `${anchors}${anchors ? '\n' : ''}${tableOutput.replace(/\n+$/, '\n')}${attrList}\n`;
|
|
267
|
+
}
|
|
187
268
|
return `${anchors}${anchors ? '\n' : ''}${tableOutput}`;
|
|
188
269
|
}
|
|
189
270
|
case 'row':
|
|
@@ -196,13 +277,27 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
196
277
|
}
|
|
197
278
|
case 'code': {
|
|
198
279
|
const meta = node.metadata;
|
|
199
|
-
|
|
280
|
+
if (meta?.math === 'block') {
|
|
281
|
+
return `\n$$\n${node.text || ''}\n$$\n\n`;
|
|
282
|
+
}
|
|
283
|
+
if (meta?.math === 'inline') {
|
|
284
|
+
return `$${node.text || ''}$`;
|
|
285
|
+
}
|
|
286
|
+
const lang = (meta?.language || '').replace(/[\r\n`]+/g, '');
|
|
200
287
|
// Block code if it contains newlines, else inline
|
|
201
288
|
if (node.text && node.text.includes('\n')) {
|
|
202
|
-
|
|
289
|
+
// Fence with one more backtick than the longest run inside the content
|
|
290
|
+
// so an embedded ``` can't close the block early and inject markup.
|
|
291
|
+
const longestRun = Math.max(0, ...(node.text.match(/`+/g) || []).map(s => s.length));
|
|
292
|
+
const fence = '`'.repeat(Math.max(3, longestRun + 1));
|
|
293
|
+
return `\n${fence}${lang}\n${node.text}\n${fence}\n\n`;
|
|
203
294
|
}
|
|
204
295
|
else {
|
|
205
|
-
|
|
296
|
+
const t = node.text || '';
|
|
297
|
+
const longestRun = Math.max(0, ...(t.match(/`+/g) || []).map(s => s.length));
|
|
298
|
+
const fence = '`'.repeat(Math.max(1, longestRun + 1));
|
|
299
|
+
const pad = (t.startsWith('`') || t.endsWith('`')) ? ' ' : '';
|
|
300
|
+
return `${fence}${pad}${t}${pad}${fence} `;
|
|
206
301
|
}
|
|
207
302
|
}
|
|
208
303
|
case 'sheet': {
|
|
@@ -220,9 +315,40 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
220
315
|
}
|
|
221
316
|
case 'note': {
|
|
222
317
|
const meta = node.metadata;
|
|
223
|
-
|
|
224
|
-
|
|
318
|
+
if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
|
|
319
|
+
return `[^${this.getFootnoteKey(node)}]: ${childrenOutput.trim()}\n\n`;
|
|
320
|
+
}
|
|
321
|
+
return `> **Note:** ${childrenOutput.trim()}\n\n`;
|
|
322
|
+
}
|
|
323
|
+
case 'embed': {
|
|
324
|
+
// Markdown has no native embed syntax. When fallbackToHtml is on (our save
|
|
325
|
+
// default), emit the exact single-line div MarkdownParser recognises on
|
|
326
|
+
// reimport; otherwise degrade to a plain link.
|
|
327
|
+
const meta = node.metadata;
|
|
328
|
+
const id = meta?.videoId || '';
|
|
329
|
+
if (this.config.mdConfig.fallbackToHtml) {
|
|
330
|
+
const width = meta?.width ? ` data-width="${(0, sanitize_js_1.escapeHtml)(meta.width)}"` : '';
|
|
331
|
+
const align = meta?.align ? ` data-align="${(0, sanitize_js_1.escapeHtml)(meta.align)}"` : '';
|
|
332
|
+
return `\n<div data-youtube-video="${(0, sanitize_js_1.escapeHtml)(id)}"${width}${align}></div>\n\n`;
|
|
333
|
+
}
|
|
334
|
+
const url = meta?.url || (id ? `https://youtu.be/${id}` : '');
|
|
335
|
+
return url ? `[YouTube](${(0, sanitize_js_1.sanitizeMarkdownUrl)(url)})\n\n` : '';
|
|
336
|
+
}
|
|
337
|
+
case 'admonition': {
|
|
338
|
+
// Canonical output is always the GitHub blockquote form, even when the
|
|
339
|
+
// source was GLFM's `:::type` fenced-div - see MARKDOWN_DIALECT.md's Decisions.
|
|
340
|
+
const meta = node.metadata;
|
|
341
|
+
const label = (meta?.admonitionType || 'note').toUpperCase();
|
|
342
|
+
const body = childrenOutput.trim();
|
|
343
|
+
const quotedLines = body.split('\n').map(l => l.length > 0 ? `> ${l}` : '>').join('\n');
|
|
344
|
+
return `> [!${label}]\n${quotedLines}\n\n`;
|
|
225
345
|
}
|
|
346
|
+
case 'definitionList':
|
|
347
|
+
return `${childrenOutput}\n`;
|
|
348
|
+
case 'definitionTerm':
|
|
349
|
+
return `${childrenOutput}\n`;
|
|
350
|
+
case 'definitionDescription':
|
|
351
|
+
return `: ${childrenOutput}\n`;
|
|
226
352
|
default:
|
|
227
353
|
return childrenOutput;
|
|
228
354
|
}
|
|
@@ -253,6 +379,12 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
253
379
|
}
|
|
254
380
|
output += notesMd;
|
|
255
381
|
}
|
|
382
|
+
if (this.collectedAbbreviations.size > 0) {
|
|
383
|
+
output += '\n\n';
|
|
384
|
+
for (const [abbr, title] of this.collectedAbbreviations) {
|
|
385
|
+
output += `*[${abbr}]: ${title}\n`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
256
388
|
return {
|
|
257
389
|
value: (output + '\n\n' + this.hoistedContent.join('\n\n')).trim(),
|
|
258
390
|
messages: this.messages
|
|
@@ -290,6 +422,17 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
290
422
|
result += await this.processNodeRecursive(note, processor);
|
|
291
423
|
}
|
|
292
424
|
}
|
|
425
|
+
else if (node.notes && node.notes.length > 0) {
|
|
426
|
+
// Emit the [^id] reference marker at the point of reference. Without this,
|
|
427
|
+
// a footnote/endnote would only ever show up in the collected ### Notes
|
|
428
|
+
// section at the end, with no indication of where it was originally cited.
|
|
429
|
+
for (const note of node.notes) {
|
|
430
|
+
const meta = note.metadata;
|
|
431
|
+
if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
|
|
432
|
+
result += `[^${this.getFootnoteKey(note)}]`;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
293
436
|
return result;
|
|
294
437
|
}
|
|
295
438
|
/**
|
|
@@ -460,7 +603,11 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
460
603
|
rows += await this.renderTableAsHtml(row, await this.handleOnNode(row));
|
|
461
604
|
}
|
|
462
605
|
}
|
|
463
|
-
|
|
606
|
+
// Carry table-layout alignment through the HTML fallback so it isn't lost
|
|
607
|
+
// just because the table also needed HTML for merged cells.
|
|
608
|
+
const tableMeta = node.metadata;
|
|
609
|
+
const alignAttr = tableMeta?.align ? ` data-align="${(0, sanitize_js_1.escapeHtml)(tableMeta.align)}"` : '';
|
|
610
|
+
return `<table${alignAttr}>\n${rows}</table>\n`;
|
|
464
611
|
}
|
|
465
612
|
else if (node.type === 'row') {
|
|
466
613
|
let cells = '';
|
|
@@ -482,7 +629,9 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
482
629
|
content += await this.processNodeRecursive(child, async (n, co) => {
|
|
483
630
|
switch (n.type) {
|
|
484
631
|
case 'text': {
|
|
485
|
-
|
|
632
|
+
// Inside HTML table cells, entity-encode angle brackets so cell
|
|
633
|
+
// text can't inject a raw tag (e.g. </td><script>).
|
|
634
|
+
let text = (0, sanitize_js_1.markdownEscapeText)(n.text || '');
|
|
486
635
|
if (n.formatting?.bold)
|
|
487
636
|
text = `<b>${text}</b>`;
|
|
488
637
|
if (n.formatting?.italic)
|
|
@@ -497,7 +646,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
497
646
|
}
|
|
498
647
|
case 'paragraph': return `<p>${co}</p>`;
|
|
499
648
|
case 'heading': {
|
|
500
|
-
const level = n.metadata?.level || 1;
|
|
649
|
+
const level = Math.min(Math.max(Number(n.metadata?.level) || 1, 1), 6);
|
|
501
650
|
return `<h${level}>${co}</h${level}>`;
|
|
502
651
|
}
|
|
503
652
|
case 'table': return await this.renderTableAsHtml(n);
|
|
@@ -85,6 +85,35 @@ class PdfGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
85
85
|
// a system-installed Chrome, both of which are too intrusive for a library.
|
|
86
86
|
browser = await puppeteer.launch(launchOptions);
|
|
87
87
|
const page = await browser.newPage();
|
|
88
|
+
// Harden against SSRF: the HTML being rendered is derived from an untrusted
|
|
89
|
+
// document, and `networkidle0` would otherwise fetch every URL it references
|
|
90
|
+
// (external images, stylesheets, etc.) from this host — reaching internal
|
|
91
|
+
// services or a cloud metadata endpoint (169.254.169.254). Intercept requests
|
|
92
|
+
// and allow only inline data/blob URIs and the configured chart CDN; abort every
|
|
93
|
+
// other remote fetch.
|
|
94
|
+
const allowedHosts = new Set();
|
|
95
|
+
try {
|
|
96
|
+
const chartSrc = this.config.htmlConfig?.chartJsSrc;
|
|
97
|
+
if (chartSrc)
|
|
98
|
+
allowedHosts.add(new URL(chartSrc).host);
|
|
99
|
+
}
|
|
100
|
+
catch { /* no or invalid chart CDN configured */ }
|
|
101
|
+
let blockedRemoteResource = false;
|
|
102
|
+
await page.setRequestInterception(true);
|
|
103
|
+
page.on('request', (req) => {
|
|
104
|
+
const url = req.url();
|
|
105
|
+
if (url.startsWith('data:') || url.startsWith('blob:') || url.startsWith('about:')) {
|
|
106
|
+
return req.continue().catch(() => { });
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
if (allowedHosts.has(new URL(url).host)) {
|
|
110
|
+
return req.continue().catch(() => { });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch { /* unparseable URL — fall through and block */ }
|
|
114
|
+
blockedRemoteResource = true;
|
|
115
|
+
return req.abort().catch(() => { });
|
|
116
|
+
});
|
|
88
117
|
const pdfConfig = this.config.pdfConfig;
|
|
89
118
|
const timeout = pdfConfig.timeout;
|
|
90
119
|
if (timeout !== undefined && timeout > 0) {
|
|
@@ -107,6 +136,9 @@ class PdfGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
107
136
|
});
|
|
108
137
|
await browser.close();
|
|
109
138
|
browser = null;
|
|
139
|
+
if (blockedRemoteResource) {
|
|
140
|
+
this.warn(types_js_1.OfficeWarningType.BROWSER_GENERATION_LIMITATION, 'One or more remote resources referenced by the document were blocked during PDF rendering to prevent server-side request forgery (SSRF). Only inline images and the configured chart CDN are loaded.');
|
|
141
|
+
}
|
|
110
142
|
return {
|
|
111
143
|
value: new Uint8Array(pdfBuffer),
|
|
112
144
|
messages: this.messages
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RtfGenerator = void 0;
|
|
4
|
+
const sanitize_js_1 = require("../utils/sanitize.js");
|
|
4
5
|
const BaseGenerator_js_1 = require("./BaseGenerator.js");
|
|
5
6
|
/**
|
|
6
7
|
* Generates high-fidelity RTF (Rich Text Format) from an AST.
|
|
@@ -134,7 +135,7 @@ class RtfGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
134
135
|
if (meta?.link) {
|
|
135
136
|
const isInternal = meta.linkType !== 'external';
|
|
136
137
|
if (!this.config.ignoreInternalLinks || !isInternal) {
|
|
137
|
-
return `{\\field{\\*\\fldinst{HYPERLINK "${meta.link}"}}{\\fldrslt ${text}}}`;
|
|
138
|
+
return `{\\field{\\*\\fldinst{HYPERLINK "${this.escapeRtf(meta.link)}"}}{\\fldrslt ${text}}}`;
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
return text;
|
|
@@ -214,6 +215,15 @@ class RtfGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
214
215
|
case 'break': {
|
|
215
216
|
return node.metadata?.breakType === 'page' ? '\\page\n' : '\\line\n';
|
|
216
217
|
}
|
|
218
|
+
case 'embed': {
|
|
219
|
+
// RTF has no embed concept - degrade to the URL as plain text rather than
|
|
220
|
+
// silently dropping the node (it has no children to fall back to).
|
|
221
|
+
const meta = node.metadata;
|
|
222
|
+
if (!meta?.url)
|
|
223
|
+
return '';
|
|
224
|
+
const pPr = this.inTable ? '\\pard\\intbl' : '\\pard';
|
|
225
|
+
return `${pPr}\\sa120 ${this.escapeRtf(meta.url)}\\par\n`;
|
|
226
|
+
}
|
|
217
227
|
default:
|
|
218
228
|
return childrenOutput;
|
|
219
229
|
}
|
|
@@ -251,20 +261,7 @@ class RtfGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
251
261
|
return luminance > 0.8;
|
|
252
262
|
}
|
|
253
263
|
escapeRtf(text) {
|
|
254
|
-
return text
|
|
255
|
-
.replace(/\\/g, '\\\\')
|
|
256
|
-
.replace(/{/g, '\\{')
|
|
257
|
-
.replace(/}/g, '\\}')
|
|
258
|
-
.replace(/[^\x00-\x7F]/g, (match) => {
|
|
259
|
-
let code = match.charCodeAt(0);
|
|
260
|
-
if (code < 256) {
|
|
261
|
-
return `\\'${code.toString(16).padStart(2, '0')}`;
|
|
262
|
-
}
|
|
263
|
-
if (code > 32767) {
|
|
264
|
-
code -= 65536;
|
|
265
|
-
}
|
|
266
|
-
return `{\\uc0\\u${code}}`;
|
|
267
|
-
});
|
|
264
|
+
return (0, sanitize_js_1.escapeRtf)(text);
|
|
268
265
|
}
|
|
269
266
|
}
|
|
270
267
|
exports.RtfGenerator = RtfGenerator;
|
|
@@ -40,6 +40,17 @@ class TextGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
40
40
|
const meta = node.metadata;
|
|
41
41
|
return `[Image: ${meta?.altText || meta?.attachmentName || 'Untitled'}]${newline}`;
|
|
42
42
|
}
|
|
43
|
+
if (node.type === 'embed') {
|
|
44
|
+
const meta = node.metadata;
|
|
45
|
+
return meta?.url ? `[${meta.embedType === 'youtube' ? 'YouTube' : 'Embed'}: ${meta.url}]${newline}` : '';
|
|
46
|
+
}
|
|
47
|
+
if (node.type === 'admonition') {
|
|
48
|
+
const meta = node.metadata;
|
|
49
|
+
if (childrenOutput.trim() === '')
|
|
50
|
+
return '';
|
|
51
|
+
const label = (meta?.admonitionType || 'note').toUpperCase();
|
|
52
|
+
return `[${label}] ${childrenOutput.trim()}${newline}`;
|
|
53
|
+
}
|
|
43
54
|
if (node.type === 'table' && this.config.textConfig.preserveLayout) {
|
|
44
55
|
return await this.renderTable(node, processor, newline);
|
|
45
56
|
}
|
package/dist/index.d.ts
CHANGED