@json-to-office/core-docx 0.32.0 → 0.34.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 (58) 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/core/cached-render.d.ts +9 -1
  8. package/dist/core/cached-render.d.ts.map +1 -1
  9. package/dist/core/collectTocHeadings.d.ts +60 -0
  10. package/dist/core/collectTocHeadings.d.ts.map +1 -0
  11. package/dist/core/content.d.ts +17 -1
  12. package/dist/core/content.d.ts.map +1 -1
  13. package/dist/core/layout.d.ts +5 -0
  14. package/dist/core/layout.d.ts.map +1 -1
  15. package/dist/core/render.d.ts.map +1 -1
  16. package/dist/core/sectionBookmarks.d.ts +53 -0
  17. package/dist/core/sectionBookmarks.d.ts.map +1 -0
  18. package/dist/core/sectionOrdinals.d.ts +32 -0
  19. package/dist/core/sectionOrdinals.d.ts.map +1 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +816 -169
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugin/example/index.js +815 -169
  25. package/dist/plugin/example/index.js.map +1 -1
  26. package/dist/styles/index.d.ts +2 -2
  27. package/dist/styles/index.d.ts.map +1 -1
  28. package/dist/styles/themeToDocxAdapter.d.ts.map +1 -1
  29. package/dist/styles/utils/layoutUtils.d.ts +48 -0
  30. package/dist/styles/utils/layoutUtils.d.ts.map +1 -1
  31. package/dist/templates/themes/index.d.ts +400 -8
  32. package/dist/templates/themes/index.d.ts.map +1 -1
  33. package/dist/themes/defaults.d.ts +96 -0
  34. package/dist/themes/defaults.d.ts.map +1 -1
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/dist/types/index.d.ts +8 -0
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/dist/utils/commentAnchors.d.ts +39 -0
  39. package/dist/utils/commentAnchors.d.ts.map +1 -0
  40. package/dist/utils/commentRegistry.d.ts +49 -0
  41. package/dist/utils/commentRegistry.d.ts.map +1 -0
  42. package/dist/utils/componentAnnotations.d.ts +23 -0
  43. package/dist/utils/componentAnnotations.d.ts.map +1 -0
  44. package/dist/utils/fixFloatingImageIds.d.ts +1 -1
  45. package/dist/utils/fixFloatingImageIds.d.ts.map +1 -1
  46. package/dist/utils/noteRegistry.d.ts +43 -0
  47. package/dist/utils/noteRegistry.d.ts.map +1 -0
  48. package/dist/utils/noteResolver.d.ts +31 -0
  49. package/dist/utils/noteResolver.d.ts.map +1 -0
  50. package/dist/utils/numberingConfig.d.ts +15 -0
  51. package/dist/utils/numberingConfig.d.ts.map +1 -1
  52. package/dist/utils/placeholderProcessor.d.ts +2 -2
  53. package/dist/utils/placeholderProcessor.d.ts.map +1 -1
  54. package/dist/utils/revisionUtils.d.ts +32 -9
  55. package/dist/utils/revisionUtils.d.ts.map +1 -1
  56. package/dist/utils/textParser.d.ts +12 -0
  57. package/dist/utils/textParser.d.ts.map +1 -1
  58. 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
  }
@@ -7395,6 +7949,78 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
7395
7949
  return map;
7396
7950
  }
7397
7951
 
