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
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HtmlGenerator = void 0;
|
|
4
4
|
const BaseGenerator_js_1 = require("./BaseGenerator.js");
|
|
5
|
+
const sanitize_js_1 = require("../utils/sanitize.js");
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes `HtmlGeneratorConfig.standalone` (`boolean | StandaloneConfig`) into a fully
|
|
8
|
+
* resolved object. `true`/undefined turns every part on (a complete standalone document);
|
|
9
|
+
* `false` turns every part off (a bare content fragment). When an object is passed, any field
|
|
10
|
+
* left unspecified defaults to its "on" value, matching the boolean-shorthand semantics.
|
|
11
|
+
*/
|
|
12
|
+
function resolveStandalone(standalone) {
|
|
13
|
+
const uniform = (on) => ({
|
|
14
|
+
document: on,
|
|
15
|
+
metaTags: on,
|
|
16
|
+
styles: on ? 'full' : 'none',
|
|
17
|
+
scripts: on,
|
|
18
|
+
headInjections: on,
|
|
19
|
+
bodyInjections: on,
|
|
20
|
+
});
|
|
21
|
+
if (standalone === undefined || typeof standalone === 'boolean') {
|
|
22
|
+
return uniform(standalone ?? true);
|
|
23
|
+
}
|
|
24
|
+
const on = uniform(true);
|
|
25
|
+
return {
|
|
26
|
+
document: standalone.document ?? on.document,
|
|
27
|
+
metaTags: standalone.metaTags ?? on.metaTags,
|
|
28
|
+
styles: standalone.styles ?? on.styles,
|
|
29
|
+
scripts: standalone.scripts ?? on.scripts,
|
|
30
|
+
headInjections: standalone.headInjections ?? on.headInjections,
|
|
31
|
+
bodyInjections: standalone.bodyInjections ?? on.bodyInjections,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
5
34
|
/**
|
|
6
35
|
* Generates semantic, high-fidelity HTML from an AST.
|
|
7
36
|
*/
|
|
@@ -29,11 +58,31 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
29
58
|
containerClass = 'pdf-container';
|
|
30
59
|
let bodyContent = await this.processNodeArray(this.ast.content);
|
|
31
60
|
if (this.collectedNotes.length > 0) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
// Footnotes/endnotes get their own <section data-footnotes> (the agreed
|
|
62
|
+
// contract with inscript-editor's footnote node); other note types (e.g.
|
|
63
|
+
// slide speaker notes) keep the existing generic notes wrapper.
|
|
64
|
+
const footnotes = this.collectedNotes.filter(n => {
|
|
65
|
+
const t = n.metadata?.noteType;
|
|
66
|
+
return t === 'footnote' || t === 'endnote';
|
|
67
|
+
});
|
|
68
|
+
const otherNotes = this.collectedNotes.filter(n => !footnotes.includes(n));
|
|
69
|
+
if (footnotes.length > 0) {
|
|
70
|
+
let footnotesHtml = '';
|
|
71
|
+
for (const note of footnotes) {
|
|
72
|
+
footnotesHtml += await this.processNodeRecursive(note, this.nodeProcessor.bind(this));
|
|
73
|
+
}
|
|
74
|
+
// data-footnotes carries an explicit empty value (not a bare attribute) so
|
|
75
|
+
// the markup is valid XHTML too - EpubGenerator embeds this verbatim, and
|
|
76
|
+
// XML rejects valueless attributes. HtmlParser only checks for presence.
|
|
77
|
+
bodyContent += `\n<section data-footnotes="">\n${footnotesHtml}\n</section>\n`;
|
|
78
|
+
}
|
|
79
|
+
if (otherNotes.length > 0) {
|
|
80
|
+
let notesHtml = '';
|
|
81
|
+
for (const note of otherNotes) {
|
|
82
|
+
notesHtml += await this.processNodeRecursive(note, this.nodeProcessor.bind(this));
|
|
83
|
+
}
|
|
84
|
+
bodyContent += `\n<div class="document-notes-section">\n<hr class="page-break">\n${notesHtml}\n</div>\n`;
|
|
35
85
|
}
|
|
36
|
-
bodyContent += `\n<div class="document-notes-section">\n<hr class="page-break">\n${notesHtml}\n</div>\n`;
|
|
37
86
|
}
|
|
38
87
|
const metadataBlock = this.config.renderMetadata ? this.renderMetadataSummary() : '';
|
|
39
88
|
let title = 'Document';
|
|
@@ -214,28 +263,38 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
214
263
|
else window.addEventListener('load', switchSheet);
|
|
215
264
|
</script>`;
|
|
216
265
|
}
|
|
217
|
-
|
|
266
|
+
const sa = resolveStandalone(this.config.htmlConfig.standalone);
|
|
267
|
+
if (sa.document && sa.metaTags) {
|
|
218
268
|
title = this.ast.metadata?.title || 'Document';
|
|
219
269
|
metaTags = this.renderMetaTags();
|
|
220
270
|
}
|
|
221
|
-
const
|
|
271
|
+
const styleBlock = sa.styles === 'none' ? '' : `<style>${sa.styles === 'scoped'
|
|
272
|
+
? this.getScopedPremiumStyles(this.isSpreadsheetMode, isPresentation, isPdf)
|
|
273
|
+
: this.getPremiumStyles(this.isSpreadsheetMode, isPresentation, isPdf)}</style>`;
|
|
274
|
+
// 'scoped' styles are anchored to this wrapper via CSS @scope, so custom properties and
|
|
275
|
+
// base body-level styling attach here instead of leaking onto a host page's real <html>/
|
|
276
|
+
// <body> when the output is embedded as a fragment.
|
|
277
|
+
const scopeOpen = sa.styles === 'scoped' ? '<div class="op-html-scope">' : '';
|
|
278
|
+
const scopeClose = sa.styles === 'scoped' ? '</div>' : '';
|
|
222
279
|
const inj = this.config.htmlConfig.injections;
|
|
223
|
-
const
|
|
280
|
+
const headInjectionsOn = sa.document && sa.headInjections;
|
|
281
|
+
const chartScriptTag = (sa.scripts && this.config.includeCharts) ? `<script src="${this.config.htmlConfig.chartJsSrc}"></script>` : '';
|
|
282
|
+
const spreadsheetScriptOut = sa.scripts ? spreadsheetScript : '';
|
|
283
|
+
const value = sa.document ? `<!DOCTYPE html>
|
|
224
284
|
<html lang="en">
|
|
225
285
|
<head>
|
|
226
|
-
${inj.headStart}
|
|
286
|
+
${headInjectionsOn ? inj.headStart : ''}
|
|
227
287
|
<meta charset="UTF-8">
|
|
228
288
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
229
289
|
<title>${this.escape(title)}</title>
|
|
230
290
|
${metaTags}
|
|
231
|
-
${
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
</style>
|
|
235
|
-
${inj.headEnd}
|
|
291
|
+
${chartScriptTag}
|
|
292
|
+
${styleBlock}
|
|
293
|
+
${headInjectionsOn ? inj.headEnd : ''}
|
|
236
294
|
</head>
|
|
237
295
|
<body>
|
|
238
|
-
${inj.bodyStart}
|
|
296
|
+
${sa.bodyInjections ? inj.bodyStart : ''}
|
|
297
|
+
${scopeOpen}
|
|
239
298
|
<div class="${containerClass}">
|
|
240
299
|
<article>
|
|
241
300
|
${metadataBlock}
|
|
@@ -243,10 +302,11 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
243
302
|
</article>
|
|
244
303
|
${spreadsheetTabs}
|
|
245
304
|
</div>
|
|
246
|
-
${
|
|
247
|
-
${
|
|
305
|
+
${scopeClose}
|
|
306
|
+
${spreadsheetScriptOut}
|
|
307
|
+
${sa.bodyInjections ? inj.bodyEnd : ''}
|
|
248
308
|
</body>
|
|
249
|
-
</html>` : `${
|
|
309
|
+
</html>` : `${styleBlock}${sa.bodyInjections ? inj.bodyStart : ''}${scopeOpen}<div class="${containerClass}">${metadataBlock}${bodyContent}${spreadsheetTabs}</div>${scopeClose}${spreadsheetScriptOut}${sa.bodyInjections ? inj.bodyEnd : ''}`;
|
|
250
310
|
return {
|
|
251
311
|
value,
|
|
252
312
|
messages: this.messages
|
|
@@ -261,10 +321,12 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
261
321
|
tags.push(`<meta name="author" content="${this.escape(m.author)}">`);
|
|
262
322
|
if (m.description)
|
|
263
323
|
tags.push(`<meta name="description" content="${this.escape(m.description)}">`);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
tags.push(`<meta name="dcterms.
|
|
324
|
+
const created = this.toIsoDate(m.created);
|
|
325
|
+
const modified = this.toIsoDate(m.modified);
|
|
326
|
+
if (created)
|
|
327
|
+
tags.push(`<meta name="dcterms.created" content="${created}">`);
|
|
328
|
+
if (modified)
|
|
329
|
+
tags.push(`<meta name="dcterms.modified" content="${modified}">`);
|
|
268
330
|
if (m.lastModifiedBy)
|
|
269
331
|
tags.push(`<meta name="lastModifiedBy" content="${this.escape(m.lastModifiedBy)}">`);
|
|
270
332
|
if (m.customProperties) {
|
|
@@ -312,12 +374,18 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
312
374
|
*/
|
|
313
375
|
async processNodeArray(nodes) {
|
|
314
376
|
let html = '';
|
|
315
|
-
// Stack to track active lists: { indentation, type }
|
|
377
|
+
// Stack to track active lists: { indentation, type, isTask }
|
|
316
378
|
const listStack = [];
|
|
379
|
+
const openListTag = (type, isTask) => {
|
|
380
|
+
if (isTask)
|
|
381
|
+
return '<ul data-type="taskList">';
|
|
382
|
+
return type === 'ordered' ? '<ol>' : '<ul>';
|
|
383
|
+
};
|
|
384
|
+
const closeListTag = (type) => type === 'ordered' ? '</ol>' : '</ul>';
|
|
317
385
|
const closeListsToLevel = (level) => {
|
|
318
386
|
while (listStack.length > 0 && listStack[listStack.length - 1].indentation > level) {
|
|
319
387
|
const list = listStack.pop();
|
|
320
|
-
html += (list
|
|
388
|
+
html += closeListTag(list.type) + '\n\n';
|
|
321
389
|
}
|
|
322
390
|
};
|
|
323
391
|
for (const node of nodes) {
|
|
@@ -329,23 +397,24 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
329
397
|
if (node.type === 'list') {
|
|
330
398
|
const meta = node.metadata;
|
|
331
399
|
const type = meta?.listType === 'ordered' ? 'ordered' : 'unordered';
|
|
400
|
+
const isTask = !!meta?.isTask;
|
|
332
401
|
const indentation = meta?.indentation || 0;
|
|
333
402
|
// Close deeper lists
|
|
334
403
|
closeListsToLevel(indentation);
|
|
335
404
|
// Handle current level
|
|
336
405
|
if (listStack.length > 0 && listStack[listStack.length - 1].indentation === indentation) {
|
|
337
|
-
if (listStack[listStack.length - 1].type !== type) {
|
|
406
|
+
if (listStack[listStack.length - 1].type !== type || listStack[listStack.length - 1].isTask !== isTask) {
|
|
338
407
|
// Type changed at same level
|
|
339
408
|
const last = listStack.pop();
|
|
340
|
-
html += (last
|
|
341
|
-
html += (type
|
|
342
|
-
listStack.push({ indentation, type });
|
|
409
|
+
html += closeListTag(last.type) + '\n';
|
|
410
|
+
html += openListTag(type, isTask) + '\n';
|
|
411
|
+
listStack.push({ indentation, type, isTask });
|
|
343
412
|
}
|
|
344
413
|
}
|
|
345
414
|
else {
|
|
346
415
|
// Start a new nested list
|
|
347
|
-
html += (type
|
|
348
|
-
listStack.push({ indentation, type });
|
|
416
|
+
html += openListTag(type, isTask) + '\n';
|
|
417
|
+
listStack.push({ indentation, type, isTask });
|
|
349
418
|
}
|
|
350
419
|
html += await this.processNodeRecursive(node, this.nodeProcessor.bind(this), override);
|
|
351
420
|
}
|
|
@@ -405,6 +474,18 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
405
474
|
result += await this.processNodeRecursive(note, processor);
|
|
406
475
|
}
|
|
407
476
|
}
|
|
477
|
+
else if (node.notes && node.notes.length > 0) {
|
|
478
|
+
// Emit the reference marker at the point of citation. Without this, a
|
|
479
|
+
// footnote/endnote would only ever show up in the collected footnotes
|
|
480
|
+
// section, with no indication of where it was originally cited.
|
|
481
|
+
for (const note of node.notes) {
|
|
482
|
+
const meta = note.metadata;
|
|
483
|
+
if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
|
|
484
|
+
const key = this.escape(this.getFootnoteKey(note));
|
|
485
|
+
result += `<sup data-footnote-ref="${key}" id="footnote-ref-${key}"><a href="#footnote-${key}">${key}</a></sup>`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
408
489
|
return result;
|
|
409
490
|
}
|
|
410
491
|
/**
|
|
@@ -442,9 +523,9 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
442
523
|
}
|
|
443
524
|
}
|
|
444
525
|
if (anchorIds.length > 0) {
|
|
445
|
-
idAttr = ` id="${anchorIds[0]}"`;
|
|
526
|
+
idAttr = ` id="${this.escape(anchorIds[0])}"`;
|
|
446
527
|
if (anchorIds.length > 1) {
|
|
447
|
-
extraAnchors = anchorIds.slice(1).map(aid => `<a id="${aid}" name="${aid}"></a>`).join('');
|
|
528
|
+
extraAnchors = anchorIds.slice(1).map(aid => `<a id="${this.escape(aid)}" name="${this.escape(aid)}"></a>`).join('');
|
|
448
529
|
}
|
|
449
530
|
}
|
|
450
531
|
// Inline Styles for structural nodes
|
|
@@ -469,7 +550,30 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
469
550
|
src = `data:${attachment.mimeType || 'image/png'};base64,${attachment.data}`;
|
|
470
551
|
}
|
|
471
552
|
}
|
|
472
|
-
|
|
553
|
+
// Match CustomImage's exact data-width/data-align + style contract so a loaded
|
|
554
|
+
// image re-hydrates the editor node without losing size/alignment.
|
|
555
|
+
let imgDataAttrs = '';
|
|
556
|
+
const imgStyleParts = [];
|
|
557
|
+
const baseImgStyle = this.getInlineStyles(node);
|
|
558
|
+
if (baseImgStyle)
|
|
559
|
+
imgStyleParts.push(baseImgStyle);
|
|
560
|
+
if (meta?.width) {
|
|
561
|
+
imgDataAttrs += ` data-width="${this.escape(meta.width)}"`;
|
|
562
|
+
// Sanitize before it enters the style="" attribute: an unescaped width
|
|
563
|
+
// (e.g. `1px" onerror="alert(1)`) would otherwise break out and inject an
|
|
564
|
+
// event handler, and a CSS `url(...)` would fetch a remote resource.
|
|
565
|
+
const safeWidth = (0, sanitize_js_1.sanitizeCssValue)(meta.width);
|
|
566
|
+
if (safeWidth)
|
|
567
|
+
imgStyleParts.push(`width: ${safeWidth}`);
|
|
568
|
+
}
|
|
569
|
+
if (meta?.align) {
|
|
570
|
+
imgDataAttrs += ` data-align="${this.escape(meta.align)}"`;
|
|
571
|
+
const ml = meta.align === 'left' ? '0' : 'auto';
|
|
572
|
+
const mr = meta.align === 'right' ? '0' : 'auto';
|
|
573
|
+
imgStyleParts.push('display: block', `margin-left: ${ml}`, `margin-right: ${mr}`);
|
|
574
|
+
}
|
|
575
|
+
const imgStyleAttr = imgStyleParts.length > 0 ? ` style="${imgStyleParts.join('; ')}"` : '';
|
|
576
|
+
const img = `<img src="${(0, sanitize_js_1.sanitizeImageUrl)(src)}" alt="${this.escape(node.text || meta?.altText || '')}"${className}${mappedAttrs}${imgDataAttrs}${imgStyleAttr}>`;
|
|
473
577
|
const content = this.config.includeFormatting ? `<div class="image-container">${img}<div class="caption">${this.escape(attachmentName || '')}</div></div>` : img;
|
|
474
578
|
return `${extraAnchors}<div${idAttr}>${content}</div>`;
|
|
475
579
|
}
|
|
@@ -489,7 +593,7 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
489
593
|
(function() {
|
|
490
594
|
const initChart = () => {
|
|
491
595
|
const ctx = document.getElementById('${chartId}').getContext('2d');
|
|
492
|
-
const chartData = ${
|
|
596
|
+
const chartData = ${(0, sanitize_js_1.serializeForInlineScript)(chartData)};
|
|
493
597
|
const getRandomColor = (index, alpha) => {
|
|
494
598
|
const colors = [
|
|
495
599
|
'rgba(255, 99, 132, ' + alpha + ')',
|
|
@@ -556,6 +660,15 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
556
660
|
return node.metadata?.breakType === 'page' ? '<hr class="page-break">' : '<br>';
|
|
557
661
|
case 'code': {
|
|
558
662
|
const meta = node.metadata;
|
|
663
|
+
if (meta?.math) {
|
|
664
|
+
// No pinned editor contract yet (inscript-editor's math node is v1.2,
|
|
665
|
+
// not built) - this is the proposed shape: data-math signals the
|
|
666
|
+
// display mode, and the visible text keeps its $ delimiters so the
|
|
667
|
+
// raw LaTeX degrades gracefully without a KaTeX renderer.
|
|
668
|
+
const delimited = meta.math === 'block' ? `$$${node.text || ''}$$` : `$${node.text || ''}$`;
|
|
669
|
+
const tag = meta.math === 'block' ? 'div' : 'span';
|
|
670
|
+
return `${extraAnchors}<${tag} class="math math-${this.escape(meta.math)}" data-math="${this.escape(meta.math)}"${idAttr}${mappedAttrs}${styleAttr}>${this.escape(delimited)}</${tag}>`;
|
|
671
|
+
}
|
|
559
672
|
const lang = meta?.language ? ` class="language-${this.escape(meta.language)}"` : '';
|
|
560
673
|
const codeHtml = `<code${lang}>${this.escape(node.text || '')}</code>`;
|
|
561
674
|
if (node.text && node.text.includes('\n')) {
|
|
@@ -567,6 +680,11 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
567
680
|
}
|
|
568
681
|
case 'list': {
|
|
569
682
|
const meta = node.metadata;
|
|
683
|
+
if (meta?.isTask) {
|
|
684
|
+
const checkedAttr = ` data-checked="${meta.checked ? 'true' : 'false'}"`;
|
|
685
|
+
const checkedBool = meta.checked ? ' checked' : '';
|
|
686
|
+
return `${extraAnchors}<li${checkedAttr}${idAttr}${className}${mappedAttrs}${styleAttr}><label><input type="checkbox"${checkedBool}><span></span></label><div>${childrenOutput}</div></li>`;
|
|
687
|
+
}
|
|
570
688
|
const value = (meta?.listType === 'ordered' && typeof meta.itemIndex === 'number')
|
|
571
689
|
? ` value="${meta.itemIndex + 1}"`
|
|
572
690
|
: '';
|
|
@@ -595,7 +713,21 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
595
713
|
finalChildren = `<thead>${headOutput}</thead><tbody>${bodyOutput}</tbody>`;
|
|
596
714
|
}
|
|
597
715
|
}
|
|
598
|
-
|
|
716
|
+
// Match CustomTable's exact data-align + margin style contract so a loaded
|
|
717
|
+
// table re-hydrates the editor node without losing its layout alignment.
|
|
718
|
+
const tableMeta = node.metadata;
|
|
719
|
+
let tableDataAttrs = '';
|
|
720
|
+
let tableStyleAttr = styleAttr;
|
|
721
|
+
if (tableMeta?.align) {
|
|
722
|
+
tableDataAttrs = ` data-align="${this.escape(tableMeta.align)}"`;
|
|
723
|
+
const ml = tableMeta.align === 'left' ? '0' : 'auto';
|
|
724
|
+
const mr = tableMeta.align === 'right' ? '0' : 'auto';
|
|
725
|
+
const marginStyle = `margin-left: ${ml}; margin-right: ${mr}`;
|
|
726
|
+
tableStyleAttr = styleAttr
|
|
727
|
+
? ` style="${styleAttr.replace(/^ style="|"$/g, '')}; ${marginStyle}"`
|
|
728
|
+
: ` style="${marginStyle}"`;
|
|
729
|
+
}
|
|
730
|
+
const tableHtml = `<table${idAttr}${className}${mappedAttrs}${tableDataAttrs}${tableStyleAttr}>${finalChildren}</table>`;
|
|
599
731
|
const result = this.tableNestingLevel > 1 ? tableHtml : `<div class="table-container">${tableHtml}</div>`;
|
|
600
732
|
return `${extraAnchors}${result}`;
|
|
601
733
|
}
|
|
@@ -714,7 +846,7 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
714
846
|
}
|
|
715
847
|
const rAnchorIds = this.config.ignoreInternalLinks ? [] : [...(rowNode.metadata?.anchorIds || [])];
|
|
716
848
|
if (rAnchorIds.length > 0) {
|
|
717
|
-
trAttrs += ` id="${rAnchorIds[0]}"`;
|
|
849
|
+
trAttrs += ` id="${this.escape(rAnchorIds[0])}"`;
|
|
718
850
|
}
|
|
719
851
|
if (this.config.includeFormatting) {
|
|
720
852
|
const styles = this.getInlineStyles(rowNode);
|
|
@@ -779,19 +911,50 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
779
911
|
}
|
|
780
912
|
case 'slide': {
|
|
781
913
|
const meta = node.metadata;
|
|
782
|
-
const slideNum = meta?.slideNumber || '';
|
|
914
|
+
const slideNum = this.escape(String(meta?.slideNumber || ''));
|
|
783
915
|
return `${extraAnchors}<section class="slide" data-slide-num="${slideNum}"${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</section>`;
|
|
784
916
|
}
|
|
785
917
|
case 'page': {
|
|
786
918
|
const meta = node.metadata;
|
|
787
|
-
const pageNum = meta?.pageNumber || '';
|
|
919
|
+
const pageNum = this.escape(String(meta?.pageNumber || ''));
|
|
788
920
|
return `${extraAnchors}<section class="page" data-page-num="${pageNum}"${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</section>`;
|
|
789
921
|
}
|
|
790
922
|
case 'note': {
|
|
791
923
|
const meta = node.metadata;
|
|
792
|
-
|
|
924
|
+
if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
|
|
925
|
+
const key = this.escape(this.getFootnoteKey(node));
|
|
926
|
+
return `<p id="footnote-${key}" data-footnote-id="${key}">${childrenOutput} <a href="#footnote-ref-${key}">↩</a></p>`;
|
|
927
|
+
}
|
|
928
|
+
const noteClass = meta?.noteType ? ` note-${this.escape(meta.noteType)}` : '';
|
|
793
929
|
return `${extraAnchors}<div class="slide-note${noteClass}"${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</div>`;
|
|
794
930
|
}
|
|
931
|
+
case 'embed': {
|
|
932
|
+
// Match the Youtube extension's exact wrapper shape so a loaded embed
|
|
933
|
+
// re-hydrates the editor's Youtube node.
|
|
934
|
+
const meta = node.metadata;
|
|
935
|
+
const id = meta?.videoId || '';
|
|
936
|
+
const width = meta?.width || '100%';
|
|
937
|
+
const align = meta?.align || 'center';
|
|
938
|
+
const ml = align === 'left' ? '0' : 'auto';
|
|
939
|
+
const mr = align === 'right' ? '0' : 'auto';
|
|
940
|
+
const iframe = id
|
|
941
|
+
? `<iframe src="https://www.youtube.com/embed/${this.escape(id)}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>`
|
|
942
|
+
: '';
|
|
943
|
+
return `${extraAnchors}<div data-youtube-video="${this.escape(id)}" data-width="${this.escape(width)}" data-align="${this.escape(align)}" class="youtube-embed"${idAttr}${mappedAttrs} style="width: ${(0, sanitize_js_1.sanitizeCssValue)(width)}; margin-left: ${ml}; margin-right: ${mr};">${iframe}</div>`;
|
|
944
|
+
}
|
|
945
|
+
case 'admonition': {
|
|
946
|
+
// Match inscript-editor's Admonition node wrapper so a loaded admonition
|
|
947
|
+
// reaches the editor as that node instead of a plain blockquote.
|
|
948
|
+
const meta = node.metadata;
|
|
949
|
+
const admonitionType = this.escape(meta?.admonitionType || 'note');
|
|
950
|
+
return `${extraAnchors}<div class="admonition admonition-${admonitionType}" data-type="${admonitionType}"${idAttr}${mappedAttrs}${styleAttr}>${childrenOutput}</div>`;
|
|
951
|
+
}
|
|
952
|
+
case 'definitionList':
|
|
953
|
+
return `${extraAnchors}<dl${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</dl>`;
|
|
954
|
+
case 'definitionTerm':
|
|
955
|
+
return `${extraAnchors}<dt${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</dt>`;
|
|
956
|
+
case 'definitionDescription':
|
|
957
|
+
return `${extraAnchors}<dd${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</dd>`;
|
|
795
958
|
default:
|
|
796
959
|
return childrenOutput;
|
|
797
960
|
}
|
|
@@ -829,24 +992,50 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
829
992
|
}
|
|
830
993
|
}
|
|
831
994
|
const meta = node.metadata;
|
|
832
|
-
if (meta?.
|
|
995
|
+
if (meta?.wikilink) {
|
|
996
|
+
// No pinned editor contract yet for wikilinks - data-wikilink-page preserves the
|
|
997
|
+
// exact page name separately from the display text/alias and from href (which
|
|
998
|
+
// the host app's resolver may rewrite to a real URL).
|
|
999
|
+
if (!this.config.ignoreInternalLinks) {
|
|
1000
|
+
result = `<a href="#${this.escape(this.slugify(meta.link || ''))}" data-wikilink-page="${this.escape(meta.link || '')}">${result}</a>`;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
else if (meta?.link) {
|
|
833
1004
|
const isInternal = meta.linkType !== 'external';
|
|
834
1005
|
if (!this.config.ignoreInternalLinks || !isInternal) {
|
|
835
|
-
result = `<a href="${meta.link}"${meta.linkType === 'external' ? ' target="_blank"' : ''}>${result}</a>`;
|
|
1006
|
+
result = `<a href="${(0, sanitize_js_1.sanitizeUrl)(meta.link)}"${meta.linkType === 'external' ? ' target="_blank"' : ''}>${result}</a>`;
|
|
836
1007
|
}
|
|
837
1008
|
}
|
|
1009
|
+
if (meta?.abbreviationTitle) {
|
|
1010
|
+
result = `<abbr title="${this.escape(meta.abbreviationTitle)}">${result}</abbr>`;
|
|
1011
|
+
}
|
|
1012
|
+
if (meta?.citationKey) {
|
|
1013
|
+
// No pinned editor contract yet for citations (unlike footnotes/admonitions) -
|
|
1014
|
+
// this is the proposed shape: a <cite> carrying the bare key, matching Pandoc's
|
|
1015
|
+
// [@citekey] on the Markdown side. Keep in sync if inscript-editor's citation
|
|
1016
|
+
// node lands with a different attribute name.
|
|
1017
|
+
result = `<cite data-citation-key="${this.escape(meta.citationKey)}">[@${this.escape(meta.citationKey)}]</cite>`;
|
|
1018
|
+
}
|
|
838
1019
|
return result;
|
|
839
1020
|
}
|
|
840
1021
|
getInlineStyles(node) {
|
|
841
1022
|
const styles = [];
|
|
1023
|
+
// Colors/sizes/fonts/alignments are free strings from an untrusted document;
|
|
1024
|
+
// run each through sanitizeCssValue so it can't break out of the style="" attribute
|
|
1025
|
+
// or inject a resource-fetching CSS construct. Drop the declaration if nothing survives.
|
|
1026
|
+
const pushSafe = (prop, value) => {
|
|
1027
|
+
const safe = (0, sanitize_js_1.sanitizeCssValue)(value);
|
|
1028
|
+
if (safe)
|
|
1029
|
+
styles.push(`${prop}: ${safe}`);
|
|
1030
|
+
};
|
|
842
1031
|
if (node.metadata) {
|
|
843
1032
|
const meta = node.metadata;
|
|
844
1033
|
if (meta.alignment)
|
|
845
|
-
|
|
1034
|
+
pushSafe('text-align', meta.alignment);
|
|
846
1035
|
if (meta.backgroundColor)
|
|
847
|
-
|
|
1036
|
+
pushSafe('background-color', meta.backgroundColor);
|
|
848
1037
|
if (meta.verticalAlign)
|
|
849
|
-
|
|
1038
|
+
pushSafe('vertical-align', meta.verticalAlign);
|
|
850
1039
|
if (meta.paragraphIndentation) {
|
|
851
1040
|
const ind = meta.paragraphIndentation;
|
|
852
1041
|
if (ind.left)
|
|
@@ -860,13 +1049,16 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
860
1049
|
if (node.formatting) {
|
|
861
1050
|
const f = node.formatting;
|
|
862
1051
|
if (f.color)
|
|
863
|
-
|
|
1052
|
+
pushSafe('color', f.color);
|
|
864
1053
|
if (f.backgroundColor)
|
|
865
|
-
|
|
1054
|
+
pushSafe('background-color', f.backgroundColor);
|
|
866
1055
|
if (f.size)
|
|
867
|
-
|
|
868
|
-
if (f.font)
|
|
869
|
-
|
|
1056
|
+
pushSafe('font-size', f.size);
|
|
1057
|
+
if (f.font) {
|
|
1058
|
+
const safeFont = (0, sanitize_js_1.sanitizeCssValue)(f.font);
|
|
1059
|
+
if (safeFont)
|
|
1060
|
+
styles.push(`font-family: ${safeFont}, sans-serif`);
|
|
1061
|
+
}
|
|
870
1062
|
}
|
|
871
1063
|
return styles.join('; ');
|
|
872
1064
|
}
|
|
@@ -1431,6 +1623,20 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1431
1623
|
${this.config.htmlConfig.customCss}
|
|
1432
1624
|
`;
|
|
1433
1625
|
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Same as `getPremiumStyles()`, but wrapped in a CSS `@scope` block anchored to the
|
|
1628
|
+
* `.op-html-scope` wrapper so the rules only apply within the generated fragment - they
|
|
1629
|
+
* cannot leak onto a host page's own elements. `:root` and `body` selectors specifically
|
|
1630
|
+
* target the real page root/body, so they're remapped to `:scope` (the scope root, i.e. the
|
|
1631
|
+
* `.op-html-scope` wrapper) first; every other selector is naturally confined by `@scope`
|
|
1632
|
+
* without needing per-selector rewriting. `customCss` is included in this scoping too.
|
|
1633
|
+
*/
|
|
1634
|
+
getScopedPremiumStyles(isSpreadsheet = false, isPresentation = false, isPdf = false) {
|
|
1635
|
+
const css = this.getPremiumStyles(isSpreadsheet, isPresentation, isPdf)
|
|
1636
|
+
.replace(/:root(\s*\{)/g, ':scope$1')
|
|
1637
|
+
.replace(/(^|\n)(\s*)body(\s*\{)/g, '$1$2:scope$3');
|
|
1638
|
+
return `@scope (.op-html-scope) {\n${css}\n}`;
|
|
1639
|
+
}
|
|
1434
1640
|
slugify(text) {
|
|
1435
1641
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
1436
1642
|
}
|
|
@@ -1443,10 +1649,19 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1443
1649
|
}
|
|
1444
1650
|
return letter;
|
|
1445
1651
|
}
|
|
1652
|
+
// Attribute/text escaping, URL sanitizing, and inline-script serialization all
|
|
1653
|
+
// live in ../utils/sanitize.js so every generator shares one implementation.
|
|
1654
|
+
// escape() stays as a thin wrapper because it has many call sites here.
|
|
1446
1655
|
escape(text) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1656
|
+
return (0, sanitize_js_1.escapeHtml)(text);
|
|
1657
|
+
}
|
|
1658
|
+
/** Converts a document-supplied date to an ISO string, or '' if it is invalid
|
|
1659
|
+
* (a malformed date would otherwise throw a RangeError and abort generation). */
|
|
1660
|
+
toIsoDate(value) {
|
|
1661
|
+
if (value === undefined || value === null || value === '')
|
|
1662
|
+
return '';
|
|
1663
|
+
const d = new Date(value);
|
|
1664
|
+
return isNaN(d.getTime()) ? '' : d.toISOString();
|
|
1450
1665
|
}
|
|
1451
1666
|
}
|
|
1452
1667
|
exports.HtmlGenerator = HtmlGenerator;
|
|
@@ -27,11 +27,27 @@ import { BaseGenerator } from './BaseGenerator.js';
|
|
|
27
27
|
export declare class MarkdownGenerator extends BaseGenerator<'md'> {
|
|
28
28
|
private isInsideTable;
|
|
29
29
|
private hoistedContent;
|
|
30
|
+
private collectedAbbreviations;
|
|
30
31
|
constructor(ast: OfficeParserAST, config?: GeneratorConfig<'md'>);
|
|
31
32
|
/**
|
|
32
33
|
* Renders anchor tags if HTML fallback is allowed.
|
|
33
34
|
*/
|
|
34
35
|
private renderAnchors;
|
|
36
|
+
/**
|
|
37
|
+
* Serializes a frontmatter array as a YAML flow sequence (e.g. `[a, b]`), matching
|
|
38
|
+
* MarkdownParser's frontmatter array handling. Plain strings are left bare; anything
|
|
39
|
+
* that would break flow-array syntax (or isn't a string) falls back to JSON encoding.
|
|
40
|
+
*/
|
|
41
|
+
private serializeFrontmatterArray;
|
|
42
|
+
/**
|
|
43
|
+
* Renders a Pandoc-style attribute list (e.g. `{width=50% align=left}`) from
|
|
44
|
+
* ImageMetadata/TableMetadata's width/align fields - the canonical form is always
|
|
45
|
+
* `key=value`, matching MarkdownParser's own vocabulary (MARKDOWN_DIALECT.md §15).
|
|
46
|
+
*/
|
|
47
|
+
private renderAttributeList;
|
|
48
|
+
/** Converts a document-supplied date to an ISO string, or '' if invalid
|
|
49
|
+
* (a malformed date would otherwise throw a RangeError and abort generation). */
|
|
50
|
+
private toIsoDate;
|
|
35
51
|
/**
|
|
36
52
|
* Generates Markdown string from the provided AST.
|
|
37
53
|
*
|