@json-to-office/core-docx 0.33.0 → 0.35.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 (69) hide show
  1. package/dist/components/heading.d.ts.map +1 -1
  2. package/dist/components/list.d.ts.map +1 -1
  3. package/dist/components/paragraph.d.ts.map +1 -1
  4. package/dist/components/section.d.ts.map +1 -1
  5. package/dist/components/table.d.ts.map +1 -1
  6. package/dist/components/toc/index.d.ts.map +1 -1
  7. package/dist/components/visual.d.ts +15 -2
  8. package/dist/components/visual.d.ts.map +1 -1
  9. package/dist/core/cached-render.d.ts +9 -1
  10. package/dist/core/cached-render.d.ts.map +1 -1
  11. package/dist/core/collectTocHeadings.d.ts +60 -0
  12. package/dist/core/collectTocHeadings.d.ts.map +1 -0
  13. package/dist/core/content.d.ts +17 -1
  14. package/dist/core/content.d.ts.map +1 -1
  15. package/dist/core/flattenVisuals.d.ts +8 -1
  16. package/dist/core/flattenVisuals.d.ts.map +1 -1
  17. package/dist/core/fontResolution.d.ts +15 -1
  18. package/dist/core/fontResolution.d.ts.map +1 -1
  19. package/dist/core/generator.d.ts.map +1 -1
  20. package/dist/core/layout.d.ts +5 -0
  21. package/dist/core/layout.d.ts.map +1 -1
  22. package/dist/core/prerasterizeVisuals.d.ts +8 -1
  23. package/dist/core/prerasterizeVisuals.d.ts.map +1 -1
  24. package/dist/core/render.d.ts +8 -1
  25. package/dist/core/render.d.ts.map +1 -1
  26. package/dist/core/sectionBookmarks.d.ts +53 -0
  27. package/dist/core/sectionBookmarks.d.ts.map +1 -0
  28. package/dist/core/sectionOrdinals.d.ts +32 -0
  29. package/dist/core/sectionOrdinals.d.ts.map +1 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +943 -206
  33. package/dist/index.js.map +1 -1
  34. package/dist/plugin/createDocumentGenerator.d.ts.map +1 -1
  35. package/dist/plugin/example/index.js +3286 -2568
  36. package/dist/plugin/example/index.js.map +1 -1
  37. package/dist/styles/index.d.ts +2 -2
  38. package/dist/styles/index.d.ts.map +1 -1
  39. package/dist/styles/themeToDocxAdapter.d.ts.map +1 -1
  40. package/dist/styles/utils/layoutUtils.d.ts +48 -0
  41. package/dist/styles/utils/layoutUtils.d.ts.map +1 -1
  42. package/dist/templates/themes/index.d.ts +552 -12
  43. package/dist/templates/themes/index.d.ts.map +1 -1
  44. package/dist/themes/defaults.d.ts +96 -0
  45. package/dist/themes/defaults.d.ts.map +1 -1
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/dist/types/index.d.ts +16 -1
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/utils/commentAnchors.d.ts +39 -0
  50. package/dist/utils/commentAnchors.d.ts.map +1 -0
  51. package/dist/utils/commentRegistry.d.ts +49 -0
  52. package/dist/utils/commentRegistry.d.ts.map +1 -0
  53. package/dist/utils/componentAnnotations.d.ts +23 -0
  54. package/dist/utils/componentAnnotations.d.ts.map +1 -0
  55. package/dist/utils/fixFloatingImageIds.d.ts +1 -1
  56. package/dist/utils/fixFloatingImageIds.d.ts.map +1 -1
  57. package/dist/utils/noteRegistry.d.ts +43 -0
  58. package/dist/utils/noteRegistry.d.ts.map +1 -0
  59. package/dist/utils/noteResolver.d.ts +31 -0
  60. package/dist/utils/noteResolver.d.ts.map +1 -0
  61. package/dist/utils/numberingConfig.d.ts +15 -0
  62. package/dist/utils/numberingConfig.d.ts.map +1 -1
  63. package/dist/utils/placeholderProcessor.d.ts +2 -2
  64. package/dist/utils/placeholderProcessor.d.ts.map +1 -1
  65. package/dist/utils/revisionUtils.d.ts +32 -9
  66. package/dist/utils/revisionUtils.d.ts.map +1 -1
  67. package/dist/utils/textParser.d.ts +12 -0
  68. package/dist/utils/textParser.d.ts.map +1 -1
  69. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1134,7 +1134,7 @@ function getPageDimensions(size) {
1134
1134
  if (typeof size === "string") {
1135
1135
  return PAGE_SIZES[size];
1136
1136
  }
1137
- return size;
1137
+ return { width: size.width, height: size.height };
1138
1138
  }
1139
1139
  var resolveTableParagraphSpacing, PAGE_SIZES, getTableStyle, getDocumentMargins, getPageSetup;