7952
+ // src/core/sectionOrdinals.ts
7953
+ function computeSectionOrdinals(sections) {
7954
+ let counter = 0;
7955
+ return sections.map((section, index) => {
7956
+ if (!section.belongsToUserSection) {
7957
+ if (section.isUserSection) counter++;
7958
+ return { closeBookmark: false };
7959
+ }
7960
+ const ordinal = section.isUserSection ? counter + 1 : counter;
7961
+ if (section.isUserSection) counter++;
7962
+ const next = sections[index + 1];
7963
+ const closeBookmark = !next || !next.belongsToUserSection || next.isUserSection;
7964
+ return { ordinal, closeBookmark };
7965
+ });
7966
+ }
7967
+
7968
+ // src/core/collectTocHeadings.ts
7969
+ import { getStandardComponent } from "@json-to-office/shared-docx";
7970
+ function normalizeEntryTitle(text) {
7971
+ return normalizeUnicodeText(text).replace(/(\*\*\*|___)([\s\S]*?)\1/g, "$2").replace(/(\*\*|__)([\s\S]*?)\1/g, "$2").replace(/(\*|_)([\s\S]*?)\1/g, "$2").trim();
7972
+ }
7973
+ function isContainer(name) {
7974
+ return getStandardComponent(name)?.hasChildren === true;
7975
+ }
7976
+ function isEnabled(component) {
7977
+ return !("enabled" in component && component.enabled === false);
7978
+ }
7979
+ function styleEntryKey(props) {
7980
+ const themeStyle = props.themeStyle;
7981
+ if (typeof themeStyle !== "string" || !themeStyle) return void 0;
7982
+ if (/^heading[1-6]$/i.test(themeStyle)) return void 0;
7983
+ if (["normal", "title", "subtitle"].includes(themeStyle.toLowerCase())) {
7984
+ return void 0;
7985
+ }
7986
+ return themeStyle;
7987
+ }
7988
+ function collectTocHeadings(sections) {
7989
+ const entries = [];
7990
+ const ordinals = computeSectionOrdinals(sections);
7991
+ sections.forEach((section, index) => {
7992
+ const ordinal = ordinals[index]?.ordinal;
7993
+ const sectionBookmarkId = ordinal ? globalSectionBookmarkRegistry.forLayoutSection(ordinal).id : void 0;
7994
+ const visit = (component) => {
7995
+ if (!isEnabled(component)) return;
7996
+ const props = component.props ?? {};
7997
+ if (component.name === "heading") {
7998
+ const text = typeof props.text === "string" ? props.text : "";
7999
+ const title = normalizeEntryTitle(text);
8000
+ if (title) {
8001
+ const level = typeof props.level === "number" ? props.level : 1;
8002
+ entries.push({ title, level, sectionBookmarkId });
8003
+ }
8004
+ return;
8005
+ }
8006
+ if (component.name === "paragraph") {
8007
+ const styleId = styleEntryKey(props);
8008
+ const text = typeof props.text === "string" ? props.text : "";
8009
+ const title = normalizeEntryTitle(text);
8010
+ if (styleId && title) {
8011
+ entries.push({ title, level: 1, styleId, sectionBookmarkId });
8012
+ }
8013
+ return;
8014
+ }
8015
+ if (!isContainer(component.name)) return;
8016
+ const children = component.children;
8017
+ if (Array.isArray(children)) children.forEach(visit);
8018
+ };
8019
+ section.components.forEach(visit);
8020
+ });
8021
+ return entries;
8022
+ }
8023
+
7398
8024
  // src/core/render.ts
