officeparser 7.2.3 → 7.4.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 +277 -17
- package/dist/OfficeConverter.d.ts +1 -1
- package/dist/OfficeConverter.js +3 -0
- 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 +5 -2
- package/dist/defaults.js +12 -0
- package/dist/generators/BaseGenerator.d.ts +34 -1
- package/dist/generators/BaseGenerator.js +98 -0
- package/dist/generators/CsvGenerator.d.ts +9 -1
- package/dist/generators/CsvGenerator.js +28 -16
- package/dist/generators/EpubGenerator.d.ts +43 -0
- package/dist/generators/EpubGenerator.js +312 -0
- package/dist/generators/HtmlGenerator.d.ts +12 -0
- package/dist/generators/HtmlGenerator.js +378 -61
- package/dist/generators/MarkdownGenerator.d.ts +28 -5
- package/dist/generators/MarkdownGenerator.js +432 -51
- package/dist/generators/PdfGenerator.js +32 -0
- package/dist/generators/RtfGenerator.js +47 -22
- package/dist/generators/TextGenerator.js +98 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/officeparser.browser.d.ts +427 -20
- package/dist/officeparser.browser.iife.js +338 -206
- package/dist/officeparser.browser.mjs +346 -214
- package/dist/officeparser.browser.slim.d.ts +427 -20
- package/dist/officeparser.browser.slim.iife.js +346 -214
- package/dist/officeparser.browser.slim.mjs +346 -214
- package/dist/parsers/EpubParser.d.ts +8 -0
- package/dist/parsers/EpubParser.js +217 -0
- package/dist/parsers/ExcelParser.js +2 -0
- package/dist/parsers/HtmlParser.js +507 -48
- package/dist/parsers/MarkdownParser.js +704 -92
- package/dist/parsers/OpenOfficeParser.js +128 -20
- package/dist/parsers/PdfParser.js +4 -1
- package/dist/parsers/PowerPointParser.js +1 -0
- package/dist/parsers/WordParser.js +1 -0
- package/dist/sbom.cdx.json +1695 -0
- package/dist/types.d.ts +427 -20
- package/dist/types.js +8 -0
- package/dist/utils/configUtils.js +53 -4
- package/dist/utils/errorUtils.js +7 -3
- package/dist/utils/sanitize.d.ts +139 -0
- package/dist/utils/sanitize.js +318 -0
- package/dist/utils/xmlUtils.js +2 -2
- package/dist/utils/zipUtils.js +76 -26
- package/package.json +16 -12
|
@@ -1,7 +1,87 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HtmlGenerator = void 0;
|
|
4
|
+
const types_js_1 = require("../types.js");
|
|
4
5
|
const BaseGenerator_js_1 = require("./BaseGenerator.js");
|
|
6
|
+
const errorUtils_js_1 = require("../utils/errorUtils.js");
|
|
7
|
+
const sanitize_js_1 = require("../utils/sanitize.js");
|
|
8
|
+
/**
|
|
9
|
+
* Attributes that carry a URL and therefore must go through `sanitizeUrl` rather than plain
|
|
10
|
+
* escaping - an escaped `javascript:` payload is still a `javascript:` payload.
|
|
11
|
+
*/
|
|
12
|
+
const URL_BEARING_ATTRS = new Set([
|
|
13
|
+
'href', 'src', 'srcset', 'action', 'formaction', 'poster', 'cite', 'data', 'background', 'ping',
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* Renders `node.htmlAttributes` (see `BaseContentNode.htmlAttributes`) as an attribute string.
|
|
17
|
+
*
|
|
18
|
+
* This re-applies the parser's filtering rather than trusting it, because an AST can be built
|
|
19
|
+
* programmatically and handed straight to the generator - the parse-side pass is defence in depth,
|
|
20
|
+
* not the only gate. `class` is returned separately so the caller can merge it into the class
|
|
21
|
+
* attribute it already composes: emitting a second `class=` would be invalid HTML and, worse, a
|
|
22
|
+
* *fatal* XML well-formedness error once EpubGenerator converts the output to XHTML.
|
|
23
|
+
*/
|
|
24
|
+
function renderHtmlAttributeBag(node, alreadyEmitted = []) {
|
|
25
|
+
const bag = node.htmlAttributes;
|
|
26
|
+
if (!bag)
|
|
27
|
+
return { attrs: '' };
|
|
28
|
+
const taken = new Set([...alreadyEmitted].map(k => k.toLowerCase()));
|
|
29
|
+
let attrs = '';
|
|
30
|
+
let className;
|
|
31
|
+
for (const [rawKey, rawValue] of Object.entries(bag)) {
|
|
32
|
+
const key = rawKey.toLowerCase();
|
|
33
|
+
// Same policy as the parser, restated here because this path is independently reachable.
|
|
34
|
+
if (/^on/i.test(key))
|
|
35
|
+
continue;
|
|
36
|
+
if (key === 'srcdoc' || key === 'style' || key === 'id')
|
|
37
|
+
continue;
|
|
38
|
+
if (!(0, sanitize_js_1.isSafeHtmlAttributeName)(key))
|
|
39
|
+
continue;
|
|
40
|
+
if (taken.has(key))
|
|
41
|
+
continue;
|
|
42
|
+
if (key === 'class') {
|
|
43
|
+
className = String(rawValue);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (URL_BEARING_ATTRS.has(key)) {
|
|
47
|
+
const safe = (0, sanitize_js_1.sanitizeUrl)(String(rawValue));
|
|
48
|
+
if (!safe)
|
|
49
|
+
continue;
|
|
50
|
+
attrs += ` ${key}="${safe}"`;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
attrs += ` ${key}="${(0, sanitize_js_1.escapeHtml)(String(rawValue))}"`;
|
|
54
|
+
}
|
|
55
|
+
return { attrs, className };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Normalizes `HtmlGeneratorConfig.standalone` (`boolean | StandaloneConfig`) into a fully
|
|
59
|
+
* resolved object. `true`/undefined turns every part on (a complete standalone document);
|
|
60
|
+
* `false` turns every part off (a bare content fragment). When an object is passed, any field
|
|
61
|
+
* left unspecified defaults to its "on" value, matching the boolean-shorthand semantics.
|
|
62
|
+
*/
|
|
63
|
+
function resolveStandalone(standalone) {
|
|
64
|
+
const uniform = (on) => ({
|
|
65
|
+
document: on,
|
|
66
|
+
metaTags: on,
|
|
67
|
+
styles: on ? 'full' : 'none',
|
|
68
|
+
scripts: on,
|
|
69
|
+
headInjections: on,
|
|
70
|
+
bodyInjections: on,
|
|
71
|
+
});
|
|
72
|
+
if (standalone === undefined || typeof standalone === 'boolean') {
|
|
73
|
+
return uniform(standalone ?? true);
|
|
74
|
+
}
|
|
75
|
+
const on = uniform(true);
|
|
76
|
+
return {
|
|
77
|
+
document: standalone.document ?? on.document,
|
|
78
|
+
metaTags: standalone.metaTags ?? on.metaTags,
|
|
79
|
+
styles: standalone.styles ?? on.styles,
|
|
80
|
+
scripts: standalone.scripts ?? on.scripts,
|
|
81
|
+
headInjections: standalone.headInjections ?? on.headInjections,
|
|
82
|
+
bodyInjections: standalone.bodyInjections ?? on.bodyInjections,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
5
85
|
/**
|
|
6
86
|
* Generates semantic, high-fidelity HTML from an AST.
|
|
7
87
|
*/
|
|
@@ -29,11 +109,31 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
29
109
|
containerClass = 'pdf-container';
|
|
30
110
|
let bodyContent = await this.processNodeArray(this.ast.content);
|
|
31
111
|
if (this.collectedNotes.length > 0) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
112
|
+
// Footnotes/endnotes get their own <section data-footnotes> (the agreed
|
|
113
|
+
// contract with inscript-editor's footnote node); other note types (e.g.
|
|
114
|
+
// slide speaker notes) keep the existing generic notes wrapper.
|
|
115
|
+
const footnotes = this.collectedNotes.filter(n => {
|
|
116
|
+
const t = n.metadata?.noteType;
|
|
117
|
+
return t === 'footnote' || t === 'endnote';
|
|
118
|
+
});
|
|
119
|
+
const otherNotes = this.collectedNotes.filter(n => !footnotes.includes(n));
|
|
120
|
+
if (footnotes.length > 0) {
|
|
121
|
+
let footnotesHtml = '';
|
|
122
|
+
for (const note of footnotes) {
|
|
123
|
+
footnotesHtml += await this.processNodeRecursive(note, this.nodeProcessor.bind(this));
|
|
124
|
+
}
|
|
125
|
+
// data-footnotes carries an explicit empty value (not a bare attribute) so
|
|
126
|
+
// the markup is valid XHTML too - EpubGenerator embeds this verbatim, and
|
|
127
|
+
// XML rejects valueless attributes. HtmlParser only checks for presence.
|
|
128
|
+
bodyContent += `\n<section data-footnotes="">\n${footnotesHtml}\n</section>\n`;
|
|
129
|
+
}
|
|
130
|
+
if (otherNotes.length > 0) {
|
|
131
|
+
let notesHtml = '';
|
|
132
|
+
for (const note of otherNotes) {
|
|
133
|
+
notesHtml += await this.processNodeRecursive(note, this.nodeProcessor.bind(this));
|
|
134
|
+
}
|
|
135
|
+
bodyContent += `\n<div class="document-notes-section">\n<hr class="page-break">\n${notesHtml}\n</div>\n`;
|
|
35
136
|
}
|
|
36
|
-
bodyContent += `\n<div class="document-notes-section">\n<hr class="page-break">\n${notesHtml}\n</div>\n`;
|
|
37
137
|
}
|
|
38
138
|
const metadataBlock = this.config.renderMetadata ? this.renderMetadataSummary() : '';
|
|
39
139
|
let title = 'Document';
|
|
@@ -214,28 +314,38 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
214
314
|
else window.addEventListener('load', switchSheet);
|
|
215
315
|
</script>`;
|
|
216
316
|
}
|
|
217
|
-
|
|
218
|
-
|
|
317
|
+
const sa = resolveStandalone(this.config.htmlConfig.standalone);
|
|
318
|
+
if (sa.document && sa.metaTags) {
|
|
319
|
+
title = this.effectiveMetadata.title || 'Document';
|
|
219
320
|
metaTags = this.renderMetaTags();
|
|
220
321
|
}
|
|
221
|
-
const
|
|
322
|
+
const styleBlock = sa.styles === 'none' ? '' : `<style>${sa.styles === 'scoped'
|
|
323
|
+
? this.getScopedPremiumStyles(this.isSpreadsheetMode, isPresentation, isPdf)
|
|
324
|
+
: this.getPremiumStyles(this.isSpreadsheetMode, isPresentation, isPdf)}</style>`;
|
|
325
|
+
// 'scoped' styles are anchored to this wrapper via CSS @scope, so custom properties and
|
|
326
|
+
// base body-level styling attach here instead of leaking onto a host page's real <html>/
|
|
327
|
+
// <body> when the output is embedded as a fragment.
|
|
328
|
+
const scopeOpen = sa.styles === 'scoped' ? '<div class="op-html-scope">' : '';
|
|
329
|
+
const scopeClose = sa.styles === 'scoped' ? '</div>' : '';
|
|
222
330
|
const inj = this.config.htmlConfig.injections;
|
|
223
|
-
const
|
|
331
|
+
const headInjectionsOn = sa.document && sa.headInjections;
|
|
332
|
+
const chartScriptTag = (sa.scripts && this.config.includeCharts) ? `<script src="${this.config.htmlConfig.chartJsSrc}"></script>` : '';
|
|
333
|
+
const spreadsheetScriptOut = sa.scripts ? spreadsheetScript : '';
|
|
334
|
+
const value = sa.document ? `<!DOCTYPE html>
|
|
224
335
|
<html lang="en">
|
|
225
336
|
<head>
|
|
226
|
-
${inj.headStart}
|
|
337
|
+
${headInjectionsOn ? inj.headStart : ''}
|
|
227
338
|
<meta charset="UTF-8">
|
|
228
339
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
229
340
|
<title>${this.escape(title)}</title>
|
|
230
341
|
${metaTags}
|
|
231
|
-
${
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
</style>
|
|
235
|
-
${inj.headEnd}
|
|
342
|
+
${chartScriptTag}
|
|
343
|
+
${styleBlock}
|
|
344
|
+
${headInjectionsOn ? inj.headEnd : ''}
|
|
236
345
|
</head>
|
|
237
346
|
<body>
|
|
238
|
-
${inj.bodyStart}
|
|
347
|
+
${sa.bodyInjections ? inj.bodyStart : ''}
|
|
348
|
+
${scopeOpen}
|
|
239
349
|
<div class="${containerClass}">
|
|
240
350
|
<article>
|
|
241
351
|
${metadataBlock}
|
|
@@ -243,10 +353,11 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
243
353
|
</article>
|
|
244
354
|
${spreadsheetTabs}
|
|
245
355
|
</div>
|
|
246
|
-
${
|
|
247
|
-
${
|
|
356
|
+
${scopeClose}
|
|
357
|
+
${spreadsheetScriptOut}
|
|
358
|
+
${sa.bodyInjections ? inj.bodyEnd : ''}
|
|
248
359
|
</body>
|
|
249
|
-
</html>` : `${
|
|
360
|
+
</html>` : `${styleBlock}${sa.bodyInjections ? inj.bodyStart : ''}${scopeOpen}<div class="${containerClass}">${metadataBlock}${bodyContent}${spreadsheetTabs}</div>${scopeClose}${spreadsheetScriptOut}${sa.bodyInjections ? inj.bodyEnd : ''}`;
|
|
250
361
|
return {
|
|
251
362
|
value,
|
|
252
363
|
messages: this.messages
|
|
@@ -255,18 +366,27 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
255
366
|
renderMetaTags() {
|
|
256
367
|
if (!this.ast?.metadata)
|
|
257
368
|
return '';
|
|
258
|
-
const m = this.
|
|
369
|
+
const m = this.effectiveMetadata;
|
|
259
370
|
const tags = [];
|
|
260
371
|
if (m.author)
|
|
261
372
|
tags.push(`<meta name="author" content="${this.escape(m.author)}">`);
|
|
262
373
|
if (m.description)
|
|
263
374
|
tags.push(`<meta name="description" content="${this.escape(m.description)}">`);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
tags.push(`<meta name="dcterms.
|
|
375
|
+
const created = this.toIsoDate(m.created);
|
|
376
|
+
const modified = this.toIsoDate(m.modified);
|
|
377
|
+
if (created)
|
|
378
|
+
tags.push(`<meta name="dcterms.created" content="${created}">`);
|
|
379
|
+
if (modified)
|
|
380
|
+
tags.push(`<meta name="dcterms.modified" content="${modified}">`);
|
|
268
381
|
if (m.lastModifiedBy)
|
|
269
382
|
tags.push(`<meta name="lastModifiedBy" content="${this.escape(m.lastModifiedBy)}">`);
|
|
383
|
+
// Both are parsed from every OOXML/ODF core-properties block but had no output sink at
|
|
384
|
+
// all, so they were silently dropped on every conversion. `keywords` is the standard HTML
|
|
385
|
+
// meta name; `subject` has no standard equivalent, so it uses its Dublin Core name.
|
|
386
|
+
if (m.keywords)
|
|
387
|
+
tags.push(`<meta name="keywords" content="${this.escape(m.keywords)}">`);
|
|
388
|
+
if (m.subject)
|
|
389
|
+
tags.push(`<meta name="DC.subject" content="${this.escape(m.subject)}">`);
|
|
270
390
|
if (m.customProperties) {
|
|
271
391
|
for (const [key, val] of Object.entries(m.customProperties)) {
|
|
272
392
|
tags.push(`<meta name="custom:${this.escape(key)}" content="${this.escape(String(val))}">`);
|
|
@@ -277,7 +397,7 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
277
397
|
renderMetadataSummary() {
|
|
278
398
|
if (!this.ast?.metadata)
|
|
279
399
|
return '';
|
|
280
|
-
const m = this.
|
|
400
|
+
const m = this.effectiveMetadata;
|
|
281
401
|
let customPropsHtml = '';
|
|
282
402
|
if (m.customProperties && Object.keys(m.customProperties).length > 0) {
|
|
283
403
|
const items = Object.entries(m.customProperties)
|
|
@@ -312,12 +432,18 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
312
432
|
*/
|
|
313
433
|
async processNodeArray(nodes) {
|
|
314
434
|
let html = '';
|
|
315
|
-
// Stack to track active lists: { indentation, type }
|
|
435
|
+
// Stack to track active lists: { indentation, type, isTask }
|
|
316
436
|
const listStack = [];
|
|
437
|
+
const openListTag = (type, isTask) => {
|
|
438
|
+
if (isTask)
|
|
439
|
+
return '<ul data-type="taskList">';
|
|
440
|
+
return type === 'ordered' ? '<ol>' : '<ul>';
|
|
441
|
+
};
|
|
442
|
+
const closeListTag = (type) => type === 'ordered' ? '</ol>' : '</ul>';
|
|
317
443
|
const closeListsToLevel = (level) => {
|
|
318
444
|
while (listStack.length > 0 && listStack[listStack.length - 1].indentation > level) {
|
|
319
445
|
const list = listStack.pop();
|
|
320
|
-
html += (list
|
|
446
|
+
html += closeListTag(list.type) + '\n\n';
|
|
321
447
|
}
|
|
322
448
|
};
|
|
323
449
|
for (const node of nodes) {
|
|
@@ -329,23 +455,24 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
329
455
|
if (node.type === 'list') {
|
|
330
456
|
const meta = node.metadata;
|
|
331
457
|
const type = meta?.listType === 'ordered' ? 'ordered' : 'unordered';
|
|
458
|
+
const isTask = !!meta?.isTask;
|
|
332
459
|
const indentation = meta?.indentation || 0;
|
|
333
460
|
// Close deeper lists
|
|
334
461
|
closeListsToLevel(indentation);
|
|
335
462
|
// Handle current level
|
|
336
463
|
if (listStack.length > 0 && listStack[listStack.length - 1].indentation === indentation) {
|
|
337
|
-
if (listStack[listStack.length - 1].type !== type) {
|
|
464
|
+
if (listStack[listStack.length - 1].type !== type || listStack[listStack.length - 1].isTask !== isTask) {
|
|
338
465
|
// Type changed at same level
|
|
339
466
|
const last = listStack.pop();
|
|
340
|
-
html += (last
|
|
341
|
-
html += (type
|
|
342
|
-
listStack.push({ indentation, type });
|
|
467
|
+
html += closeListTag(last.type) + '\n';
|
|
468
|
+
html += openListTag(type, isTask) + '\n';
|
|
469
|
+
listStack.push({ indentation, type, isTask });
|
|
343
470
|
}
|
|
344
471
|
}
|
|
345
472
|
else {
|
|
346
473
|
// Start a new nested list
|
|
347
|
-
html += (type
|
|
348
|
-
listStack.push({ indentation, type });
|
|
474
|
+
html += openListTag(type, isTask) + '\n';
|
|
475
|
+
listStack.push({ indentation, type, isTask });
|
|
349
476
|
}
|
|
350
477
|
html += await this.processNodeRecursive(node, this.nodeProcessor.bind(this), override);
|
|
351
478
|
}
|
|
@@ -371,6 +498,10 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
371
498
|
*/
|
|
372
499
|
tableNestingLevel = 0;
|
|
373
500
|
async processNodeRecursive(node, processor, override) {
|
|
501
|
+
// Mirrors the check in BaseGenerator.processNodeRecursive. This override replaces that
|
|
502
|
+
// method entirely, so without repeating the check here the signal would be silently
|
|
503
|
+
// inert for this generator - which is exactly how it was missed.
|
|
504
|
+
(0, errorUtils_js_1.checkAbortSignal)(this.config.abortSignal);
|
|
374
505
|
// Use pre-evaluated override if provided, otherwise call handleOnNode
|
|
375
506
|
const actualOverride = override !== undefined ? override : await this.handleOnNode(node);
|
|
376
507
|
// Returning false skips the node and its children
|
|
@@ -405,6 +536,18 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
405
536
|
result += await this.processNodeRecursive(note, processor);
|
|
406
537
|
}
|
|
407
538
|
}
|
|
539
|
+
else if (node.notes && node.notes.length > 0) {
|
|
540
|
+
// Emit the reference marker at the point of citation. Without this, a
|
|
541
|
+
// footnote/endnote would only ever show up in the collected footnotes
|
|
542
|
+
// section, with no indication of where it was originally cited.
|
|
543
|
+
for (const note of node.notes) {
|
|
544
|
+
const meta = note.metadata;
|
|
545
|
+
if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
|
|
546
|
+
const key = this.escape(this.getFootnoteKey(note));
|
|
547
|
+
result += `<sup data-footnote-ref="${key}" id="footnote-ref-${key}"><a href="#footnote-${key}">${key}</a></sup>`;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
408
551
|
return result;
|
|
409
552
|
}
|
|
410
553
|
/**
|
|
@@ -413,17 +556,48 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
413
556
|
async nodeProcessor(node, childrenOutput) {
|
|
414
557
|
// Handle Style Mapping using the semantic mapping helper
|
|
415
558
|
const mapping = this.getSemanticMapping(node);
|
|
416
|
-
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
559
|
+
// A styleMap's `output.tag` was previously written here and then shadowed by a `const tag`
|
|
560
|
+
// in every switch branch, so HtmlGenerator silently ignored it - while MarkdownGenerator
|
|
561
|
+
// and RtfGenerator both honoured it and the README documented it as working. Honoured now,
|
|
562
|
+
// but only through the allowlist: the tag is interpolated into both `<TAG>` and `</TAG>`,
|
|
563
|
+
// and the shadowing bug was the sole reason a hostile value could not inject. A rejected
|
|
564
|
+
// tag falls back to the default rather than being emitted.
|
|
565
|
+
const mappedTag = (0, sanitize_js_1.isSafeStyleMapTag)(mapping?.tag) ? mapping.tag.toLowerCase() : undefined;
|
|
566
|
+
if (mapping?.tag && !mappedTag) {
|
|
567
|
+
this.warn(types_js_1.OfficeWarningType.INVALID_STYLE_MAP_TAG, mapping.tag, node);
|
|
568
|
+
}
|
|
420
569
|
// Handle Attributes from mapping
|
|
421
570
|
let mappedAttrs = '';
|
|
422
571
|
if (mapping?.attributes) {
|
|
423
572
|
for (const [key, val] of Object.entries(mapping.attributes)) {
|
|
573
|
+
// The value is escaped, but the NAME needs validating too: a key containing a
|
|
574
|
+
// quote closes the attribute and opens another, so `x" onmouseover="alert(1)" z`
|
|
575
|
+
// yields a live event handler however carefully the value is escaped. The
|
|
576
|
+
// attribute bag and the parser already apply this policy; this path did not.
|
|
577
|
+
if (!(0, sanitize_js_1.isSafeHtmlAttributeName)(key))
|
|
578
|
+
continue;
|
|
424
579
|
mappedAttrs += ` ${key}="${this.escape(val)}"`;
|
|
425
580
|
}
|
|
426
581
|
}
|
|
582
|
+
// Preserved source attributes (opt-in; absent unless htmlParserConfig.preserveAttributes).
|
|
583
|
+
// Dedupe against whatever the mapping already emitted so a typed field always wins, and
|
|
584
|
+
// take the bag's `class` back as a value to merge below rather than a second attribute.
|
|
585
|
+
const bag = renderHtmlAttributeBag(node, Object.keys(mapping?.attributes || {}));
|
|
586
|
+
// Fold into mappedAttrs rather than threading a separate fragment through all ~20 emission
|
|
587
|
+
// sites: every site already interpolates mappedAttrs, so this cannot miss one (a miss would
|
|
588
|
+
// silently drop preserved attributes), and an empty bag contributes nothing, so output for
|
|
589
|
+
// nodes without one stays byte-identical.
|
|
590
|
+
mappedAttrs += bag.attrs;
|
|
591
|
+
// Combine classes from mapping, defaults, and any preserved source class. Merging here is
|
|
592
|
+
// what keeps `<p class="lead">` from either losing "lead" or emitting a duplicate `class`.
|
|
593
|
+
const classes = mapping?.classes ? [...mapping.classes] : [];
|
|
594
|
+
if (bag.className) {
|
|
595
|
+
for (const c of bag.className.split(/\s+/).filter(Boolean)) {
|
|
596
|
+
if (!classes.includes(c))
|
|
597
|
+
classes.push(c);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const className = classes.length > 0 ? ` class="${this.escape(classes.join(' '))}"` : '';
|
|
427
601
|
// Handle ID and Anchors
|
|
428
602
|
let idAttr = '';
|
|
429
603
|
let extraAnchors = '';
|
|
@@ -442,9 +616,9 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
442
616
|
}
|
|
443
617
|
}
|
|
444
618
|
if (anchorIds.length > 0) {
|
|
445
|
-
idAttr = ` id="${anchorIds[0]}"`;
|
|
619
|
+
idAttr = ` id="${this.escape(anchorIds[0])}"`;
|
|
446
620
|
if (anchorIds.length > 1) {
|
|
447
|
-
extraAnchors = anchorIds.slice(1).map(aid => `<a id="${aid}" name="${aid}"></a>`).join('');
|
|
621
|
+
extraAnchors = anchorIds.slice(1).map(aid => `<a id="${this.escape(aid)}" name="${this.escape(aid)}"></a>`).join('');
|
|
448
622
|
}
|
|
449
623
|
}
|
|
450
624
|
// Inline Styles for structural nodes
|
|
@@ -469,7 +643,30 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
469
643
|
src = `data:${attachment.mimeType || 'image/png'};base64,${attachment.data}`;
|
|
470
644
|
}
|
|
471
645
|
}
|
|
472
|
-
|
|
646
|
+
// Match CustomImage's exact data-width/data-align + style contract so a loaded
|
|
647
|
+
// image re-hydrates the editor node without losing size/alignment.
|
|
648
|
+
let imgDataAttrs = '';
|
|
649
|
+
const imgStyleParts = [];
|
|
650
|
+
const baseImgStyle = this.getInlineStyles(node);
|
|
651
|
+
if (baseImgStyle)
|
|
652
|
+
imgStyleParts.push(baseImgStyle);
|
|
653
|
+
if (meta?.width) {
|
|
654
|
+
imgDataAttrs += ` data-width="${this.escape(meta.width)}"`;
|
|
655
|
+
// Sanitize before it enters the style="" attribute: an unescaped width
|
|
656
|
+
// (e.g. `1px" onerror="alert(1)`) would otherwise break out and inject an
|
|
657
|
+
// event handler, and a CSS `url(...)` would fetch a remote resource.
|
|
658
|
+
const safeWidth = (0, sanitize_js_1.sanitizeCssValue)(meta.width);
|
|
659
|
+
if (safeWidth)
|
|
660
|
+
imgStyleParts.push(`width: ${safeWidth}`);
|
|
661
|
+
}
|
|
662
|
+
if (meta?.align) {
|
|
663
|
+
imgDataAttrs += ` data-align="${this.escape(meta.align)}"`;
|
|
664
|
+
const ml = meta.align === 'left' ? '0' : 'auto';
|
|
665
|
+
const mr = meta.align === 'right' ? '0' : 'auto';
|
|
666
|
+
imgStyleParts.push('display: block', `margin-left: ${ml}`, `margin-right: ${mr}`);
|
|
667
|
+
}
|
|
668
|
+
const imgStyleAttr = imgStyleParts.length > 0 ? ` style="${imgStyleParts.join('; ')}"` : '';
|
|
669
|
+
const img = `<img src="${(0, sanitize_js_1.sanitizeImageUrl)(src)}" alt="${this.escape(node.text || meta?.altText || '')}"${className}${mappedAttrs}${imgDataAttrs}${imgStyleAttr}>`;
|
|
473
670
|
const content = this.config.includeFormatting ? `<div class="image-container">${img}<div class="caption">${this.escape(attachmentName || '')}</div></div>` : img;
|
|
474
671
|
return `${extraAnchors}<div${idAttr}>${content}</div>`;
|
|
475
672
|
}
|
|
@@ -489,7 +686,7 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
489
686
|
(function() {
|
|
490
687
|
const initChart = () => {
|
|
491
688
|
const ctx = document.getElementById('${chartId}').getContext('2d');
|
|
492
|
-
const chartData = ${
|
|
689
|
+
const chartData = ${(0, sanitize_js_1.serializeForInlineScript)(chartData)};
|
|
493
690
|
const getRandomColor = (index, alpha) => {
|
|
494
691
|
const colors = [
|
|
495
692
|
'rgba(255, 99, 132, ' + alpha + ')',
|
|
@@ -556,6 +753,15 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
556
753
|
return node.metadata?.breakType === 'page' ? '<hr class="page-break">' : '<br>';
|
|
557
754
|
case 'code': {
|
|
558
755
|
const meta = node.metadata;
|
|
756
|
+
if (meta?.math) {
|
|
757
|
+
// No pinned editor contract yet (inscript-editor's math node is v1.2,
|
|
758
|
+
// not built) - this is the proposed shape: data-math signals the
|
|
759
|
+
// display mode, and the visible text keeps its $ delimiters so the
|
|
760
|
+
// raw LaTeX degrades gracefully without a KaTeX renderer.
|
|
761
|
+
const delimited = meta.math === 'block' ? `$$${node.text || ''}$$` : `$${node.text || ''}$`;
|
|
762
|
+
const tag = meta.math === 'block' ? 'div' : 'span';
|
|
763
|
+
return `${extraAnchors}<${tag} class="math math-${this.escape(meta.math)}" data-math="${this.escape(meta.math)}"${idAttr}${mappedAttrs}${styleAttr}>${this.escape(delimited)}</${tag}>`;
|
|
764
|
+
}
|
|
559
765
|
const lang = meta?.language ? ` class="language-${this.escape(meta.language)}"` : '';
|
|
560
766
|
const codeHtml = `<code${lang}>${this.escape(node.text || '')}</code>`;
|
|
561
767
|
if (node.text && node.text.includes('\n')) {
|
|
@@ -567,6 +773,11 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
567
773
|
}
|
|
568
774
|
case 'list': {
|
|
569
775
|
const meta = node.metadata;
|
|
776
|
+
if (meta?.isTask) {
|
|
777
|
+
const checkedAttr = ` data-checked="${meta.checked ? 'true' : 'false'}"`;
|
|
778
|
+
const checkedBool = meta.checked ? ' checked' : '';
|
|
779
|
+
return `${extraAnchors}<li${checkedAttr}${idAttr}${className}${mappedAttrs}${styleAttr}><label><input type="checkbox"${checkedBool}><span></span></label><div>${childrenOutput}</div></li>`;
|
|
780
|
+
}
|
|
570
781
|
const value = (meta?.listType === 'ordered' && typeof meta.itemIndex === 'number')
|
|
571
782
|
? ` value="${meta.itemIndex + 1}"`
|
|
572
783
|
: '';
|
|
@@ -595,7 +806,21 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
595
806
|
finalChildren = `<thead>${headOutput}</thead><tbody>${bodyOutput}</tbody>`;
|
|
596
807
|
}
|
|
597
808
|
}
|
|
598
|
-
|
|
809
|
+
// Match CustomTable's exact data-align + margin style contract so a loaded
|
|
810
|
+
// table re-hydrates the editor node without losing its layout alignment.
|
|
811
|
+
const tableMeta = node.metadata;
|
|
812
|
+
let tableDataAttrs = '';
|
|
813
|
+
let tableStyleAttr = styleAttr;
|
|
814
|
+
if (tableMeta?.align) {
|
|
815
|
+
tableDataAttrs = ` data-align="${this.escape(tableMeta.align)}"`;
|
|
816
|
+
const ml = tableMeta.align === 'left' ? '0' : 'auto';
|
|
817
|
+
const mr = tableMeta.align === 'right' ? '0' : 'auto';
|
|
818
|
+
const marginStyle = `margin-left: ${ml}; margin-right: ${mr}`;
|
|
819
|
+
tableStyleAttr = styleAttr
|
|
820
|
+
? ` style="${styleAttr.replace(/^ style="|"$/g, '')}; ${marginStyle}"`
|
|
821
|
+
: ` style="${marginStyle}"`;
|
|
822
|
+
}
|
|
823
|
+
const tableHtml = `<table${idAttr}${className}${mappedAttrs}${tableDataAttrs}${tableStyleAttr}>${finalChildren}</table>`;
|
|
599
824
|
const result = this.tableNestingLevel > 1 ? tableHtml : `<div class="table-container">${tableHtml}</div>`;
|
|
600
825
|
return `${extraAnchors}${result}`;
|
|
601
826
|
}
|
|
@@ -706,15 +931,20 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
706
931
|
const rClasses = ['excel-row'];
|
|
707
932
|
if (mapping?.classes)
|
|
708
933
|
rClasses.push(...mapping.classes);
|
|
709
|
-
|
|
934
|
+
// Escaped like the `className` built for every other node type. This
|
|
935
|
+
// path rebuilds the class attribute from the raw mapping array rather
|
|
936
|
+
// than reusing that value, and was the only place it went out unescaped.
|
|
937
|
+
trAttrs += ` class="${this.escape(rClasses.join(' '))}"`;
|
|
710
938
|
if (mapping?.attributes) {
|
|
711
939
|
for (const [key, val] of Object.entries(mapping.attributes)) {
|
|
940
|
+
if (!(0, sanitize_js_1.isSafeHtmlAttributeName)(key))
|
|
941
|
+
continue;
|
|
712
942
|
trAttrs += ` ${key}="${this.escape(val)}"`;
|
|
713
943
|
}
|
|
714
944
|
}
|
|
715
945
|
const rAnchorIds = this.config.ignoreInternalLinks ? [] : [...(rowNode.metadata?.anchorIds || [])];
|
|
716
946
|
if (rAnchorIds.length > 0) {
|
|
717
|
-
trAttrs += ` id="${rAnchorIds[0]}"`;
|
|
947
|
+
trAttrs += ` id="${this.escape(rAnchorIds[0])}"`;
|
|
718
948
|
}
|
|
719
949
|
if (this.config.includeFormatting) {
|
|
720
950
|
const styles = this.getInlineStyles(rowNode);
|
|
@@ -760,14 +990,18 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
760
990
|
mergedClasses.push('active');
|
|
761
991
|
if (classes.length > 0)
|
|
762
992
|
mergedClasses.push(...classes);
|
|
763
|
-
|
|
993
|
+
// Escaped for the same reason as the row above; `classes` here also carries the
|
|
994
|
+
// attribute bag's raw className, so escaping at the join covers both sources.
|
|
995
|
+
const classAttr = ` class="${this.escape(mergedClasses.join(' '))}"`;
|
|
764
996
|
// Ensure we don't have duplicate IDs
|
|
765
997
|
const finalIdAttr = ` id="${sheetId}"`;
|
|
766
998
|
return `${extraAnchors}<div${finalIdAttr}${classAttr}${mappedAttrs}${styleAttr}>${tableHtml}${nonRowHtml}</div>`;
|
|
767
999
|
}
|
|
768
1000
|
case 'paragraph':
|
|
769
1001
|
case 'heading': {
|
|
770
|
-
|
|
1002
|
+
// The styleMap tag wins over the structural default; that is the whole point of
|
|
1003
|
+
// mapping "Heading 1"/"Intense Quote" onto a semantic element.
|
|
1004
|
+
const tag = mappedTag ?? (node.type === 'heading' ? `h${node.metadata?.level || 1}` : 'p');
|
|
771
1005
|
// Normalize empty paragraphs so DOCX and PPTX empty cells render with consistent height
|
|
772
1006
|
// Strip tags to check if it's purely empty or just contains non-breaking spaces (like PPTX)
|
|
773
1007
|
const textOnly = childrenOutput.replace(/<[^>]+>/g, '').trim();
|
|
@@ -779,19 +1013,50 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
779
1013
|
}
|
|
780
1014
|
case 'slide': {
|
|
781
1015
|
const meta = node.metadata;
|
|
782
|
-
const slideNum = meta?.slideNumber || '';
|
|
1016
|
+
const slideNum = this.escape(String(meta?.slideNumber || ''));
|
|
783
1017
|
return `${extraAnchors}<section class="slide" data-slide-num="${slideNum}"${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</section>`;
|
|
784
1018
|
}
|
|
785
1019
|
case 'page': {
|
|
786
1020
|
const meta = node.metadata;
|
|
787
|
-
const pageNum = meta?.pageNumber || '';
|
|
1021
|
+
const pageNum = this.escape(String(meta?.pageNumber || ''));
|
|
788
1022
|
return `${extraAnchors}<section class="page" data-page-num="${pageNum}"${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</section>`;
|
|
789
1023
|
}
|
|
790
1024
|
case 'note': {
|
|
791
1025
|
const meta = node.metadata;
|
|
792
|
-
|
|
1026
|
+
if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
|
|
1027
|
+
const key = this.escape(this.getFootnoteKey(node));
|
|
1028
|
+
return `<p id="footnote-${key}" data-footnote-id="${key}">${childrenOutput} <a href="#footnote-ref-${key}">↩</a></p>`;
|
|
1029
|
+
}
|
|
1030
|
+
const noteClass = meta?.noteType ? ` note-${this.escape(meta.noteType)}` : '';
|
|
793
1031
|
return `${extraAnchors}<div class="slide-note${noteClass}"${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</div>`;
|
|
794
1032
|
}
|
|
1033
|
+
case 'embed': {
|
|
1034
|
+
// Match the Youtube extension's exact wrapper shape so a loaded embed
|
|
1035
|
+
// re-hydrates the editor's Youtube node.
|
|
1036
|
+
const meta = node.metadata;
|
|
1037
|
+
const id = meta?.videoId || '';
|
|
1038
|
+
const width = meta?.width || '100%';
|
|
1039
|
+
const align = meta?.align || 'center';
|
|
1040
|
+
const ml = align === 'left' ? '0' : 'auto';
|
|
1041
|
+
const mr = align === 'right' ? '0' : 'auto';
|
|
1042
|
+
const iframe = id
|
|
1043
|
+
? `<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>`
|
|
1044
|
+
: '';
|
|
1045
|
+
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>`;
|
|
1046
|
+
}
|
|
1047
|
+
case 'admonition': {
|
|
1048
|
+
// Match inscript-editor's Admonition node wrapper so a loaded admonition
|
|
1049
|
+
// reaches the editor as that node instead of a plain blockquote.
|
|
1050
|
+
const meta = node.metadata;
|
|
1051
|
+
const admonitionType = this.escape(meta?.admonitionType || 'note');
|
|
1052
|
+
return `${extraAnchors}<div class="admonition admonition-${admonitionType}" data-type="${admonitionType}"${idAttr}${mappedAttrs}${styleAttr}>${childrenOutput}</div>`;
|
|
1053
|
+
}
|
|
1054
|
+
case 'definitionList':
|
|
1055
|
+
return `${extraAnchors}<dl${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</dl>`;
|
|
1056
|
+
case 'definitionTerm':
|
|
1057
|
+
return `${extraAnchors}<dt${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</dt>`;
|
|
1058
|
+
case 'definitionDescription':
|
|
1059
|
+
return `${extraAnchors}<dd${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}</dd>`;
|
|
795
1060
|
default:
|
|
796
1061
|
return childrenOutput;
|
|
797
1062
|
}
|
|
@@ -829,24 +1094,50 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
829
1094
|
}
|
|
830
1095
|
}
|
|
831
1096
|
const meta = node.metadata;
|
|
832
|
-
if (meta?.
|
|
1097
|
+
if (meta?.wikilink) {
|
|
1098
|
+
// No pinned editor contract yet for wikilinks - data-wikilink-page preserves the
|
|
1099
|
+
// exact page name separately from the display text/alias and from href (which
|
|
1100
|
+
// the host app's resolver may rewrite to a real URL).
|
|
1101
|
+
if (!this.config.ignoreInternalLinks) {
|
|
1102
|
+
result = `<a href="#${this.escape(this.slugify(meta.link || ''))}" data-wikilink-page="${this.escape(meta.link || '')}">${result}</a>`;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
else if (meta?.link) {
|
|
833
1106
|
const isInternal = meta.linkType !== 'external';
|
|
834
1107
|
if (!this.config.ignoreInternalLinks || !isInternal) {
|
|
835
|
-
result = `<a href="${meta.link}"${meta.linkType === 'external' ? ' target="_blank"' : ''}>${result}</a>`;
|
|
1108
|
+
result = `<a href="${(0, sanitize_js_1.sanitizeUrl)(meta.link)}"${meta.linkType === 'external' ? ' target="_blank"' : ''}>${result}</a>`;
|
|
836
1109
|
}
|
|
837
1110
|
}
|
|
1111
|
+
if (meta?.abbreviationTitle) {
|
|
1112
|
+
result = `<abbr title="${this.escape(meta.abbreviationTitle)}">${result}</abbr>`;
|
|
1113
|
+
}
|
|
1114
|
+
if (meta?.citationKey) {
|
|
1115
|
+
// No pinned editor contract yet for citations (unlike footnotes/admonitions) -
|
|
1116
|
+
// this is the proposed shape: a <cite> carrying the bare key, matching Pandoc's
|
|
1117
|
+
// [@citekey] on the Markdown side. Keep in sync if inscript-editor's citation
|
|
1118
|
+
// node lands with a different attribute name.
|
|
1119
|
+
result = `<cite data-citation-key="${this.escape(meta.citationKey)}">[@${this.escape(meta.citationKey)}]</cite>`;
|
|
1120
|
+
}
|
|
838
1121
|
return result;
|
|
839
1122
|
}
|
|
840
1123
|
getInlineStyles(node) {
|
|
841
1124
|
const styles = [];
|
|
1125
|
+
// Colors/sizes/fonts/alignments are free strings from an untrusted document;
|
|
1126
|
+
// run each through sanitizeCssValue so it can't break out of the style="" attribute
|
|
1127
|
+
// or inject a resource-fetching CSS construct. Drop the declaration if nothing survives.
|
|
1128
|
+
const pushSafe = (prop, value) => {
|
|
1129
|
+
const safe = (0, sanitize_js_1.sanitizeCssValue)(value);
|
|
1130
|
+
if (safe)
|
|
1131
|
+
styles.push(`${prop}: ${safe}`);
|
|
1132
|
+
};
|
|
842
1133
|
if (node.metadata) {
|
|
843
1134
|
const meta = node.metadata;
|
|
844
1135
|
if (meta.alignment)
|
|
845
|
-
|
|
1136
|
+
pushSafe('text-align', meta.alignment);
|
|
846
1137
|
if (meta.backgroundColor)
|
|
847
|
-
|
|
1138
|
+
pushSafe('background-color', meta.backgroundColor);
|
|
848
1139
|
if (meta.verticalAlign)
|
|
849
|
-
|
|
1140
|
+
pushSafe('vertical-align', meta.verticalAlign);
|
|
850
1141
|
if (meta.paragraphIndentation) {
|
|
851
1142
|
const ind = meta.paragraphIndentation;
|
|
852
1143
|
if (ind.left)
|
|
@@ -860,13 +1151,16 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
860
1151
|
if (node.formatting) {
|
|
861
1152
|
const f = node.formatting;
|
|
862
1153
|
if (f.color)
|
|
863
|
-
|
|
1154
|
+
pushSafe('color', f.color);
|
|
864
1155
|
if (f.backgroundColor)
|
|
865
|
-
|
|
1156
|
+
pushSafe('background-color', f.backgroundColor);
|
|
866
1157
|
if (f.size)
|
|
867
|
-
|
|
868
|
-
if (f.font)
|
|
869
|
-
|
|
1158
|
+
pushSafe('font-size', f.size);
|
|
1159
|
+
if (f.font) {
|
|
1160
|
+
const safeFont = (0, sanitize_js_1.sanitizeCssValue)(f.font);
|
|
1161
|
+
if (safeFont)
|
|
1162
|
+
styles.push(`font-family: ${safeFont}, sans-serif`);
|
|
1163
|
+
}
|
|
870
1164
|
}
|
|
871
1165
|
return styles.join('; ');
|
|
872
1166
|
}
|
|
@@ -1431,6 +1725,20 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1431
1725
|
${this.config.htmlConfig.customCss}
|
|
1432
1726
|
`;
|
|
1433
1727
|
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Same as `getPremiumStyles()`, but wrapped in a CSS `@scope` block anchored to the
|
|
1730
|
+
* `.op-html-scope` wrapper so the rules only apply within the generated fragment - they
|
|
1731
|
+
* cannot leak onto a host page's own elements. `:root` and `body` selectors specifically
|
|
1732
|
+
* target the real page root/body, so they're remapped to `:scope` (the scope root, i.e. the
|
|
1733
|
+
* `.op-html-scope` wrapper) first; every other selector is naturally confined by `@scope`
|
|
1734
|
+
* without needing per-selector rewriting. `customCss` is included in this scoping too.
|
|
1735
|
+
*/
|
|
1736
|
+
getScopedPremiumStyles(isSpreadsheet = false, isPresentation = false, isPdf = false) {
|
|
1737
|
+
const css = this.getPremiumStyles(isSpreadsheet, isPresentation, isPdf)
|
|
1738
|
+
.replace(/:root(\s*\{)/g, ':scope$1')
|
|
1739
|
+
.replace(/(^|\n)(\s*)body(\s*\{)/g, '$1$2:scope$3');
|
|
1740
|
+
return `@scope (.op-html-scope) {\n${css}\n}`;
|
|
1741
|
+
}
|
|
1434
1742
|
slugify(text) {
|
|
1435
1743
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
1436
1744
|
}
|
|
@@ -1443,10 +1751,19 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1443
1751
|
}
|
|
1444
1752
|
return letter;
|
|
1445
1753
|
}
|
|
1754
|
+
// Attribute/text escaping, URL sanitizing, and inline-script serialization all
|
|
1755
|
+
// live in ../utils/sanitize.js so every generator shares one implementation.
|
|
1756
|
+
// escape() stays as a thin wrapper because it has many call sites here.
|
|
1446
1757
|
escape(text) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1758
|
+
return (0, sanitize_js_1.escapeHtml)(text);
|
|
1759
|
+
}
|
|
1760
|
+
/** Converts a document-supplied date to an ISO string, or '' if it is invalid
|
|
1761
|
+
* (a malformed date would otherwise throw a RangeError and abort generation). */
|
|
1762
|
+
toIsoDate(value) {
|
|
1763
|
+
if (value === undefined || value === null || value === '')
|
|
1764
|
+
return '';
|
|
1765
|
+
const d = new Date(value);
|
|
1766
|
+
return isNaN(d.getTime()) ? '' : d.toISOString();
|
|
1450
1767
|
}
|
|
1451
1768
|
}
|
|
1452
1769
|
exports.HtmlGenerator = HtmlGenerator;
|