1140
1140
  var init_layoutUtils = __esm({
@@ -1151,10 +1151,10 @@ var init_layoutUtils = __esm({
1151
1151
  };
1152
1152
  };
1153
1153
  PAGE_SIZES = {
1154
- A4: { width: 11906, height: 16838 },
1155
- A3: { width: 16838, height: 23811 },
1156
- LETTER: { width: 12240, height: 15840 },
1157
- LEGAL: { width: 12240, height: 20160 }
1154
+ A4: { width: 11906, height: 16838, code: 9 },
1155
+ A3: { width: 16838, height: 23811, code: 8 },
1156
+ LETTER: { width: 12240, height: 15840, code: 1 },
1157
+ LEGAL: { width: 12240, height: 20160, code: 5 }
1158
1158
  };
1159
1159
  getTableStyle = (theme, themeName) => {
1160
1160
  const themeConfig = resolveTheme(theme, themeName);
@@ -1291,7 +1291,8 @@ var init_layoutUtils = __esm({
1291
1291
  return {
1292
1292
  size: {
1293
1293
  width: dimensions.width,
1294
- height: dimensions.height
1294
+ height: dimensions.height,
1295
+ ...dimensions.code !== void 0 && { code: dimensions.code }
1295
1296
  },
1296
1297
  margin: {
1297
1298
  top: margins.top ?? defaultMargins.top ?? 1440,
@@ -1303,10 +1304,11 @@ var init_layoutUtils = __esm({
1303
1304
  }
1304
1305
  return {
1305
1306
  size: {
1306
- width: 11906,
1307
- // 8.5 inches in twips (Letter size)
1308
- height: 16838
1309
- // 11.7 inches in twips (Letter size)
1307
+ width: PAGE_SIZES.A4.width,
1308
+ // 210mm in twips (A4)
1309
+ height: PAGE_SIZES.A4.height,
1310
+ // 297mm in twips (A4)
1311
+ code: PAGE_SIZES.A4.code
1310
1312
  },
1311
1313
  margin: {
1312
1314
  top: defaultMargins.top ?? 1440,
@@ -2528,18 +2530,7 @@ function getColumnSettings(layout) {
2528
2530
  function createSectionProperties(columnSettings, theme, themeName, sectionType, pageOverride) {
2529
2531
  const basePageSetup = getPageSetup(theme, themeName);
2530
2532
  const pageSetup = pageOverride ? {
2531
- size: {
2532
- ...basePageSetup.size,
2533
- ...pageOverride.size && typeof pageOverride.size === "object" ? pageOverride.size : pageOverride.size ? (() => {
2534
- const sizes = {
2535
- A4: { width: 11906, height: 16838 },
2536
- A3: { width: 16838, height: 23811 },
2537
- LETTER: { width: 12240, height: 15840 },
2538
- LEGAL: { width: 12240, height: 20160 }
2539
- };
2540
- return sizes[pageOverride.size];
2541
- })() : {}
2542
- },
2533
+ size: pageOverride.size ? getPageDimensions(pageOverride.size) : basePageSetup.size,
2543
2534
  margin: {
2544
2535
  ...basePageSetup.margin,
2545
2536
  ...pageOverride.margins || {}
@@ -2641,8 +2632,8 @@ Suggestion: ${suggestion}`
2641
2632
  // src/core/render.ts
2642
2633
  import {
2643
2634
  Document,
2644
- Paragraph as Paragraph7,
2645
- TextRun as TextRun6,
2635
+ Paragraph as Paragraph9,
2636
+ TextRun as TextRun9,
2646
2637
  AlignmentType as AlignmentType5,
2647
2638
  BookmarkStart as BookmarkStart2,
2648
2639
  BookmarkEnd as BookmarkEnd2
@@ -3061,17 +3052,19 @@ function convertBorders(borders, theme) {
3061
3052
  color: resolveColor(side.color, theme),
3062
3053
  ...side.space !== void 0 ? { space: side.space } : {}
3063
3054
  } : void 0;
3064
- const top = mapSide(borders.top);
3065
- const bottom = mapSide(borders.bottom);
3066
- const left = mapSide(borders.left);
3067
- const right = mapSide(borders.right);
3068
- const anyDefined = top || bottom || left || right;
3069
- return anyDefined ? {
3070
- ...top && { top },
3071
- ...bottom && { bottom },
3072
- ...left && { left },
3073
- ...right && { right }
3074
- } : void 0;
3055
+ const sides = [
3056
+ "top",
3057
+ "bottom",
3058
+ "left",
3059
+ "right",
3060
+ "between"
3061
+ ];
3062
+ const converted = {};
3063
+ for (const side of sides) {
3064
+ const value = mapSide(borders[side]);
3065
+ if (value) converted[side] = value;
3066
+ }
3067
+ return Object.keys(converted).length > 0 ? converted : void 0;
3075
3068
  }
3076
3069
  function createWordStyles(themeNameOrObject = "minimal", language) {
3077
3070
  const theme = typeof themeNameOrObject === "string" ? getTheme(themeNameOrObject) || getTheme("minimal") : themeNameOrObject;
@@ -3440,7 +3433,7 @@ function createWordStyles(themeNameOrObject = "minimal", language) {
3440
3433
  }
3441
3434
  const tocMatch = /^TOC([1-9])$/.exec(styleKey);
3442
3435
  const customStyle = tocMatch ? styleValue : resolveStyleWithBaseStyle(theme, styleKey) || styleValue;
3443
- const styleId = tocMatch ? `JTD_TOC${tocMatch[1]}` : styleKey;
3436
+ const styleId = tocMatch ? `TOC${tocMatch[1]}` : styleKey;
3444
3437
  const styleName = tocMatch ? `TOC ${tocMatch[1]}` : styleKey.replace(/([A-Z])/g, " $1").trim();
3445
3438
  const mapBaseStyleId = (base) => {
3446
3439
  if (!base) return "Normal";
@@ -3512,8 +3505,8 @@ function createWordStyles(themeNameOrObject = "minimal", language) {
3512
3505
  continue;
3513
3506
  }
3514
3507
  paragraphStyles.push({
3515
- // Use a namespaced ID but canonical display name
3516
- id: `JTD_TOC${i}`,
3508
+ // Canonical id and display name see the styleId note above.
3509
+ id: `TOC${i}`,
3517
3510
  name: `TOC ${i}`,
3518
3511
  basedOn: "Normal",
3519
3512
  next: "Normal",
@@ -3536,20 +3529,43 @@ function createWordStyles(themeNameOrObject = "minimal", language) {
3536
3529
  }
3537
3530
  });
3538
3531
  }
3532
+ const noteStyle = resolveStyleWithBaseStyle(theme, "normal") || theme.styles?.normal;
3533
+ const bodyRun = convertRunProperties(
3534
+ mergeFontAndStyleProperties(resolveFontProperties(theme, noteStyle?.font), {
3535
+ size: noteStyle?.size,
3536
+ color: noteStyle?.color
3537
+ }),
3538
+ theme,
3539
+ theme.colors.text
3540
+ );
3541
+ const noteRun = {
3542
+ ...bodyRun,
3543
+ ...typeof bodyRun.size === "number" && {
3544
+ size: Math.max(12, bodyRun.size - 4)
3545
+ }
3546
+ };
3539
3547
  return {
3540
3548
  paragraphStyles,
3541
3549
  // Cast needed due to docx typing
3542
- // Document-default proofing language (w:docDefaults/w:rPrDefault). Runs that
3543
- // don't set their own w:lang inherit this, so Word spell-checks the whole
3544
- // document in the requested language unless a component overrides it.
3545
- ...language && {
3546
- default: {
3550
+ default: {
3551
+ // Document-default proofing language (w:docDefaults/w:rPrDefault). Runs
3552
+ // that don't set their own w:lang inherit this, so Word spell-checks the
3553
+ // whole document in the requested language unless a component overrides
3554
+ // it.
3555
+ ...language && {
3547
3556
  document: {
3548
3557
  run: {
3549
3558
  language: { value: language }
3550
3559
  }
3551
3560
  }
3552
- }
3561
+ },
3562
+ // Run properties only: docx's own note paragraph defaults (single line
3563
+ // spacing, no space after) are already what a note wants, and an empty
3564
+ // spacing override would drop its line rule.
3565
+ footnoteText: { run: noteRun },
3566
+ footnoteReference: { run: { ...noteRun, superScript: true } },
3567
+ endnoteText: { run: noteRun },
3568
+ endnoteReference: { run: { ...noteRun, superScript: true } }
3553
3569
  }
3554
3570
  };
3555
3571
  }
@@ -3671,7 +3687,15 @@ import {
3671
3687
  } from "docx";
3672
3688
 
3673
3689
  // src/utils/textParser.ts
3674
- import { TextRun, ExternalHyperlink, InternalHyperlink, Tab } from "docx";
3690
+ import {
3691
+ TextRun,
3692
+ ExternalHyperlink,
3693
+ InternalHyperlink,
3694
+ FootnoteReferenceRun,
3695
+ EndnoteReferenceRun,
3696
+ Tab
3697
+ } from "docx";
3698
+ var FOOTNOTE_MARKER_REGEX = /\[\^([^\]\s]+)\]/;
3675
3699
  function escapeRegExp(s) {
3676
3700
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3677
3701
  }
@@ -3848,8 +3872,8 @@ function buildTextRuns(text, commonProps, opts = {}) {
3848
3872
  return runs;
3849
3873
  }
3850
3874
  function createTextRunsWithNewlines(text, baseStyle, options, overrideStyle) {
3851
- return buildTextRuns(
3852
- text,
3875
+ const runs = (segment) => buildTextRuns(
3876
+ segment,
3853
3877
  buildRunCommonProps(baseStyle, {
3854
3878
  bold: overrideStyle?.bold,
3855
3879
  italics: overrideStyle?.italics,
@@ -3857,6 +3881,34 @@ function createTextRunsWithNewlines(text, baseStyle, options, overrideStyle) {
3857
3881
  }),
3858
3882
  { noProof: baseStyle.noProof, noProofWords: options.noProofWords }
3859
3883
  );
3884
+ if (!options.noteRef) return runs(text);
3885
+ return splitNoteMarkers(text, options.noteRef, runs);
3886
+ }
3887
+ function splitNoteMarkers(text, noteRef, runs) {
3888
+ const regex = new RegExp(FOOTNOTE_MARKER_REGEX.source, "g");
3889
+ const out = [];
3890
+ let lastIndex = 0;
3891
+ let pending = "";
3892
+ let match;
3893
+ while ((match = regex.exec(text)) !== null) {
3894
+ const note = noteRef(match[1]);
3895
+ if (note === void 0) {
3896
+ pending += text.slice(lastIndex, regex.lastIndex);
3897
+ lastIndex = regex.lastIndex;
3898
+ continue;
3899
+ }
3900
+ const before = pending + text.slice(lastIndex, match.index);
3901
+ pending = "";
3902
+ if (before) out.push(...runs(before));
3903
+ out.push(
3904
+ note.endnote ? new EndnoteReferenceRun(note.id) : new FootnoteReferenceRun(note.id)
3905
+ );
3906
+ lastIndex = regex.lastIndex;
3907
+ }
3908
+ if (out.length === 0) return runs(text);
3909
+ const trailing = pending + text.slice(lastIndex);
3910
+ if (trailing) out.push(...runs(trailing));
3911
+ return out;
3860
3912
  }
3861
3913
  function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
3862
3914
  const normalizedText = normalizeUnicodeText(text);
@@ -4112,6 +4164,52 @@ initializeBuiltinPlaceholders();
4112
4164
 
4113
4165
  // src/utils/revisionUtils.ts
4114
4166
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
4167
+
4168
+ // src/utils/componentAnnotations.ts
4169
+ function isRecord(value) {
4170
+ return typeof value === "object" && value !== null;
4171
+ }
4172
+ function hasKey(value, key) {
4173
+ return isRecord(value) && Boolean(value[key]);
4174
+ }
4175
+ function cellHasAnnotation(cell, key) {
4176
+ if (!isRecord(cell)) return false;
4177
+ if (hasKey(cell, key)) return true;
4178
+ const content = cell.content;
4179
+ return isRecord(content) && componentHasAnnotation(content, key);
4180
+ }
4181
+ function componentHasAnnotation(component, key) {
4182
+ const props = component.props;
4183
+ if (props) {
4184
+ if (props[key]) return true;
4185
+ const items = props.items;
4186
+ if (Array.isArray(items) && items.some((item) => hasKey(item, key))) {
4187
+ return true;
4188
+ }
4189
+ const rows = props.rows;
4190
+ if (Array.isArray(rows) && rows.some((row) => hasKey(row, key))) {
4191
+ return true;
4192
+ }
4193
+ const columns = props.columns;
4194
+ if (Array.isArray(columns) && columns.some((column) => {
4195
+ if (!isRecord(column)) return false;
4196
+ if (cellHasAnnotation(column.header, key)) return true;
4197
+ const cells = column.cells;
4198
+ return Array.isArray(cells) && cells.some((cell) => cellHasAnnotation(cell, key));
4199
+ })) {
4200
+ return true;
4201
+ }
4202
+ }
4203
+ const children = component.children;
4204
+ if (Array.isArray(children)) {
4205
+ return children.some(
4206
+ (child) => isRecord(child) && componentHasAnnotation(child, key)
4207
+ );
4208
+ }
4209
+ return false;
4210
+ }
4211
+
4212
+ // src/utils/revisionUtils.ts
4115
4213
  var DEFAULT_REVISION_AUTHOR = "json-to-office";
4116
4214
  var DEFAULT_REVISION_DATE = "1970-01-01T00:00:00Z";
4117
4215
  var RevisionIdRegistry = class {
@@ -4196,25 +4294,25 @@ function createRevisionRuns(revision, baseStyle) {
4196
4294
  return runs;
4197
4295
  }
4198
4296
  function componentHasRevision(component) {
4199
- const props = component.props;
4200
- if (props) {
4201
- if (props.revision) return true;
4202
- const items = props.items;
4203
- if (Array.isArray(items)) {
4204
- if (items.some(
4205
- (item) => typeof item === "object" && item !== null && "revision" in item && item.revision
4206
- )) {
4207
- return true;
4208
- }
4209
- }
4210
- }
4211
- const children = component.children;
4212
- if (Array.isArray(children)) {
4213
- return children.some(
4214
- (child) => typeof child === "object" && child !== null && componentHasRevision(child)
4215
- );
4216
- }
4217
- return false;
4297
+ return componentHasAnnotation(component, "revision");
4298
+ }
4299
+ function createRevisionMark(mark) {
4300
+ const attributes = {
4301
+ id: globalRevisionIdRegistry.next(),
4302
+ author: mark.author || DEFAULT_REVISION_AUTHOR,
4303
+ date: mark.date || DEFAULT_REVISION_DATE
4304
+ };
4305
+ return mark.type === "insert" ? { insertion: attributes } : { deletion: attributes };
4306
+ }
4307
+ function createMarkedTextRuns(text, mark, baseStyle) {
4308
+ return createRevisionRuns(
4309
+ {
4310
+ author: mark.author,
4311
+ date: mark.date,
4312
+ segments: [{ type: mark.type, text }]
4313
+ },
4314
+ baseStyle
4315
+ );
4218
4316
  }
4219
4317
 
4220
4318
  // src/core/cached-render.ts
@@ -4291,14 +4389,23 @@ async function clearComponentCache() {
4291
4389
  }
4292
4390
  bypassStats.clear();
4293
4391
  }
4392
+ var DYNAMIC_CONTEXT_COMPONENTS = /* @__PURE__ */ new Set([
4393
+ "toc",
4394
+ "section",
4395
+ "visual",
4396
+ "heading",
4397
+ "list",
4398
+ "paragraph"
4399
+ ]);
4400
+ function componentBypassReason(component) {
4401
+ if (componentHasRevision(component)) return "revision-ids";
4402
+ if (componentHasAnnotation(component, "comment")) return "comment-ids";
4403
+ if ("id" in component) return "bookmark-id";
4404
+ if (DYNAMIC_CONTEXT_COMPONENTS.has(component.name)) return "dynamic-context";
4405
+ return null;
4406
+ }
4294
4407
  async function renderComponentWithCache(component, theme, themeName, context, bypassCache = false) {
4295
- const bypassReason = componentHasRevision(
4296
- component
4297
- ) ? "revision-ids" : "id" in component ? "bookmark-id" : component.name === "toc" || component.name === "section" || component.name === "visual" || component.name === "heading" || // Both explicit lists and markdown-list paragraphs register
4298
- // numbering in the current document scope. Cached paragraphs can
4299
- // otherwise reference definitions that only existed in a previous
4300
- // render.
4301
- component.name === "list" || component.name === "paragraph" ? "dynamic-context" : null;
4408
+ const bypassReason = componentBypassReason(component);
4302
4409
  if (!componentCache) {
4303
4410
  initializeComponentCache();
4304
4411
  }
@@ -4368,8 +4475,8 @@ function getComponentCacheStats() {
4368
4475
 
4369
4476
  // src/core/content.ts
4370
4477
  import {
4371
- Paragraph,
4372
- TextRun as TextRun4,
4478
+ Paragraph as Paragraph3,
4479
+ TextRun as TextRun7,
4373
4480
  Table,
4374
4481
  TableRow,
4375
4482
  TableCell,
@@ -4464,6 +4571,252 @@ var BookmarkRegistry = class {
4464
4571
  };
4465
4572
  var globalBookmarkRegistry = new BookmarkRegistry();
4466
4573
 
4574
+ // src/utils/commentAnchors.ts
4575
+ import {
4576
+ CommentRangeEnd,
4577
+ CommentRangeStart,
4578
+ CommentReference,
4579
+ TextRun as TextRun5
4580
+ } from "docx";
4581
+
4582
+ // src/utils/commentRegistry.ts
4583
+ import { Paragraph, TextRun as TextRun4 } from "docx";
4584
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
4585
+ var DEFAULT_COMMENT_AUTHOR = "json-to-office";
4586
+ var DEFAULT_COMMENT_DATE = "1970-01-01T00:00:00Z";
4587
+ function createState() {
4588
+ return { counter: 0, comments: [], hasResolved: false };
4589
+ }
4590
+ function deriveInitials(author) {
4591
+ const initials = author.split(/[\s._-]+/).filter(Boolean).map((word) => word[0]).join("").toUpperCase().slice(0, 3);
4592
+ return initials || author.slice(0, 2).toUpperCase();
4593
+ }
4594
+ function bodyParagraphs(text) {
4595
+ return normalizeUnicodeText(text).split("\n").map((line) => new Paragraph({ children: [new TextRun4({ text: line })] }));
4596
+ }
4597
+ var CommentRegistry = class {
4598
+ fallback = createState();
4599
+ scopes = new AsyncLocalStorage4();
4600
+ get state() {
4601
+ return this.scopes.getStore() ?? this.fallback;
4602
+ }
4603
+ /** Run work with an isolated registry that follows its async call chain. */
4604
+ runScoped(callback) {
4605
+ return this.scopes.run(createState(), callback);
4606
+ }
4607
+ /**
4608
+ * Register a comment thread and return every id its anchors must carry — the
4609
+ * root first, then each reply in order. Word anchors every comment in a
4610
+ * thread over the same range, so all of them need a range and a reference.
4611
+ *
4612
+ * Ids are unique and monotonic within a render. `parentId` is derived here
4613
+ * rather than authored; docx turns it into the `w15:paraIdParent` links in
4614
+ * `word/commentsExtended.xml`.
4615
+ */
4616
+ register(comment) {
4617
+ const state = this.state;
4618
+ const resolved = comment.resolved;
4619
+ const rootId = ++state.counter;
4620
+ state.comments.push({
4621
+ ...this.toOptions(comment, rootId),
4622
+ ...resolved !== void 0 && { resolved }
4623
+ });
4624
+ const ids = [rootId];
4625
+ for (const reply of comment.replies ?? []) {
4626
+ const replyId = ++state.counter;
4627
+ state.comments.push({
4628
+ ...this.toOptions(reply, replyId),
4629
+ parentId: rootId,
4630
+ // Word resolves a thread as a whole, so the flag rides every member.
4631
+ ...resolved !== void 0 && { resolved }
4632
+ });
4633
+ ids.push(replyId);
4634
+ }
4635
+ if (resolved !== void 0) state.hasResolved = true;
4636
+ return ids;
4637
+ }
4638
+ toOptions(comment, id) {
4639
+ const author = comment.author || DEFAULT_COMMENT_AUTHOR;
4640
+ return {
4641
+ id,
4642
+ author,
4643
+ initials: comment.initials || deriveInitials(author),
4644
+ date: new Date(comment.date || DEFAULT_COMMENT_DATE),
4645
+ children: bodyParagraphs(comment.text)
4646
+ };
4647
+ }
4648
+ /**
4649
+ * Every comment registered in this scope, in id order.
4650
+ *
4651
+ * docx writes `word/commentsExtended.xml` — and therefore any `w15:done` —
4652
+ * only when at least one comment in the document carries a `parentId`. A
4653
+ * document whose only resolved comment has no replies would silently lose
4654
+ * that state, so say so rather than dropping it quietly.
4655
+ */
4656
+ getAll() {
4657
+ const state = this.state;
4658
+ if (state.hasResolved && !state.comments.some((comment) => comment.parentId !== void 0)) {
4659
+ console.warn(
4660
+ "A comment sets `resolved` but the document has no replies. Word stores the resolved flag in commentsExtended.xml, which is written only for threaded comments, so the flag will not survive."
4661
+ );
4662
+ }
4663
+ return [...state.comments];
4664
+ }
4665
+ /** Test-only: reset the current scope's counter and bodies. */
4666
+ clear() {
4667
+ const state = this.scopes.getStore();
4668
+ if (state) {
4669
+ state.counter = 0;
4670
+ state.comments.length = 0;
4671
+ state.hasResolved = false;
4672
+ } else {
4673
+ this.fallback = createState();
4674
+ }
4675
+ }
4676
+ };
4677
+ var globalCommentRegistry = new CommentRegistry();
4678
+
4679
+ // src/utils/commentAnchors.ts
4680
+ function openCommentRange(comment) {
4681
+ if (!comment) return void 0;
4682
+ const ids = globalCommentRegistry.register(comment);
4683
+ return { ids, start: ids.map((id) => new CommentRangeStart(id)) };
4684
+ }
4685
+ function closeCommentRange(ids) {
4686
+ return ids.flatMap((id) => [
4687
+ new CommentRangeEnd(id),
4688
+ new TextRun5({ children: [new CommentReference(id)] })
4689
+ ]);
4690
+ }
4691
+
4692
+ // src/utils/noteRegistry.ts
4693
+ import { Paragraph as Paragraph2, TextRun as TextRun6 } from "docx";
4694
+ import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4695
+ function createState2() {
4696
+ return {
4697
+ footnoteCounter: 0,
4698
+ endnoteCounter: 0,
4699
+ footnotes: {},
4700
+ endnotes: {}
4701
+ };
4702
+ }
4703
+ function bodyParagraphs2(text, style) {
4704
+ return normalizeUnicodeText(text).split("\n").map(
4705
+ (line) => new Paragraph2({
4706
+ style,
4707
+ children: [new TextRun6({ text: line })]
4708
+ })
4709
+ );
4710
+ }
4711
+ var NoteRegistry = class {
4712
+ fallback = createState2();
4713
+ scopes = new AsyncLocalStorage5();
4714
+ get state() {
4715
+ return this.scopes.getStore() ?? this.fallback;
4716
+ }
4717
+ /** Run work with an isolated registry that follows its async call chain. */
4718
+ runScoped(callback) {
4719
+ return this.scopes.run(createState2(), callback);
4720
+ }
4721
+ /**
4722
+ * Register a footnote body and return the id its reference must use. Ids are
4723
+ * unique and monotonic within a render.
4724
+ */
4725
+ registerFootnote(text) {
4726
+ const state = this.state;
4727
+ const id = ++state.footnoteCounter;
4728
+ state.footnotes[String(id)] = {
4729
+ children: bodyParagraphs2(text, "FootnoteText")
4730
+ };
4731
+ return id;
4732
+ }
4733
+ /** Register an endnote body and return the id its reference must use. */
4734
+ registerEndnote(text) {
4735
+ const state = this.state;
4736
+ const id = ++state.endnoteCounter;
4737
+ state.endnotes[String(id)] = {
4738
+ children: bodyParagraphs2(text, "EndnoteText")
4739
+ };
4740
+ return id;
4741
+ }
4742
+ /** Every footnote registered in this scope, keyed by id. */
4743
+ getFootnotes() {
4744
+ return { ...this.state.footnotes };
4745
+ }
4746
+ /** Every endnote registered in this scope, keyed by id. */
4747
+ getEndnotes() {
4748
+ return { ...this.state.endnotes };
4749
+ }
4750
+ /** Test-only: reset the current scope's counters and bodies. */
4751
+ clear() {
4752
+ const state = this.scopes.getStore();
4753
+ if (state) {
4754
+ state.footnoteCounter = 0;
4755
+ state.endnoteCounter = 0;
4756
+ for (const key of Object.keys(state.footnotes)) {
4757
+ delete state.footnotes[key];
4758
+ }
4759
+ for (const key of Object.keys(state.endnotes)) {
4760
+ delete state.endnotes[key];
4761
+ }
4762
+ } else {
4763
+ this.fallback = createState2();
4764
+ }
4765
+ }
4766
+ };
4767
+ var globalNoteRegistry = new NoteRegistry();
4768
+
4769
+ // src/utils/noteResolver.ts
4770
+ function collectDeclared(footnotes, endnotes) {
4771
+ const declared = /* @__PURE__ */ new Map();
4772
+ const declare = (note, endnote) => {
4773
+ const existing = declared.get(note.id);
4774
+ if (existing) {
4775
+ console.warn(
4776
+ existing.endnote === endnote ? `Note id "${note.id}" is declared twice in the same ${endnote ? "endnotes" : "footnotes"} array. Using the first declaration and ignoring the rest.` : `Note id "${note.id}" is declared as both a footnote and an endnote in the same paragraph. Using the footnote and ignoring the endnote.`
4777
+ );
4778
+ return;
4779
+ }
4780
+ declared.set(note.id, { text: note.text, endnote });
4781
+ };
4782
+ for (const note of footnotes ?? []) declare(note, false);
4783
+ for (const note of endnotes ?? []) declare(note, true);
4784
+ return declared;
4785
+ }
4786
+ function createNoteResolver(footnotes, endnotes) {
4787
+ const declared = collectDeclared(footnotes, endnotes);
4788
+ if (declared.size === 0) return void 0;
4789
+ const registered = /* @__PURE__ */ new Map();
4790
+ return {
4791
+ resolve(id) {
4792
+ const existing = registered.get(id);
4793
+ if (existing !== void 0) return existing;
4794
+ const note = declared.get(id);
4795
+ if (note === void 0) {
4796
+ console.warn(
4797
+ `Note marker "[^${id}]" has no matching entry in this paragraph's footnotes or endnotes (declared: ${[...declared.keys()].join(", ")}). Rendering the marker as literal text.`
4798
+ );
4799
+ return void 0;
4800
+ }
4801
+ const resolved = {
4802
+ id: note.endnote ? globalNoteRegistry.registerEndnote(note.text) : globalNoteRegistry.registerFootnote(note.text),
4803
+ endnote: note.endnote
4804
+ };
4805
+ registered.set(id, resolved);
4806
+ return resolved;
4807
+ },
4808
+ reportUnemitted(text) {
4809
+ for (const [id, note] of declared) {
4810
+ if (registered.has(id)) continue;
4811
+ const kind = note.endnote ? "Endnote" : "Footnote";
4812
+ console.warn(
4813
+ text.includes(`[^${id}]`) ? `${kind} "${id}" is declared and its marker appears in the text, but the marker was not resolved \u2014 markers are not recognised in text that also contains {PLACEHOLDER} substitutions. The note will not appear in the document.` : `${kind} "${id}" is declared but never referenced as [^${id}] in this paragraph. It will not appear in the document.`
4814
+ );
4815
+ }
4816
+ }
4817
+ };
4818
+ }
4819
+
4467
4820
  // src/core/content.ts
4468
4821
  init_styleHelpers();
4469
4822
  import { synthesizeFamilyName } from "@json-to-office/shared";
@@ -4487,6 +4840,20 @@ function resolveNoProofWords(theme, optionWords) {
4487
4840
  const merged = [...themeWords || [], ...optionWords || []];
4488
4841
  return merged.length > 0 ? Array.from(new Set(merged)) : void 0;
4489
4842
  }
4843
+ function reportNotesUnsupportedInRevision(footnotes, endnotes, revision) {
4844
+ const declared = [...footnotes ?? [], ...endnotes ?? []];
4845
+ if (declared.length === 0) return;
4846
+ const text = revision.segments.map((segment) => segment.text).join("");
4847
+ const referenced = declared.filter((note) => text.includes(`[^${note.id}]`));
4848
+ console.warn(
4849
+ `Paragraph declares ${declared.length} note(s) (${declared.map((note) => note.id).join(", ")}) alongside a \`revision\`. Tracked-change text renders literally, so note markers are not resolved there` + (referenced.length > 0 ? ` \u2014 the marker(s) ${referenced.map((note) => `[^${note.id}]`).join(", ")} will render as literal text` : "") + ". The notes will not appear in the document."
4850
+ );
4851
+ }
4852
+ function wrapInComment(children, comment) {
4853
+ const anchor = openCommentRange(comment);
4854
+ if (!anchor) return children;
4855
+ return [...anchor.start, ...children, ...closeCommentRange(anchor.ids)];
4856
+ }
4490
4857
  function createText(content, theme, themeName, options = {}) {
4491
4858
  const normalizedContent = normalizeUnicodeText(content);
4492
4859
  const style = options.style || "Normal";
@@ -4506,6 +4873,10 @@ function createText(content, theme, themeName, options = {}) {
4506
4873
  if (options.columnBreak) {
4507
4874
  children.push(new ColumnBreak());
4508
4875
  }
4876
+ const commentAnchor = openCommentRange(options.comment);
4877
+ if (commentAnchor) {
4878
+ children.push(...commentAnchor.start);
4879
+ }
4509
4880
  const hasWeightRequest = options.fontWeight != null || options.bold === true;
4510
4881
  const effectiveFamily = options.fontFamily ?? (hasWeightRequest ? resolveFontFamily(theme, "body") : void 0);
4511
4882
  const weighted = applyFontWeightAlias({
@@ -4542,6 +4913,11 @@ function createText(content, theme, themeName, options = {}) {
4542
4913
  ...options.noProof !== void 0 && { noProof: options.noProof }
4543
4914
  };
4544
4915
  if (options.revision) {
4916
+ reportNotesUnsupportedInRevision(
4917
+ options.footnotes,
4918
+ options.endnotes,
4919
+ options.revision
4920
+ );
4545
4921
  const revisionRuns = createRevisionRuns(options.revision, baseTextStyle);
4546
4922
  if (options.bookmarkId) {
4547
4923
  globalBookmarkRegistry.register(
@@ -4559,11 +4935,17 @@ function createText(content, theme, themeName, options = {}) {
4559
4935
  children.push(...revisionRuns);
4560
4936
  }
4561
4937
  } else {
4938
+ const noteResolver = createNoteResolver(
4939
+ options.footnotes,
4940
+ options.endnotes
4941
+ );
4562
4942
  const textRuns = parseTextWithDecorators(normalizedContent, baseTextStyle, {
4563
4943
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
4564
4944
  enableHyperlinks: true,
4565
- noProofWords: resolveNoProofWords(theme, options.noProofWords)
4945
+ noProofWords: resolveNoProofWords(theme, options.noProofWords),
4946
+ noteRef: noteResolver?.resolve
4566
4947
  });
4948
+ noteResolver?.reportUnemitted(normalizedContent);
4567
4949
  if (options.bookmarkId) {
4568
4950
  globalBookmarkRegistry.register(
4569
4951
  options.bookmarkId,
@@ -4580,9 +4962,12 @@ function createText(content, theme, themeName, options = {}) {
4580
4962
  children.push(...textRuns);
4581
4963
  }
4582
4964
  }
4965
+ if (commentAnchor) {
4966
+ children.push(...closeCommentRange(commentAnchor.ids));
4967
+ }
4583
4968
  const isFloating = !!options.floating;
4584
4969
  const frameOptions = isFloating && options.floating ? mapFrameOptions(options.floating, theme, themeName) : void 0;
4585
- return new Paragraph({
4970
+ return new Paragraph3({
4586
4971
  children,
4587
4972
  style,
4588
4973
  alignment: options.alignment ? getAlignment(options.alignment) : void 0,
@@ -4677,6 +5062,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4677
5062
  if (options.columnBreak) {
4678
5063
  children.push(new ColumnBreak());
4679
5064
  }
5065
+ const commentAnchor = openCommentRange(options.comment);
5066
+ if (commentAnchor) {
5067
+ children.push(...commentAnchor.start);
5068
+ }
4680
5069
  const hasDecorators = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText);
4681
5070
  const headingHasWeightRequest = options.fontWeight != null || options.bold === true;
4682
5071
  const headingEffectiveFamily = options.fontFamily ?? (headingHasWeightRequest ? resolveFontFamily(theme, "heading") : void 0);
@@ -4713,7 +5102,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4713
5102
  const headingNoProofWords = resolveNoProofWords(theme, options.noProofWords);
4714
5103
  const makeHeadingRuns = (value) => splitByNoProofWords(
4715
5104
  value,
4716
- (segment, matched) => new TextRun4({
5105
+ (segment, matched) => new TextRun7({
4717
5106
  text: segment,
4718
5107
  ...baseTextStyle,
4719
5108
  ...matched && { noProof: true }
@@ -4772,7 +5161,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4772
5161
  children.push(...makeHeadingRuns(normalizedText));
4773
5162
  }
4774
5163
  }
4775
- return new Paragraph({
5164
+ if (commentAnchor) {
5165
+ children.push(...closeCommentRange(commentAnchor.ids));
5166
+ }
5167
+ return new Paragraph3({
4776
5168
  children,
4777
5169
  style: styleId,
4778
5170
  alignment: getAlignment(options.alignment || "left"),
@@ -4839,7 +5231,7 @@ async function createImage(path4, theme, themeName, options = {}) {
4839
5231
  spacing.after = pointsToTwips(options.spacing.after);
4840
5232
  }
4841
5233
  elements.push(
4842
- new Paragraph({
5234
+ new Paragraph3({
4843
5235
  children: [imageRun],
4844
5236
  alignment,
4845
5237
  ...Object.keys(spacing).length > 0 && { spacing },
@@ -4856,7 +5248,7 @@ async function createImage(path4, theme, themeName, options = {}) {
4856
5248
  const hasDecorators = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(options.caption);
4857
5249
  if (!hasDecorators) {
4858
5250
  elements.push(
4859
- new Paragraph({
5251
+ new Paragraph3({
4860
5252
  text: normalizeUnicodeText(options.caption),
4861
5253
  style: "Normal",
4862
5254
  alignment: AlignmentType2.LEFT
@@ -4872,7 +5264,7 @@ async function createImage(path4, theme, themeName, options = {}) {
4872
5264
  }
4873
5265
  );
4874
5266
  elements.push(
4875
- new Paragraph({
5267
+ new Paragraph3({
4876
5268
  children: textRuns,
4877
5269
  style: "Normal",
4878
5270
  // Use Normal style for consistent font inheritance
@@ -4889,13 +5281,13 @@ function createStatistic(data, options = {}) {
4889
5281
  const normalizedNumber = normalizeUnicodeText(data.number);
4890
5282
  const normalizedDescription = normalizeUnicodeText(data.description);
4891
5283
  return [
4892
- new Paragraph({
5284
+ new Paragraph3({
4893
5285
  text: normalizedNumber,
4894
5286
  style: "StatisticNumber",
4895
5287
  alignment,
4896
5288
  spacing: options.spacing
4897
5289
  }),
4898
- new Paragraph({
5290
+ new Paragraph3({
4899
5291
  text: normalizedDescription,
4900
5292
  style: "StatisticDescription",
4901
5293
  alignment
@@ -4907,6 +5299,15 @@ function createList(items, _theme, _themeName, options = {}) {
4907
5299
  return [];
4908
5300
  }
4909
5301
  const paragraphs = [];
5302
+ const noteResolver = createNoteResolver(options.footnotes, options.endnotes);
5303
+ const rendersAt = items.map((item) => {
5304
+ const text = typeof item === "string" ? item : item.text;
5305
+ const revision = typeof item === "object" ? item.revision : void 0;
5306
+ return Boolean(text.trim()) || Boolean(revision);
5307
+ });
5308
+ const firstRendered = rendersAt.indexOf(true);
5309
+ const lastRendered = rendersAt.lastIndexOf(true);
5310
+ const commentAnchor = firstRendered === -1 ? void 0 : openCommentRange(options.comment);
4910
5311
  items.forEach((item, index) => {
4911
5312
  const itemText = typeof item === "string" ? item : item.text;
4912
5313
  const itemLevel = typeof item === "object" ? item.level || 0 : 0;
@@ -4918,7 +5319,8 @@ function createList(items, _theme, _themeName, options = {}) {
4918
5319
  itemText,
4919
5320
  {},
4920
5321
  {
4921
- enableHyperlinks: true
5322
+ enableHyperlinks: true,
5323
+ noteRef: noteResolver?.resolve
4922
5324
  }
4923
5325
  );
4924
5326
  const spacing = {};
@@ -4930,10 +5332,15 @@ function createList(items, _theme, _themeName, options = {}) {
4930
5332
  } else if (options.spacing?.item) {
4931
5333
  spacing.after = pointsToTwips(options.spacing.item);
4932
5334
  }
4933
- const paragraph = new Paragraph({
5335
+ const paragraphChildren = [
5336
+ ...commentAnchor && index === firstRendered ? commentAnchor.start : [],
5337
+ ...textRuns,
5338
+ ...commentAnchor && index === lastRendered ? closeCommentRange(commentAnchor.ids) : []
5339
+ ];
5340
+ const paragraph = new Paragraph3({
4934
5341
  style: "Normal",
4935
5342
  // Apply Normal style for font inheritance
4936
- children: textRuns,
5343
+ children: paragraphChildren,
4937
5344
  alignment: options.alignment ? getAlignment(options.alignment) : AlignmentType2.LEFT,
4938
5345
  spacing,
4939
5346
  // Use proper docx numbering instead of prepending text
@@ -4946,6 +5353,9 @@ function createList(items, _theme, _themeName, options = {}) {
4946
5353
  });
4947
5354
  paragraphs.push(paragraph);
4948
5355
  });
5356
+ noteResolver?.reportUnemitted(
5357
+ items.map((item) => typeof item === "string" ? item : item.text).join("\n")
5358
+ );
4949
5359
  return paragraphs;
4950
5360
  }
4951
5361
  async function createTable(columns, tableConfig, theme, themeName, _options = {}) {
@@ -5279,10 +5689,10 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5279
5689
  return false;
5280
5690
  }
5281
5691
  };
5282
- const processCellContent = async (cell, cellDefaults, baseCellStyle) => {
5692
+ const processCellContent = async (cell, cellDefaults, baseCellStyle, comment, revision, rowMark) => {
5283
5693
  let cellChildren = [];
5284
5694
  if (!cell) {
5285
- return cellChildren;
5695
+ return wrapInComment(cellChildren, comment);
5286
5696
  }
5287
5697
  const cellWeighted = applyFontWeightAlias({
5288
5698
  fontFamily: cellDefaults.font?.family || baseCellStyle.font,
@@ -5326,11 +5736,10 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5326
5736
  color: resolveColor(paragraphFont.color, theme)
5327
5737
  }
5328
5738
  };
5329
- cellChildren = parseTextWithDecorators(
5330
- textComp.props.text,
5331
- paragraphStyle,
5332
- { enableHyperlinks: true }
5333
- );
5739
+ const paragraphRevision = revision ?? textComp.props.revision;
5740
+ cellChildren = paragraphRevision ? createRevisionRuns(paragraphRevision, paragraphStyle) : rowMark ? createMarkedTextRuns(textComp.props.text, rowMark, paragraphStyle) : parseTextWithDecorators(textComp.props.text, paragraphStyle, {
5741
+ enableHyperlinks: true
5742
+ });
5334
5743
  } else if (isImageComponent(cell)) {
5335
5744
  const imageComp = cell;
5336
5745
  try {
@@ -5365,7 +5774,7 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5365
5774
  } catch (error) {
5366
5775
  const imageSource = imageComp.props.svg?.trim() ? "inline-svg" : imageComp.props.base64 || imageComp.props.path || "unknown";
5367
5776
  cellChildren = [
5368
- new TextRun4({
5777
+ new TextRun7({
5369
5778
  text: `[IMAGE: ${imageSource.substring(0, 50)}${imageSource.length > 50 ? "..." : ""}]`,
5370
5779
  font: mergedStyle.font,
5371
5780
  size: mergedStyle.size,
@@ -5375,7 +5784,7 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5375
5784
  }
5376
5785
  } else {
5377
5786
  cellChildren = [
5378
- new TextRun4({
5787
+ new TextRun7({
5379
5788
  text: `[Unsupported component type: ${cell.name}]`,
5380
5789
  font: mergedStyle.font,
5381
5790
  size: mergedStyle.size,
@@ -5384,11 +5793,11 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5384
5793
  ];
5385
5794
  }
5386
5795
  } else {
5387
- cellChildren = parseTextWithDecorators(cell, mergedStyle, {
5796
+ cellChildren = revision ? createRevisionRuns(revision, mergedStyle) : rowMark ? createMarkedTextRuns(cell, rowMark, mergedStyle) : parseTextWithDecorators(cell, mergedStyle, {
5388
5797
  enableHyperlinks: true
5389
5798
  });
5390
5799
  }
5391
- return cellChildren;
5800
+ return wrapInComment(cellChildren, comment);
5392
5801
  };
5393
5802
  const numColumns = columns.length;
5394
5803
  const headerHeight = columns.reduce(
@@ -5445,7 +5854,9 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5445
5854
  const cellChildren = await processCellContent(
5446
5855
  headerCell?.content,
5447
5856
  mergedDefaults,
5448
- tableStyle.tableHeader
5857
+ tableStyle.tableHeader,
5858
+ headerCell?.comment,
5859
+ headerCell?.revision
5449
5860
  );
5450
5861
  const horizontalAlignment = mergedDefaults.horizontalAlignment;
5451
5862
  const verticalAlignment = getVerticalAlignment(
@@ -5453,7 +5864,7 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5453
5864
  );
5454
5865
  return new TableCell({
5455
5866
  children: [
5456
- new Paragraph({
5867
+ new Paragraph3({
5457
5868
  ...tableConfig.keepInOnePage && { keepNext: true },
5458
5869
  spacing: tableStyle.headerParagraph,
5459
5870
  alignment: getAlignment(horizontalAlignment),
@@ -5496,6 +5907,10 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5496
5907
  const dataRows = await Promise.all(
5497
5908
  Array.from({ length: numRows }, async (_, rowIndex) => {
5498
5909
  const isLastRow = rowIndex === numRows - 1;
5910
+ const rowProps = tableConfig.rows?.[rowIndex];
5911
+ const rowRevision = rowProps?.revision;
5912
+ const rowRevisionAttributes = rowRevision ? createRevisionMark(rowRevision) : void 0;
5913
+ const paragraphMarks = rowRevision ? columns.map(() => ({ run: createRevisionMark(rowRevision) })) : void 0;
5499
5914
  const rowHeight = columns.reduce(
5500
5915
  (maxHeight, column, colIndex) => {
5501
5916
  const cell = column.cells?.[rowIndex];
@@ -5525,6 +5940,14 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5525
5940
  );
5526
5941
  return new TableRow({
5527
5942
  height: rowHeight !== void 0 ? { value: rowHeight * 20, rule: "atLeast" } : void 0,
5943
+ ...rowProps?.cantSplit !== void 0 && {
5944
+ cantSplit: rowProps.cantSplit
5945
+ },
5946
+ ...rowProps?.tableHeader !== void 0 && {
5947
+ tableHeader: rowProps.tableHeader
5948
+ },
5949
+ // w:trPr/w:ins | w:del — the row itself was inserted or deleted.
5950
+ ...rowRevisionAttributes,
5528
5951
  children: await Promise.all(
5529
5952
  columns.map(async (column, colIndex) => {
5530
5953
  const cell = column.cells?.[rowIndex];
@@ -5548,10 +5971,11 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5548
5971
  );
5549
5972
  return new TableCell({
5550
5973
  children: [
5551
- new Paragraph({
5974
+ new Paragraph3({
5552
5975
  ...tableConfig.keepInOnePage && !isLastRow || isLastRow && tableConfig.keepNext ? { keepNext: true } : {},
5553
5976
  spacing: tableStyle.cellParagraph,
5554
5977
  alignment: AlignmentType2.LEFT,
5978
+ ...paragraphMarks?.[colIndex],
5555
5979
  children: []
5556
5980
  })
5557
5981
  ],
@@ -5616,7 +6040,10 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5616
6040
  const cellChildren = await processCellContent(
5617
6041
  cell.content,
5618
6042
  mergedDefaults,
5619
- tableStyle.tableCell
6043
+ tableStyle.tableCell,
6044
+ cell.comment,
6045
+ cell.revision,
6046
+ rowRevision
5620
6047
  );
5621
6048
  const horizontalAlignment = mergedDefaults.horizontalAlignment;
5622
6049
  const verticalAlignment = getVerticalAlignment(
@@ -5624,10 +6051,11 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5624
6051
  );
5625
6052
  return new TableCell({
5626
6053
  children: [
5627
- new Paragraph({
6054
+ new Paragraph3({
5628
6055
  ...tableConfig.keepInOnePage && !isLastRow || isLastRow && tableConfig.keepNext ? { keepNext: true } : {},
5629
6056
  spacing: tableStyle.cellParagraph,
5630
6057
  alignment: getAlignment(horizontalAlignment),
6058
+ ...paragraphMarks?.[colIndex],
5631
6059
  children: cellChildren
5632
6060
  })
5633
6061
  ],
@@ -5786,7 +6214,9 @@ function renderHeadingComponent(component, theme, themeName) {
5786
6214
  // Bookmark ID for internal linking
5787
6215
  bookmarkId,
5788
6216
  // Tracked-change segments (rendered as native Word revisions)
5789
- revision: config.revision
6217
+ revision: config.revision,
6218
+ // Review comment anchored to this heading's text
6219
+ comment: config.comment
5790
6220
  }
5791
6221
  );
5792
6222
  return [header];
@@ -5794,7 +6224,7 @@ function renderHeadingComponent(component, theme, themeName) {
5794
6224
 
5795
6225
  // src/utils/numberingConfig.ts
5796
6226
  import { AlignmentType as AlignmentType3, convertInchesToTwip, LevelFormat } from "docx";
5797
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
6227
+ import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
5798
6228
  var LEVEL_FORMAT_MAP = {
5799
6229
  decimal: LevelFormat.DECIMAL,
5800
6230
  upperRoman: LevelFormat.UPPER_ROMAN,
@@ -5872,6 +6302,21 @@ function getAlignment2(alignment) {
5872
6302
  if (!alignment) return AlignmentType3.LEFT;
5873
6303
  return ALIGNMENT_MAP[alignment] || AlignmentType3.LEFT;
5874
6304
  }
6305
+ function createMarkerRunStyle(font) {
6306
+ if (!font) return void 0;
6307
+ const run = {
6308
+ ...font.family && { font: font.family },
6309
+ // docx run sizes are half-points.
6310
+ ...font.size !== void 0 && { size: font.size * 2 },
6311
+ ...font.color && { color: font.color },
6312
+ ...font.bold !== void 0 && { bold: font.bold },
6313
+ ...font.italic !== void 0 && { italics: font.italic },
6314
+ ...font.underline !== void 0 && {
6315
+ underline: font.underline ? { type: "single" } : void 0
6316
+ }
6317
+ };
6318
+ return Object.keys(run).length > 0 ? run : void 0;
6319
+ }
5875
6320
  function createNumberingConfig(config) {
5876
6321
  const levels = [];
5877
6322
  for (const levelConfig of config.levels) {
@@ -5880,6 +6325,7 @@ function createNumberingConfig(config) {
5880
6325
  const text = levelConfig.text || (format2 === LevelFormat.BULLET ? "\u2022" : `%${levelConfig.level + 1}.`);
5881
6326
  const baseIndent = levelConfig.indent?.left !== void 0 ? levelConfig.indent.left / 72 : 0.5 * (levelConfig.level + 1);
5882
6327
  const hangingIndent = levelConfig.indent?.hanging !== void 0 ? levelConfig.indent.hanging / 72 : 0.25;
6328
+ const markerRun = createMarkerRunStyle(levelConfig.font);
5883
6329
  const level = {
5884
6330
  level: levelConfig.level,
5885
6331
  format: format2,
@@ -5891,7 +6337,10 @@ function createNumberingConfig(config) {
5891
6337
  left: convertInchesToTwip(baseIndent),
5892
6338
  hanging: convertInchesToTwip(hangingIndent)
5893
6339
  }
5894
- }
6340
+ },
6341
+ // The marker glyph carries its own run properties; without this it
6342
+ // inherits whatever the list paragraph resolves to.
6343
+ ...markerRun && { run: markerRun }
5895
6344
  },
5896
6345
  // Add start number if specified
5897
6346
  ...levelConfig.start !== void 0 && { start: levelConfig.start }
@@ -5908,7 +6357,7 @@ var NumberingRegistry = class {
5908
6357
  configs: /* @__PURE__ */ new Map(),
5909
6358
  counter: 0
5910
6359
  };
5911
- scopes = new AsyncLocalStorage4();
6360
+ scopes = new AsyncLocalStorage6();
5912
6361
  get state() {
5913
6362
  return this.scopes.getStore() ?? this.fallback;
5914
6363
  }
@@ -6020,7 +6469,10 @@ function renderParagraphComponent(component, theme, themeName) {
6020
6469
  return createList(listData.items, theme, themeName, {
6021
6470
  numberingReference: reference,
6022
6471
  spacing: resolvedConfig.spacing,
6023
- alignment: resolvedConfig.alignment
6472
+ alignment: resolvedConfig.alignment,
6473
+ comment: resolvedConfig.comment,
6474
+ footnotes: resolvedConfig.footnotes,
6475
+ endnotes: resolvedConfig.endnotes
6024
6476
  });
6025
6477
  }
6026
6478
  const styleFromTheme = (() => {
@@ -6084,12 +6536,18 @@ function renderParagraphComponent(component, theme, themeName) {
6084
6536
  // Pass bookmark ID for internal linking
6085
6537
  bookmarkId: resolvedConfig.id,
6086
6538
  // Tracked-change segments (rendered as native Word revisions)
6087
- revision: resolvedConfig.revision
6539
+ revision: resolvedConfig.revision,
6540
+ // Review comment anchored to this paragraph's text
6541
+ comment: resolvedConfig.comment,
6542
+ // Note bodies for the `[^id]` markers in this paragraph
6543
+ footnotes: resolvedConfig.footnotes,
6544
+ endnotes: resolvedConfig.endnotes
6088
6545
  });
6089
6546
  return [text];
6090
6547
  }
6091
6548
 
6092
6549
  // src/components/list.ts
6550
+ init_colorUtils();
6093
6551
  function createLevelsFromSimplifiedProps(props) {
6094
6552
  const levels = [];
6095
6553
  let format2;
@@ -6136,6 +6594,22 @@ function createLevelsFromSimplifiedProps(props) {
6136
6594
  }
6137
6595
  return levels;
6138
6596
  }
6597
+ function resolveMarkerFonts(levels, theme) {
6598
+ return levels.map((level) => {
6599
+ const font = level.font;
6600
+ if (!font?.color) return level;
6601
+ return {
6602
+ ...level,
6603
+ font: { ...font, color: resolveColor(font.color, theme) }
6604
+ };
6605
+ });
6606
+ }
6607
+ function applyListStart(levels, start) {
6608
+ if (start === void 0) return levels;
6609
+ return levels.map(
6610
+ (level) => level.level === 0 && level.start === void 0 ? { ...level, start } : level
6611
+ );
6612
+ }
6139
6613
  function getMaxLevelFromItems(items) {
6140
6614
  if (!items || !Array.isArray(items)) {
6141
6615
  return 0;
@@ -6198,7 +6672,10 @@ function renderListComponent(component, theme, themeName) {
6198
6672
  let levels;
6199
6673
  if (resolvedConfig.levels && resolvedConfig.levels.length > 0) {
6200
6674
  levels = fillMissingLevels(
6201
- resolvedConfig.levels,
6675
+ applyListStart(
6676
+ resolvedConfig.levels,
6677
+ resolvedConfig.start
6678
+ ),
6202
6679
  maxLevel
6203
6680
  );
6204
6681
  } else {
@@ -6207,7 +6684,7 @@ function renderListComponent(component, theme, themeName) {
6207
6684
  }
6208
6685
  const config = {
6209
6686
  reference,
6210
- levels
6687
+ levels: resolveMarkerFonts(levels, theme)
6211
6688
  };
6212
6689
  const numberingConfig = createNumberingConfig(config);
6213
6690
  globalNumberingRegistry.register(numberingConfig);
@@ -6215,7 +6692,9 @@ function renderListComponent(component, theme, themeName) {
6215
6692
  return createList(resolvedConfig.items, theme, themeName, {
6216
6693
  numberingReference: reference,
6217
6694
  spacing: resolvedConfig.spacing,
6218
- alignment: resolvedConfig.alignment
6695
+ alignment: resolvedConfig.alignment,
6696
+ // Review comment spanning the whole list
6697
+ comment: resolvedConfig.comment
6219
6698
  });
6220
6699
  }
6221
6700
 
@@ -6306,10 +6785,10 @@ function convertBorders2(bordersConfig, theme) {
6306
6785
  // src/styles/utils/cellUtils.ts
6307
6786
  init_styleHelpers();
6308
6787
  init_colorUtils();
6309
- import { Paragraph as Paragraph2 } from "docx";
6788
+ import { Paragraph as Paragraph4 } from "docx";
6310
6789
  function buildCellOptions(children, styleCfg, theme) {
6311
6790
  const cellOpts = {
6312
- children: children.length ? children : [new Paragraph2({})],
6791
+ children: children.length ? children : [new Paragraph4({})],
6313
6792
  margins: {
6314
6793
  top: styleCfg?.padding?.top ? pointsToTwips(styleCfg.padding.top) : 0,
6315
6794
  right: styleCfg?.padding?.right ? pointsToTwips(styleCfg.padding.right) : 0,
@@ -6512,25 +6991,66 @@ async function renderTableComponent(component, theme, themeName) {
6512
6991
  }
6513
6992
 
6514
6993
  // src/components/section.ts
6515
- import { Paragraph as Paragraph4, BookmarkStart, BookmarkEnd } from "docx";
6516
- function generateSectionBookmarkId(context) {
6517
- const custom = context.custom ??= {};
6518
- const state = custom.sectionBookmarks ??= {
6519
- next: 1
6520
- };
6521
- const ordinal = state.next++;
6522
- const linkId = 1e6 + ordinal;
6523
- return {
6524
- id: `_NestedSection_${ordinal}`,
6525
- linkId
6526
- };
6527
- }
6994
+ import { Paragraph as Paragraph6, BookmarkStart, BookmarkEnd } from "docx";
6995
+
6996
+ // src/core/sectionBookmarks.ts
6997
+ import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
6998
+ var NESTED_LINK_ID_BASE = 1e6;
6999
+ function createState3() {
7000
+ return { nextNested: 1, resolved: /* @__PURE__ */ new WeakMap() };
7001
+ }
7002
+ var SectionBookmarkRegistry = class {
7003
+ fallback = createState3();
7004
+ scopes = new AsyncLocalStorage7();
7005
+ get state() {
7006
+ return this.scopes.getStore() ?? this.fallback;
7007
+ }
7008
+ /** Run work with an isolated registry that follows its async call chain. */
7009
+ runScoped(callback) {
7010
+ return this.scopes.run(createState3(), callback);
7011
+ }
7012
+ /**
7013
+ * The bookmark for a layout section, derived from its ordinal.
7014
+ *
7015
+ * Every layout chunk of one user-defined section shares an ordinal (see
7016
+ * `computeSectionOrdinals`), so they all resolve to the same bookmark — which
7017
+ * is the point: the start lands in the first chunk and the end in the last.
7018
+ */
7019
+ forLayoutSection(ordinal) {
7020
+ return { id: `_Section_${ordinal}`, linkId: ordinal };
7021
+ }
7022
+ /**
7023
+ * The bookmark for a `section` component, allocated on first sight and
7024
+ * remembered so re-resolving the same component never allocates twice.
7025
+ */
7026
+ forSectionComponent(component) {
7027
+ const state = this.state;
7028
+ const existing = state.resolved.get(component);
7029
+ if (existing) return existing;
7030
+ const ordinal = state.nextNested++;
7031
+ const bookmark = {
7032
+ id: `_NestedSection_${ordinal}`,
7033
+ linkId: NESTED_LINK_ID_BASE + ordinal
7034
+ };
7035
+ state.resolved.set(component, bookmark);
7036
+ return bookmark;
7037
+ }
7038
+ /** Test-only: reset the unscoped fallback counters. */
7039
+ clear() {
7040
+ if (!this.scopes.getStore()) {
7041
+ this.fallback = createState3();
7042
+ }
7043
+ }
7044
+ };
7045
+ var globalSectionBookmarkRegistry = new SectionBookmarkRegistry();
7046
+
7047
+ // src/components/section.ts
6528
7048
  async function renderSectionComponent(component, theme, themeName, context) {
6529
7049
  if (!isSectionComponent(component)) return [];
6530
7050
  const elements = [];
6531
- const { id: sectionBookmarkId, linkId: bookmarkLinkId } = generateSectionBookmarkId(context);
7051
+ const { id: sectionBookmarkId, linkId: bookmarkLinkId } = globalSectionBookmarkRegistry.forSectionComponent(component);
6532
7052
  elements.push(
6533
- new Paragraph4({
7053
+ new Paragraph6({
6534
7054
  children: [new BookmarkStart(sectionBookmarkId, bookmarkLinkId)],
6535
7055
  spacing: {
6536
7056
  before: 0,
@@ -6558,7 +7078,7 @@ async function renderSectionComponent(component, theme, themeName, context) {
6558
7078
  }
6559
7079
  }
6560
7080
  elements.push(
6561
- new Paragraph4({
7081
+ new Paragraph6({
6562
7082
  children: [new BookmarkEnd(bookmarkLinkId)],
6563
7083
  spacing: {
6564
7084
  before: 0,
@@ -6572,7 +7092,7 @@ async function renderSectionComponent(component, theme, themeName, context) {
6572
7092
 
6573
7093
  // src/components/columns.ts
6574
7094
  import {
6575
- Paragraph as Paragraph5,
7095
+ Paragraph as Paragraph7,
6576
7096
  Table as Table5,
6577
7097
  TableRow as TableRow3,
6578
7098
  TableCell as TableCell3,
@@ -6664,7 +7184,7 @@ async function renderColumnsAsTable(component, theme, themeName, context) {
6664
7184
  columnElements.push(...rendered);
6665
7185
  }
6666
7186
  if (columnElements.length === 0) {
6667
- columnElements.push(new Paragraph5({}));
7187
+ columnElements.push(new Paragraph7({}));
6668
7188
  }
6669
7189
  cells.push(
6670
7190
  new TableCell3({
@@ -6715,12 +7235,34 @@ function renderStatisticComponent(component, _theme) {
6715
7235
 
6716
7236
  // src/components/toc/index.ts
6717
7237
  import {
6718
- Paragraph as Paragraph6,
7238
+ Paragraph as Paragraph8,
6719
7239
  TableOfContents,
6720
7240
  AlignmentType as AlignmentType4,
6721
- TextRun as TextRun5,
7241
+ TextRun as TextRun8,
6722
7242
  StyleLevel
6723
7243
  } from "docx";
7244
+ function toStyleDisplayName(styleId) {
7245
+ return styleId.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
7246
+ }
7247
+ function selectCachedEntries(collected, options) {
7248
+ if (!collected || collected.length === 0) return [];
7249
+ const { depthStart, depthEnd, sectionBookmarkId, styleLevels } = options;
7250
+ const entries = [];
7251
+ for (const entry of collected) {
7252
+ if (sectionBookmarkId && entry.sectionBookmarkId !== sectionBookmarkId) {
7253
+ continue;
7254
+ }
7255
+ if (entry.styleId !== void 0) {
7256
+ const level = styleLevels.get(entry.styleId) ?? styleLevels.get(toStyleDisplayName(entry.styleId));
7257
+ if (level === void 0) continue;
7258
+ entries.push({ title: entry.title, level });
7259
+ continue;
7260
+ }
7261
+ if (entry.level < depthStart || entry.level > depthEnd) continue;
7262
+ entries.push({ title: entry.title, level: entry.level });
7263
+ }
7264
+ return entries;
7265
+ }
6724
7266
  function parseDepthRange(rawDepth, fieldName, defaultFrom = 1, defaultTo = 3) {
6725
7267
  if (typeof rawDepth !== "object" || rawDepth === null) {
6726
7268
  throw new Error(
@@ -6776,9 +7318,9 @@ function renderTocComponent(component, theme, context) {
6776
7318
  const paragraphs = [];
6777
7319
  if (componentProps.title) {
6778
7320
  paragraphs.push(
6779
- new Paragraph6({
7321
+ new Paragraph8({
6780
7322
  children: [
6781
- new TextRun5({
7323
+ new TextRun8({
6782
7324
  text: componentProps.title,
6783
7325
  bold: true,
6784
7326
  size: 28
@@ -6796,11 +7338,14 @@ function renderTocComponent(component, theme, context) {
6796
7338
  );
6797
7339
  }
6798
7340
  const stylesWithLevels = [];
7341
+ const styleLevels = /* @__PURE__ */ new Map();
6799
7342
  if (componentProps.styles && componentProps.styles.length > 0) {
6800
7343
  for (const styleMapping of componentProps.styles) {
6801
7344
  const styleId = styleMapping.styleId;
6802
7345
  const isCustomStyle = !!theme.styles && Object.prototype.hasOwnProperty.call(theme.styles, styleId);
6803
- const styleDisplayName = isCustomStyle ? styleId.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim() : styleId;
7346
+ const styleDisplayName = isCustomStyle ? toStyleDisplayName(styleId) : styleId;
7347
+ styleLevels.set(styleId, styleMapping.level);
7348
+ styleLevels.set(styleDisplayName, styleMapping.level);
6804
7349
  stylesWithLevels.push(
6805
7350
  new StyleLevel(styleDisplayName, styleMapping.level)
6806
7351
  );
@@ -6808,6 +7353,12 @@ function renderTocComponent(component, theme, context) {
6808
7353
  }
6809
7354
  const effectiveDepthStart = depthStart;
6810
7355
  const effectiveDepthEnd = depthEnd;
7356
+ const cachedEntries = selectCachedEntries(context?.tocHeadings, {
7357
+ depthStart,
7358
+ depthEnd,
7359
+ sectionBookmarkId,
7360
+ styleLevels
7361
+ });
6811
7362
  const tocOptions = {
6812
7363
  hyperlink: true,
6813
7364
  // Enable clickable hyperlinks (\h switch)
@@ -6846,7 +7397,10 @@ function renderTocComponent(component, theme, context) {
6846
7397
  // default to tab
6847
7398
  };
6848
7399
  paragraphs.push(
6849
- new TableOfContents(componentProps.title ?? "Table of Contents", tocOptions)
7400
+ new TableOfContents(componentProps.title ?? "Table of Contents", {
7401
+ ...tocOptions,
7402
+ ...cachedEntries.length > 0 && { cachedEntries }
7403
+ })
6850
7404
  );
6851
7405
  return paragraphs;
6852
7406
  }
@@ -7056,14 +7610,19 @@ function effectiveVisualServerUrl(props, serviceConfig) {
7056
7610
  if (serviceConfig?.render) return void 0;
7057
7611
  return props.serverUrl;
7058
7612
  }
7059
- async function rasterizeVisualSlide(presentation, dpi, propsServerUrl, serviceConfig, baseDir) {
7613
+ async function rasterizeVisualSlide(presentation, dpi, propsServerUrl, serviceConfig, baseDir, fonts) {
7060
7614
  if (!isNodeEnvironment()) {
7061
7615
  throw new Error(
7062
7616
  "Visual rasterization requires a Node.js environment. It is not available in browser environments."
7063
7617
  );
7064
7618
  }
7065
7619
  if (serviceConfig?.render) {
7066
- return serviceConfig.render({ presentation, dpi, baseDir });
7620
+ return serviceConfig.render({
7621
+ presentation,
7622
+ dpi,
7623
+ baseDir,
7624
+ ...fonts?.length ? { fonts: [...fonts] } : {}
7625
+ });
7067
7626
  }
7068
7627
  const serverUrl = resolveServiceUrl(
7069
7628
  propsServerUrl,
@@ -7073,7 +7632,12 @@ async function rasterizeVisualSlide(presentation, dpi, propsServerUrl, serviceCo
7073
7632
  const response = await postJsonToService({
7074
7633
  url: serverUrl,
7075
7634
  path: "/rasterize",
7076
- body: { presentation, dpi, ...baseDir !== void 0 && { baseDir } },
7635
+ body: {
7636
+ presentation,
7637
+ dpi,
7638
+ ...baseDir !== void 0 && { baseDir },
7639
+ ...fonts?.length ? { fonts } : {}
7640
+ },
7077
7641
  headers: serviceConfig?.headers,
7078
7642
  serviceLabel: "PPTX rasterization service",
7079
7643
  onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Configure services.pptx with a \`render\` callback or a running \`serverUrl\`.
@@ -7119,7 +7683,8 @@ async function renderVisualComponent(component, theme, themeName, context) {
7119
7683
  dpi,
7120
7684
  props.serverUrl,
7121
7685
  serviceConfig,
7122
- getBaseDir()
7686
+ getBaseDir(),
7687
+ context?.visualFonts
7123
7688
  );
7124
7689
  }
7125
7690
  return await createImage(
@@ -7253,6 +7818,11 @@ function collectVisualProps(root) {
7253
7818
  function toErrorMessage(error) {
7254
7819
  return error instanceof Error ? error.message : String(error);
7255
7820
  }
7821
+ var HTTP_STATUS_IN_MESSAGE = /\breturned (\d{3})\b/;
7822
+ function isSchemaRejection(error) {
7823
+ const match = HTTP_STATUS_IN_MESSAGE.exec(toErrorMessage(error));
7824
+ return match !== null && match[1] === "400";
7825
+ }
7256
7826
  function* chunksOf(items, size) {
7257
7827
  for (let i = 0; i < items.length; i += size) yield items.slice(i, i + size);
7258
7828
  }
@@ -7311,22 +7881,38 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
7311
7881
  const limit = createLimiter(
7312
7882
  Math.max(1, options.concurrency ?? DEFAULT_FALLBACK_CONCURRENCY)
7313
7883
  );
7884
+ let fontsRejected = false;
7885
+ const requestFonts = () => fontsRejected || !options.fonts?.length ? void 0 : options.fonts;
7886
+ const rasterizeOne = async (target) => {
7887
+ const fonts = requestFonts();
7888
+ const call = (withFonts) => rasterizeVisualSlide(
7889
+ target.presentation,
7890
+ target.dpi,
7891
+ void 0,
7892
+ serviceConfig,
7893
+ options.baseDir,
7894
+ withFonts
7895
+ );
7896
+ try {
7897
+ return { ok: true, ...await call(fonts) };
7898
+ } catch (error) {
7899
+ if (!fonts || !isSchemaRejection(error)) {
7900
+ return { ok: false, error: toErrorMessage(error) };
7901
+ }
7902
+ try {
7903
+ const result = await call(void 0);
7904
+ fontsRejected = true;
7905
+ return { ok: true, ...result };
7906
+ } catch {
7907
+ return { ok: false, error: toErrorMessage(error) };
7908
+ }
7909
+ }
7910
+ };
7314
7911
  const rasterizeIndividually = async (chunk) => {
7315
7912
  await Promise.all(
7316
7913
  chunk.map(
7317
7914
  (target) => limit(async () => {
7318
- try {
7319
- const result = await rasterizeVisualSlide(
7320
- target.presentation,
7321
- target.dpi,
7322
- void 0,
7323
- serviceConfig,
7324
- options.baseDir
7325
- );
7326
- map.set(target.key, { ok: true, ...result });
7327
- } catch (error) {
7328
- map.set(target.key, { ok: false, error: toErrorMessage(error) });
7329
- }
7915
+ map.set(target.key, await rasterizeOne(target));
7330
7916
  })
7331
7917
  )
7332
7918
  );
@@ -7339,7 +7925,8 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
7339
7925
  presentation: target.presentation,
7340
7926
  dpi: target.dpi
7341
7927
  })),
7342
- ...options.baseDir !== void 0 && { baseDir: options.baseDir }
7928
+ ...options.baseDir !== void 0 && { baseDir: options.baseDir },
7929
+ ...options.fonts?.length ? { fonts: [...options.fonts] } : {}
7343
7930
  });
7344
7931
  if (!applyBatchResponse(chunk, response, map)) {
7345
7932
  throw new Error(
@@ -7368,10 +7955,10 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
7368
7955
  serviceConfig?.serverUrl,
7369
7956
  DEFAULT_RASTERIZE_SERVER_URL
7370
7957
  );
7371
- for (const chunk of chunksOf(unique, MAX_RASTERIZE_BATCH_SLIDES)) {
7372
- let applied = false;
7958
+ const postBatch = async (chunk, fonts) => {
7959
+ let response;
7373
7960
  try {
7374
- const response = await postJsonToService({
7961
+ response = await postJsonToService({
7375
7962
  url: serverUrl,
7376
7963
  path: "/rasterize/batch",
7377
7964
  body: {
@@ -7379,22 +7966,112 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
7379
7966
  presentation: target.presentation,
7380
7967
  dpi: target.dpi
7381
7968
  })),
7382
- ...options.baseDir !== void 0 && { baseDir: options.baseDir }
7969
+ ...options.baseDir !== void 0 && { baseDir: options.baseDir },
7970
+ ...fonts?.length ? { fonts } : {}
7383
7971
  },
7384
7972
  headers: serviceConfig?.headers,
7385
7973
  timeoutMs: BATCH_TIMEOUT_BASE_MS + BATCH_TIMEOUT_PER_SLIDE_MS * chunk.length,
7386
7974
  serviceLabel: "PPTX batch rasterization service",
7387
7975
  onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Cause: ${cause}`
7388
7976
  });
7389
- applied = applyBatchResponse(chunk, await response.json(), map);
7977
+ } catch (error) {
7978
+ return { applied: false, schemaRejected: isSchemaRejection(error) };
7979
+ }
7980
+ try {
7981
+ if (applyBatchResponse(chunk, await response.json(), map)) {
7982
+ return { applied: true };
7983
+ }
7390
7984
  } catch {
7391
- applied = false;
7392
7985
  }
7393
- if (!applied) await rasterizeIndividually(chunk);
7986
+ return { applied: false, schemaRejected: false };
7987
+ };
7988
+ for (const chunk of chunksOf(unique, MAX_RASTERIZE_BATCH_SLIDES)) {
7989
+ const fonts = requestFonts();
7990
+ let outcome = await postBatch(chunk, fonts);
7991
+ if (!outcome.applied && fonts && outcome.schemaRejected) {
7992
+ const retry = await postBatch(chunk, void 0);
7993
+ if (retry.applied) {
7994
+ fontsRejected = true;
7995
+ outcome = retry;
7996
+ }
7997
+ }
7998
+ if (!outcome.applied) await rasterizeIndividually(chunk);
7394
7999
  }
7395
8000
  return map;
7396
8001
  }
7397
8002
 
8003
+ // src/core/sectionOrdinals.ts
8004
+ function computeSectionOrdinals(sections) {
8005
+ let counter = 0;
8006
+ return sections.map((section, index) => {
8007
+ if (!section.belongsToUserSection) {
8008
+ if (section.isUserSection) counter++;
8009
+ return { closeBookmark: false };
8010
+ }
8011
+ const ordinal = section.isUserSection ? counter + 1 : counter;
8012
+ if (section.isUserSection) counter++;
8013
+ const next = sections[index + 1];
8014
+ const closeBookmark = !next || !next.belongsToUserSection || next.isUserSection;
8015
+ return { ordinal, closeBookmark };
8016
+ });
8017
+ }
8018
+
8019
+ // src/core/collectTocHeadings.ts
8020
+ import { getStandardComponent } from "@json-to-office/shared-docx";
8021
+ function normalizeEntryTitle(text) {
8022
+ return normalizeUnicodeText(text).replace(/(\*\*\*|___)([\s\S]*?)\1/g, "$2").replace(/(\*\*|__)([\s\S]*?)\1/g, "$2").replace(/(\*|_)([\s\S]*?)\1/g, "$2").trim();
8023
+ }
8024
+ function isContainer(name) {
8025
+ return getStandardComponent(name)?.hasChildren === true;
8026
+ }
8027
+ function isEnabled(component) {
8028
+ return !("enabled" in component && component.enabled === false);
8029
+ }
8030
+ function styleEntryKey(props) {
8031
+ const themeStyle = props.themeStyle;
8032
+ if (typeof themeStyle !== "string" || !themeStyle) return void 0;
8033
+ if (/^heading[1-6]$/i.test(themeStyle)) return void 0;
8034
+ if (["normal", "title", "subtitle"].includes(themeStyle.toLowerCase())) {
8035
+ return void 0;
8036
+ }
8037
+ return themeStyle;
8038
+ }
8039
+ function collectTocHeadings(sections) {
8040
+ const entries = [];
8041
+ const ordinals = computeSectionOrdinals(sections);
8042
+ sections.forEach((section, index) => {
8043
+ const ordinal = ordinals[index]?.ordinal;
8044
+ const sectionBookmarkId = ordinal ? globalSectionBookmarkRegistry.forLayoutSection(ordinal).id : void 0;
8045
+ const visit = (component) => {
8046
+ if (!isEnabled(component)) return;
8047
+ const props = component.props ?? {};
8048
+ if (component.name === "heading") {
8049
+ const text = typeof props.text === "string" ? props.text : "";
8050
+ const title = normalizeEntryTitle(text);
8051
+ if (title) {
8052
+ const level = typeof props.level === "number" ? props.level : 1;
8053
+ entries.push({ title, level, sectionBookmarkId });
8054
+ }
8055
+ return;
8056
+ }
8057
+ if (component.name === "paragraph") {
8058
+ const styleId = styleEntryKey(props);
8059
+ const text = typeof props.text === "string" ? props.text : "";
8060
+ const title = normalizeEntryTitle(text);
8061
+ if (styleId && title) {
8062
+ entries.push({ title, level: 1, styleId, sectionBookmarkId });
8063
+ }
8064
+ return;
8065
+ }
8066
+ if (!isContainer(component.name)) return;
8067
+ const children = component.children;
8068
+ if (Array.isArray(children)) children.forEach(visit);
8069
+ };
8070
+ section.components.forEach(visit);
8071
+ });
8072
+ return entries;
8073
+ }
8074
+
7398
8075
  // src/core/render.ts
7399
8076
  function getAlignment3(alignment) {
7400
8077
  switch (alignment) {
@@ -7432,7 +8109,23 @@ async function renderDocument(structure, layout, options) {
7432
8109
  () => globalBookmarkRegistry.runScoped(
7433
8110
  () => globalRevisionIdRegistry.runScoped(
7434
8111
  () => globalNumberingRegistry.runScoped(
7435
- () => renderDocumentScoped(structure, layout, options)
8112
+ () => globalSectionBookmarkRegistry.runScoped(
8113
+ () => (
8114
+ // Comment ids are a separate OOXML namespace from w:ins/w:del,
8115
+ // but they need the same per-render isolation: outside this nest
8116
+ // concurrent generations would interleave counters and an anchor
8117
+ // would point at another document's comment body.
8118
+ globalCommentRegistry.runScoped(
8119
+ () => (
8120
+ // Footnote ids are document-scoped too: a reference resolved
8121
+ // against another render's counter points at the wrong body.
8122
+ globalNoteRegistry.runScoped(
8123
+ () => renderDocumentScoped(structure, layout, options)
8124
+ )
8125
+ )
8126
+ )
8127
+ )
8128
+ )
7436
8129
  )
7437
8130
  )
7438
8131
  )
@@ -7452,11 +8145,12 @@ async function renderDocumentScoped(structure, layout, options) {
7452
8145
  structure.themeName
7453
8146
  );
7454
8147
  context.services = options?.services;
8148
+ context.visualFonts = options?.visualFonts;
7455
8149
  try {
7456
8150
  const visualRasterResults = await prerasterizeVisuals(
7457
8151
  layout.sections,
7458
8152
  options?.services?.pptx,
7459
- { baseDir: getBaseDir() }
8153
+ { baseDir: getBaseDir(), fonts: options?.visualFonts }
7460
8154
  );
7461
8155
  if (visualRasterResults.size > 0) {
7462
8156
  context.visualRasterResults = visualRasterResults;
@@ -7467,19 +8161,23 @@ async function renderDocumentScoped(structure, layout, options) {
7467
8161
  error instanceof Error ? error.message : error
7468
8162
  );
7469
8163
  }
7470
- let sectionBookmarkCounter = 0;
8164
+ try {
8165
+ const tocHeadings = collectTocHeadings(layout.sections);
8166
+ if (tocHeadings.length > 0) {
8167
+ context.tocHeadings = tocHeadings;
8168
+ }
8169
+ } catch (error) {
8170
+ console.warn(
8171
+ "[core-docx] TOC entry collection failed; the TOC field will rely on the reader refreshing it:",
8172
+ error instanceof Error ? error.message : error
8173
+ );
8174
+ }
8175
+ const sectionOrdinals = computeSectionOrdinals(layout.sections);
7471
8176
  let previousHeader = void 0;
7472
8177
  let previousFooter = void 0;
7473
8178
  for (let idx = 0; idx < layout.sections.length; idx++) {
7474
8179
  const layoutSection = layout.sections[idx];
7475
- let sectionOrdinal = void 0;
7476
- if (layoutSection.belongsToUserSection) {
7477
- if (layoutSection.isUserSection) {
7478
- sectionOrdinal = sectionBookmarkCounter + 1;
7479
- } else {
7480
- sectionOrdinal = sectionBookmarkCounter;
7481
- }
7482
- }
8180
+ const { ordinal: sectionOrdinal, closeBookmark } = sectionOrdinals[idx];
7483
8181
  let headerToUse;
7484
8182
  if (layoutSection.header === "linkToPrevious") {
7485
8183
  headerToUse = previousHeader;
@@ -7503,13 +8201,6 @@ async function renderDocumentScoped(structure, layout, options) {
7503
8201
  header: headerToUse,
7504
8202
  footer: footerToUse
7505
8203
  };
7506
- let closeBookmark = false;
7507
- if (layoutSection.belongsToUserSection && sectionOrdinal !== void 0) {
7508
- const next = layout.sections[idx + 1];
7509
- if (!next || !next.belongsToUserSection || next.isUserSection) {
7510
- closeBookmark = true;
7511
- }
7512
- }
7513
8204
  const rendered = await renderSection(
7514
8205
  sectionToRender,
7515
8206
  structure.theme,
@@ -7519,14 +8210,14 @@ async function renderDocumentScoped(structure, layout, options) {
7519
8210
  closeBookmark,
7520
8211
  options?.bypassCache === true
7521
8212
  );
7522
- if (layoutSection.isUserSection) {
7523
- sectionBookmarkCounter++;
7524
- }
7525
8213
  if (rendered.children.length > 0) {
7526
8214
  sections.push(rendered);
7527
8215
  }
7528
8216
  }
7529
8217
  const numberingConfigs = globalNumberingRegistry.getAll();
8218
+ const comments = globalCommentRegistry.getAll();
8219
+ const footnotes = globalNoteRegistry.getFootnotes();
8220
+ const endnotes = globalNoteRegistry.getEndnotes();
7530
8221
  return new Document({
7531
8222
  styles: createWordStyles(structure.theme, structure.language),
7532
8223
  sections,
@@ -7537,6 +8228,12 @@ async function renderDocumentScoped(structure, layout, options) {
7537
8228
  // Word opens the document in review mode (further edits are tracked)
7538
8229
  ...structure.trackRevisions && { trackRevisions: true }
7539
8230
  },
8231
+ // word/comments.xml, emitted only when something was actually commented
8232
+ ...comments.length > 0 && { comments: { children: comments } },
8233
+ // word/footnotes.xml and word/endnotes.xml bodies, keyed by the id their
8234
+ // references carry
8235
+ ...Object.keys(footnotes).length > 0 && { footnotes },
8236
+ ...Object.keys(endnotes).length > 0 && { endnotes },
7540
8237
  // Add numbering configurations if any lists were rendered
7541
8238
  ...numberingConfigs.length > 0 && {
7542
8239
  numbering: {
@@ -7583,9 +8280,9 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7583
8280
  let imageSource = resolveImageSource(imageComp.props);
7584
8281
  if (!imageSource) {
7585
8282
  elements.push(
7586
- new Paragraph7({
8283
+ new Paragraph9({
7587
8284
  children: [
7588
- new TextRun6({
8285
+ new TextRun9({
7589
8286
  text: "[IMAGE: Missing path, base64, or svg property]",
7590
8287
  font: getThemeFonts(theme).body.family,
7591
8288
  size: 20,
@@ -7653,7 +8350,7 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7653
8350
  ...floatingOptions && { floating: floatingOptions }
7654
8351
  });
7655
8352
  elements.push(
7656
- new Paragraph7({
8353
+ new Paragraph9({
7657
8354
  children: [imageRun],
7658
8355
  alignment: imageComp.props.alignment ? getAlignment3(imageComp.props.alignment) : void 0,
7659
8356
  style: "Normal"
@@ -7666,9 +8363,9 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7666
8363
  error instanceof Error ? error.message : error
7667
8364
  );
7668
8365
  elements.push(
7669
- new Paragraph7({
8366
+ new Paragraph9({
7670
8367
  children: [
7671
- new TextRun6({
8368
+ new TextRun9({
7672
8369
  text: `[IMAGE: ${sourcePreview}]`,
7673
8370
  font: getThemeFonts(theme).body.family,
7674
8371
  size: 20,
@@ -7698,8 +8395,9 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7698
8395
  async function renderSection(section, theme, themeName, context, sectionOrdinal, closeBookmark, bypassCache = false) {
7699
8396
  const elements = [];
7700
8397
  const isFirstLayoutOfUserSection = section.isUserSection;
7701
- const sharedLinkId = section.belongsToUserSection && sectionOrdinal ? sectionOrdinal : void 0;
7702
- const sectionBookmarkId = sharedLinkId !== void 0 ? `_Section_${sharedLinkId}` : void 0;
8398
+ const bookmark = section.belongsToUserSection && sectionOrdinal ? globalSectionBookmarkRegistry.forLayoutSection(sectionOrdinal) : void 0;
8399
+ const sharedLinkId = bookmark?.linkId;
8400
+ const sectionBookmarkId = bookmark?.id;
7703
8401
  const sectionContext = {
7704
8402
  ...context,
7705
8403
  section: {
@@ -7712,7 +8410,7 @@ async function renderSection(section, theme, themeName, context, sectionOrdinal,
7712
8410
  };
7713
8411
  if (sectionBookmarkId && isFirstLayoutOfUserSection && sharedLinkId !== void 0) {
7714
8412
  elements.push(
7715
- new Paragraph7({
8413
+ new Paragraph9({
7716
8414
  children: [new BookmarkStart2(sectionBookmarkId, sharedLinkId)],
7717
8415
  spacing: {
7718
8416
  before: 0,
@@ -7737,7 +8435,7 @@ async function renderSection(section, theme, themeName, context, sectionOrdinal,
7737
8435
  }
7738
8436
  if (closeBookmark && sharedLinkId !== void 0) {
7739
8437
  elements.push(
7740
- new Paragraph7({
8438
+ new Paragraph9({
7741
8439
  children: [new BookmarkEnd2(sharedLinkId)]
7742
8440
  })
7743
8441
  );
@@ -7822,14 +8520,17 @@ async function renderComponent(component, theme, themeName, context) {
7822
8520
  import {
7823
8521
  collectFontNamesFromDocx,
7824
8522
  validateFontReferences,
7825
- FontRegistry
8523
+ FontRegistry,
8524
+ documentFontRegistry,
8525
+ themeFontRegistry,
8526
+ mergeFontRegistries
7826
8527
  } from "@json-to-office/shared";
7827
8528
  import {
7828
8529
  loadFileFontSource,
7829
8530
  FontDiskCache,
7830
8531
  fetchVariableFontSource
7831
8532
  } from "@json-to-office/shared/fonts/node";
7832
- async function resolveDocumentFonts(document, theme, fonts, warnings) {
8533
+ async function resolveDocumentFonts(document, theme, fonts, warnings, forceMaterialize = false) {
7833
8534
  const emit = (code, message) => {
7834
8535
  if (warnings) {
7835
8536
  warnings.push({
@@ -7846,9 +8547,14 @@ async function resolveDocumentFonts(document, theme, fonts, warnings) {
7846
8547
  for (const n of collectFontNamesFromDocx(document)) names.add(n);
7847
8548
  for (const n of collectFontNamesFromDocx(theme)) names.add(n);
7848
8549
  if (names.size === 0) return [];
8550
+ const registryEntries = mergeFontRegistries(
8551
+ themeFontRegistry(theme),
8552
+ documentFontRegistry(document),
8553
+ fonts?.extraEntries
8554
+ );
7849
8555
  const validation = validateFontReferences({
7850
8556
  referencedNames: names,
7851
- registeredEntries: fonts?.extraEntries
8557
+ registeredEntries: registryEntries
7852
8558
  });
7853
8559
  if (validation.warnings.length > 0) {
7854
8560
  if (fonts?.strict) {
@@ -7861,9 +8567,11 @@ async function resolveDocumentFonts(document, theme, fonts, warnings) {
7861
8567
  emit(w.code, w.message);
7862
8568
  }
7863
8569
  }
7864
- if (!fonts?.onResolved) return [];
8570
+ if (!fonts?.onResolved && !forceMaterialize) return [];
7865
8571
  const registry = new FontRegistry({
7866
- opts: fonts,
8572
+ // Spread keeps baseDir/googleFonts/mode/substitution intact for
8573
+ // materializeSource; extraEntries carries the merged registry.
8574
+ opts: { ...fonts, extraEntries: registryEntries },
7867
8575
  fileLoader: loadFileFontSource,
7868
8576
  variableLoader: fetchVariableFontSource,
7869
8577
  diskCache: fonts?.googleFonts?.cacheDir ? new FontDiskCache(fonts.googleFonts.cacheDir) : void 0
@@ -7874,10 +8582,13 @@ async function resolveDocumentFonts(document, theme, fonts, warnings) {
7874
8582
  emit("FONT_UNRESOLVED", msg);
7875
8583
  }
7876
8584
  }
7877
- fonts.onResolved(resolved);
8585
+ fonts?.onResolved?.(resolved);
7878
8586
  return resolved;
7879
8587
  }
7880
8588
 
8589
+ // src/core/generator.ts
8590
+ import { toRasterizeFontFaces } from "@json-to-office/shared/fonts/node";
8591
+
7881
8592
  // src/utils/packageDocument.ts
7882
8593
  import AdmZip2 from "adm-zip";
7883
8594
  import { Packer } from "docx";
@@ -7891,9 +8602,9 @@ function fixFloatingImageIdsInBuffer(buffer) {
7891
8602
  throw new Error("document.xml not found in DOCX");
7892
8603
  }
7893
8604
  let idCounter = 1;
7894
- const documentXml = documentEntry.getData().toString("utf8").replace(/<wp:docPr\s+id="(\d+)"/g, () => {
8605
+ const documentXml = documentEntry.getData().toString("utf8").replace(/(<wp:docPr\b[^>]*?\s)id="\d+"/g, (_match, prefix) => {
7895
8606
  const newId = idCounter++;
7896
- return `<wp:docPr id="${newId}"`;
8607
+ return `${prefix}id="${newId}"`;
7897
8608
  });
7898
8609
  zip.updateFile(documentEntry, Buffer.from(documentXml, "utf8"));
7899
8610
  return zip.toBuffer();
@@ -8137,7 +8848,15 @@ async function generateDocumentWithCustomThemes(documentIn, customThemes, servic
8137
8848
  fonts,
8138
8849
  warnings
8139
8850
  });
8140
- await resolveDocumentFonts(document, theme, fonts, warnings);
8851
+ const hasVisual = collectVisualProps(document).length > 0;
8852
+ const resolvedFonts = await resolveDocumentFonts(
8853
+ document,
8854
+ theme,
8855
+ fonts,
8856
+ warnings,
8857
+ hasVisual
8858
+ );
8859
+ const visualFonts = hasVisual ? toRasterizeFontFaces(resolvedFonts, warnings) : [];
8141
8860
  const structure = await processDocument(
8142
8861
  document,
8143
8862
  theme,
@@ -8148,7 +8867,8 @@ async function generateDocumentWithCustomThemes(documentIn, customThemes, servic
8148
8867
  const renderedDocument = await renderDocument(structure, layout, {
8149
8868
  bypassCache: false,
8150
8869
  services,
8151
- baseDir
8870
+ baseDir,
8871
+ ...visualFonts.length > 0 && { visualFonts }
8152
8872
  });
8153
8873
  return renderedDocument;
8154
8874
  }
@@ -8262,7 +8982,11 @@ async function flattenVisuals(doc, options) {
8262
8982
  renderBatch: options.rasterizeBatch,
8263
8983
  dpi: options.dpi
8264
8984
  },
8265
- { baseDir: options.baseDir, concurrency: options.concurrency }
8985
+ {
8986
+ baseDir: options.baseDir,
8987
+ concurrency: options.concurrency,
8988
+ fonts: options.fonts
8989
+ }
8266
8990
  ).catch(() => /* @__PURE__ */ new Map());
8267
8991
  rasterize = async (request) => {
8268
8992
  const hit = preRasterized.get(
@@ -8283,6 +9007,7 @@ async function flattenVisuals(doc, options) {
8283
9007
  rasterize,
8284
9008
  dpi: options.dpi,
8285
9009
  baseDir: options.baseDir,
9010
+ fonts: options.fonts,
8286
9011
  limit: createLimiter(
8287
9012
  Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY)
8288
9013
  )
@@ -8296,7 +9021,8 @@ async function rasterizeVisual(obj, ctx) {
8296
9021
  () => ctx.rasterize({
8297
9022
  presentation: buildVisualPresentation(props),
8298
9023
  dpi,
8299
- baseDir: ctx.baseDir
9024
+ baseDir: ctx.baseDir,
9025
+ ...ctx.fonts?.length ? { fonts: [...ctx.fonts] } : {}
8300
9026
  })
8301
9027
  );
8302
9028
  const image = {
@@ -8572,6 +9298,7 @@ async function runExample(example, options = {}) {
8572
9298
 
8573
9299
  // src/plugin/createDocumentGenerator.ts
8574
9300
  init_themes();
9301
+ import { toRasterizeFontFaces as toRasterizeFontFaces2 } from "@json-to-office/shared/fonts/node";
8575
9302
 
8576
9303
  // src/plugin/version-resolver.ts
8577
9304
  import { resolveComponentVersion } from "@json-to-office/shared/plugin";
@@ -9099,7 +9826,15 @@ function createBuilderImpl(state) {
9099
9826
  warnings,
9100
9827
  preserveSet
9101
9828
  } = await expandDocument(document, options);
9102
- await resolveDocumentFonts(modedDoc, modedTheme, state.fonts, warnings);
9829
+ const hasVisual = collectVisualProps(modedDoc).length > 0;
9830
+ const resolvedFonts = await resolveDocumentFonts(
9831
+ modedDoc,
9832
+ modedTheme,
9833
+ state.fonts,
9834
+ warnings,
9835
+ hasVisual
9836
+ );
9837
+ const visualFonts = hasVisual ? toRasterizeFontFaces2(resolvedFonts, warnings) : [];
9103
9838
  const packageOptions = {
9104
9839
  deterministic: options?.deterministic ?? state.deterministic,
9105
9840
  generatedAt: options?.generatedAt ?? state.generatedAt
@@ -9114,7 +9849,8 @@ function createBuilderImpl(state) {
9114
9849
  const generatedDocument = await renderDocument(structure, layout, {
9115
9850
  services: state.services,
9116
9851
  bypassCache: !state.enableCache,
9117
- baseDir: options?.baseDir ?? state.baseDir
9852
+ baseDir: options?.baseDir ?? state.baseDir,
9853
+ ...visualFonts.length > 0 && { visualFonts }
9118
9854
  });
9119
9855
  const preservedDefinition = preserveSet ? {
9120
9856
  ...modedRoot,
@@ -9273,6 +10009,7 @@ export {
9273
10009
  UnknownPreservedComponentError,
9274
10010
  cleanComponentProps,
9275
10011
  clearComponentCache,
10012
+ componentBypassReason,
9276
10013
  corporateTheme,
9277
10014
  createComponent,
9278
10015
  createDocumentGenerator,