officeparser 7.2.2 → 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.
Files changed (43) hide show
  1. package/README.md +161 -17
  2. package/dist/OfficeGenerator.js +4 -0
  3. package/dist/OfficeParser.d.ts +2 -0
  4. package/dist/OfficeParser.js +6 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +3 -2
  7. package/dist/defaults.js +3 -3
  8. package/dist/generators/BaseGenerator.d.ts +11 -0
  9. package/dist/generators/BaseGenerator.js +29 -0
  10. package/dist/generators/CsvGenerator.d.ts +9 -1
  11. package/dist/generators/CsvGenerator.js +24 -14
  12. package/dist/generators/EpubGenerator.d.ts +18 -0
  13. package/dist/generators/EpubGenerator.js +242 -0
  14. package/dist/generators/HtmlGenerator.d.ts +12 -0
  15. package/dist/generators/HtmlGenerator.js +266 -51
  16. package/dist/generators/MarkdownGenerator.d.ts +16 -0
  17. package/dist/generators/MarkdownGenerator.js +173 -24
  18. package/dist/generators/PdfGenerator.js +32 -0
  19. package/dist/generators/RtfGenerator.js +12 -15
  20. package/dist/generators/TextGenerator.js +11 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/officeparser.browser.d.ts +144 -7
  24. package/dist/officeparser.browser.iife.js +289 -193
  25. package/dist/officeparser.browser.mjs +289 -193
  26. package/dist/officeparser.browser.slim.d.ts +2129 -0
  27. package/dist/officeparser.browser.slim.iife.js +1278 -0
  28. package/dist/officeparser.browser.slim.mjs +1277 -0
  29. package/dist/parsers/EpubParser.d.ts +8 -0
  30. package/dist/parsers/EpubParser.js +217 -0
  31. package/dist/parsers/HtmlParser.js +284 -20
  32. package/dist/parsers/MarkdownParser.js +424 -33
  33. package/dist/parsers/OpenOfficeParser.js +241 -54
  34. package/dist/parsers/PdfParser.js +4 -1
  35. package/dist/parsers/WordParser.js +2 -2
  36. package/dist/sbom.cdx.json +111 -223
  37. package/dist/types.d.ts +146 -7
  38. package/dist/types.js +2 -0
  39. package/dist/utils/errorUtils.js +3 -2
  40. package/dist/utils/sanitize.d.ts +99 -0
  41. package/dist/utils/sanitize.js +228 -0
  42. package/dist/utils/zipUtils.js +76 -26
  43. package/package.json +19 -10
@@ -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
- let notesHtml = '';
33
- for (const note of this.collectedNotes) {
34
- notesHtml += await this.processNodeRecursive(note, this.nodeProcessor.bind(this));
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
- if (this.config.htmlConfig.standalone) {
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 styles = this.config.htmlConfig.standalone ? '' : `<style>${this.getPremiumStyles(this.isSpreadsheetMode, isPresentation, isPdf)}</style>`;
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 value = this.config.htmlConfig.standalone ? `<!DOCTYPE html>
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
- ${this.config.includeCharts ? `<script src="${this.config.htmlConfig.chartJsSrc}"></script>` : ''}
232
- <style>
233
- ${this.getPremiumStyles(this.isSpreadsheetMode, isPresentation, isPdf)}
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
- ${spreadsheetScript}
247
- ${inj.bodyEnd}
305
+ ${scopeClose}
306
+ ${spreadsheetScriptOut}
307
+ ${sa.bodyInjections ? inj.bodyEnd : ''}
248
308
  </body>
249
- </html>` : `${styles}<div class="${containerClass}">${metadataBlock}${bodyContent}${spreadsheetTabs}</div>${spreadsheetScript}`;
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
- if (m.created)
265
- tags.push(`<meta name="dcterms.created" content="${new Date(m.created).toISOString()}">`);
266
- if (m.modified)
267
- tags.push(`<meta name="dcterms.modified" content="${new Date(m.modified).toISOString()}">`);
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?.type === 'ordered' ? '</ol>' : '</ul>') + '\n\n';
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?.type === 'ordered' ? '</ol>' : '</ul>') + '\n';
341
- html += (type === 'ordered' ? '<ol>' : '<ul>') + '\n';
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 === 'ordered' ? '<ol>' : '<ul>') + '\n';
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
- const img = `<img src="${this.escape(src)}" alt="${this.escape(node.text || meta?.altText || '')}"${className}${mappedAttrs}${styleAttr}>`;
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 = ${JSON.stringify(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
- const tableHtml = `<table${idAttr}${className}${mappedAttrs}${styleAttr}>${finalChildren}</table>`;
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
- const noteClass = meta?.noteType ? ` note-${meta.noteType}` : '';
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?.link) {
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
- styles.push(`text-align: ${meta.alignment}`);
1034
+ pushSafe('text-align', meta.alignment);
846
1035
  if (meta.backgroundColor)
847
- styles.push(`background-color: ${meta.backgroundColor}`);
1036
+ pushSafe('background-color', meta.backgroundColor);
848
1037
  if (meta.verticalAlign)
849
- styles.push(`vertical-align: ${meta.verticalAlign}`);
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
- styles.push(`color: ${f.color}`);
1052
+ pushSafe('color', f.color);
864
1053
  if (f.backgroundColor)
865
- styles.push(`background-color: ${f.backgroundColor}`);
1054
+ pushSafe('background-color', f.backgroundColor);
866
1055
  if (f.size)
867
- styles.push(`font-size: ${f.size}`);
868
- if (f.font)
869
- styles.push(`font-family: ${f.font}, sans-serif`);
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
- if (typeof text !== 'string')
1448
- return text;
1449
- return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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
  *