7399
8025
  function getAlignment3(alignment) {
7400
8026
  switch (alignment) {
@@ -7432,7 +8058,23 @@ async function renderDocument(structure, layout, options) {
7432
8058
  () => globalBookmarkRegistry.runScoped(
7433
8059
  () => globalRevisionIdRegistry.runScoped(
7434
8060
  () => globalNumberingRegistry.runScoped(
7435
- () => renderDocumentScoped(structure, layout, options)
8061
+ () => globalSectionBookmarkRegistry.runScoped(
8062
+ () => (
8063
+ // Comment ids are a separate OOXML namespace from w:ins/w:del,
8064
+ // but they need the same per-render isolation: outside this nest
8065
+ // concurrent generations would interleave counters and an anchor
8066
+ // would point at another document's comment body.
8067
+ globalCommentRegistry.runScoped(
8068
+ () => (
8069
+ // Footnote ids are document-scoped too: a reference resolved
8070
+ // against another render's counter points at the wrong body.
8071
+ globalNoteRegistry.runScoped(
8072
+ () => renderDocumentScoped(structure, layout, options)
8073
+ )
8074
+ )
8075
+ )
8076
+ )
8077
+ )
7436
8078
  )
7437
8079
  )
7438
8080
  )
@@ -7467,19 +8109,23 @@ async function renderDocumentScoped(structure, layout, options) {
7467
8109
  error instanceof Error ? error.message : error
7468
8110
  );
7469
8111
  }
7470
- let sectionBookmarkCounter = 0;
8112
+ try {
8113
+ const tocHeadings = collectTocHeadings(layout.sections);
8114
+ if (tocHeadings.length > 0) {
8115
+ context.tocHeadings = tocHeadings;
8116
+ }
8117
+ } catch (error) {
8118
+ console.warn(
8119
+ "[core-docx] TOC entry collection failed; the TOC field will rely on the reader refreshing it:",
8120
+ error instanceof Error ? error.message : error
8121
+ );
8122
+ }
8123
+ const sectionOrdinals = computeSectionOrdinals(layout.sections);
7471
8124
  let previousHeader = void 0;
7472
8125
  let previousFooter = void 0;
7473
8126
  for (let idx = 0; idx < layout.sections.length; idx++) {
7474
8127
  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
- }
8128
+ const { ordinal: sectionOrdinal, closeBookmark } = sectionOrdinals[idx];
7483
8129
  let headerToUse;
7484
8130
  if (layoutSection.header === "linkToPrevious") {
7485
8131
  headerToUse = previousHeader;
@@ -7503,13 +8149,6 @@ async function renderDocumentScoped(structure, layout, options) {
7503
8149
  header: headerToUse,
7504
8150
  footer: footerToUse
7505
8151
  };
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
8152
  const rendered = await renderSection(
7514
8153
  sectionToRender,
7515
8154
  structure.theme,
@@ -7519,14 +8158,14 @@ async function renderDocumentScoped(structure, layout, options) {
7519
8158
  closeBookmark,
7520
8159
  options?.bypassCache === true
7521
8160
  );
7522
- if (layoutSection.isUserSection) {
7523
- sectionBookmarkCounter++;
7524
- }
7525
8161
  if (rendered.children.length > 0) {
7526
8162
  sections.push(rendered);
7527
8163
  }
7528
8164
  }
7529
8165
  const numberingConfigs = globalNumberingRegistry.getAll();
8166
+ const comments = globalCommentRegistry.getAll();
8167
+ const footnotes = globalNoteRegistry.getFootnotes();
8168
+ const endnotes = globalNoteRegistry.getEndnotes();
7530
8169
  return new Document({
7531
8170
  styles: createWordStyles(structure.theme, structure.language),
7532
8171
  sections,
@@ -7537,6 +8176,12 @@ async function renderDocumentScoped(structure, layout, options) {
7537
8176
  // Word opens the document in review mode (further edits are tracked)
7538
8177
  ...structure.trackRevisions && { trackRevisions: true }
7539
8178
  },
8179
+ // word/comments.xml, emitted only when something was actually commented
8180
+ ...comments.length > 0 && { comments: { children: comments } },
8181
+ // word/footnotes.xml and word/endnotes.xml bodies, keyed by the id their
8182
+ // references carry
8183
+ ...Object.keys(footnotes).length > 0 && { footnotes },
8184
+ ...Object.keys(endnotes).length > 0 && { endnotes },
7540
8185
  // Add numbering configurations if any lists were rendered
7541
8186
  ...numberingConfigs.length > 0 && {
7542
8187
  numbering: {
@@ -7583,9 +8228,9 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7583
8228
  let imageSource = resolveImageSource(imageComp.props);
7584
8229
  if (!imageSource) {
7585
8230
  elements.push(
7586
- new Paragraph7({
8231
+ new Paragraph9({
7587
8232
  children: [
7588
- new TextRun6({
8233
+ new TextRun9({
7589
8234
  text: "[IMAGE: Missing path, base64, or svg property]",
7590
8235
  font: getThemeFonts(theme).body.family,
7591
8236
  size: 20,
@@ -7653,7 +8298,7 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7653
8298
  ...floatingOptions && { floating: floatingOptions }
7654
8299
  });
7655
8300
  elements.push(
7656
- new Paragraph7({
8301
+ new Paragraph9({
7657
8302
  children: [imageRun],
7658
8303
  alignment: imageComp.props.alignment ? getAlignment3(imageComp.props.alignment) : void 0,
7659
8304
  style: "Normal"
@@ -7666,9 +8311,9 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7666
8311
  error instanceof Error ? error.message : error
7667
8312
  );
7668
8313
  elements.push(
7669
- new Paragraph7({
8314
+ new Paragraph9({
7670
8315
  children: [
7671
- new TextRun6({
8316
+ new TextRun9({
7672
8317
  text: `[IMAGE: ${sourcePreview}]`,
7673
8318
  font: getThemeFonts(theme).body.family,
7674
8319
  size: 20,
@@ -7698,8 +8343,9 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7698
8343
  async function renderSection(section, theme, themeName, context, sectionOrdinal, closeBookmark, bypassCache = false) {
7699
8344
  const elements = [];
7700
8345
  const isFirstLayoutOfUserSection = section.isUserSection;
7701
- const sharedLinkId = section.belongsToUserSection && sectionOrdinal ? sectionOrdinal : void 0;
7702
- const sectionBookmarkId = sharedLinkId !== void 0 ? `_Section_${sharedLinkId}` : void 0;
8346
+ const bookmark = section.belongsToUserSection && sectionOrdinal ? globalSectionBookmarkRegistry.forLayoutSection(sectionOrdinal) : void 0;
8347
+ const sharedLinkId = bookmark?.linkId;
8348
+ const sectionBookmarkId = bookmark?.id;
7703
8349
  const sectionContext = {
7704
8350
  ...context,
7705
8351
  section: {
@@ -7712,7 +8358,7 @@ async function renderSection(section, theme, themeName, context, sectionOrdinal,
7712
8358
  };
7713
8359
  if (sectionBookmarkId && isFirstLayoutOfUserSection && sharedLinkId !== void 0) {
7714
8360
  elements.push(
7715
- new Paragraph7({
8361
+ new Paragraph9({
7716
8362
  children: [new BookmarkStart2(sectionBookmarkId, sharedLinkId)],
7717
8363
  spacing: {
7718
8364
  before: 0,
@@ -7737,7 +8383,7 @@ async function renderSection(section, theme, themeName, context, sectionOrdinal,
7737
8383
  }
7738
8384
  if (closeBookmark && sharedLinkId !== void 0) {
7739
8385
  elements.push(
7740
- new Paragraph7({
8386
+ new Paragraph9({
7741
8387
  children: [new BookmarkEnd2(sharedLinkId)]
7742
8388
  })
7743
8389
  );
@@ -7891,9 +8537,9 @@ function fixFloatingImageIdsInBuffer(buffer) {
7891
8537
  throw new Error("document.xml not found in DOCX");
7892
8538
  }
7893
8539
  let idCounter = 1;
7894
- const documentXml = documentEntry.getData().toString("utf8").replace(/<wp:docPr\s+id="(\d+)"/g, () => {
8540
+ const documentXml = documentEntry.getData().toString("utf8").replace(/(<wp:docPr\b[^>]*?\s)id="\d+"/g, (_match, prefix) => {
7895
8541
  const newId = idCounter++;
7896
- return `<wp:docPr id="${newId}"`;
8542
+ return `${prefix}id="${newId}"`;
7897
8543
  });
7898
8544
  zip.updateFile(documentEntry, Buffer.from(documentXml, "utf8"));
7899
8545
  return zip.toBuffer();
@@ -9273,6 +9919,7 @@ export {
9273
9919
  UnknownPreservedComponentError,
9274
9920
  cleanComponentProps,
9275
9921
  clearComponentCache,
9922
+ componentBypassReason,
9276
9923
  corporateTheme,
9277
9924
  createComponent,
9278
9925
  createDocumentGenerator,