@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
@@ -1128,7 +1128,7 @@ function getPageDimensions(size) {
1128
1128
  if (typeof size === "string") {
1129
1129
  return PAGE_SIZES[size];
1130
1130
  }
1131
- return size;
1131
+ return { width: size.width, height: size.height };
1132
1132
  }
1133
1133
  var resolveTableParagraphSpacing, PAGE_SIZES, getTableStyle, getDocumentMargins, getPageSetup;
1134
1134
  var init_layoutUtils = __esm({
@@ -1145,10 +1145,10 @@ var init_layoutUtils = __esm({
1145
1145
  };
1146
1146
  };
1147
1147
  PAGE_SIZES = {
1148
- A4: { width: 11906, height: 16838 },
1149
- A3: { width: 16838, height: 23811 },
1150
- LETTER: { width: 12240, height: 15840 },
1151
- LEGAL: { width: 12240, height: 20160 }
1148
+ A4: { width: 11906, height: 16838, code: 9 },
1149
+ A3: { width: 16838, height: 23811, code: 8 },
1150
+ LETTER: { width: 12240, height: 15840, code: 1 },
1151
+ LEGAL: { width: 12240, height: 20160, code: 5 }
1152
1152
  };
1153
1153
  getTableStyle = (theme, themeName) => {
1154
1154
  const themeConfig = resolveTheme(theme, themeName);
@@ -1285,7 +1285,8 @@ var init_layoutUtils = __esm({
1285
1285
  return {
1286
1286
  size: {
1287
1287
  width: dimensions.width,
1288
- height: dimensions.height
1288
+ height: dimensions.height,
1289
+ ...dimensions.code !== void 0 && { code: dimensions.code }
1289
1290
  },
1290
1291
  margin: {
1291
1292
  top: margins.top ?? defaultMargins.top ?? 1440,
@@ -1297,10 +1298,11 @@ var init_layoutUtils = __esm({
1297
1298
  }
1298
1299
  return {
1299
1300
  size: {
1300
- width: 11906,
1301
- // 8.5 inches in twips (Letter size)
1302
- height: 16838
1303
- // 11.7 inches in twips (Letter size)
1301
+ width: PAGE_SIZES.A4.width,
1302
+ // 210mm in twips (A4)
1303
+ height: PAGE_SIZES.A4.height,
1304
+ // 297mm in twips (A4)
1305
+ code: PAGE_SIZES.A4.code
1304
1306
  },
1305
1307
  margin: {
1306
1308
  top: defaultMargins.top ?? 1440,
@@ -2699,18 +2701,7 @@ function getColumnSettings(layout) {
2699
2701
  function createSectionProperties(columnSettings, theme, themeName, sectionType, pageOverride) {
2700
2702
  const basePageSetup = getPageSetup(theme, themeName);
2701
2703
  const pageSetup = pageOverride ? {
2702
- size: {
2703
- ...basePageSetup.size,
2704
- ...pageOverride.size && typeof pageOverride.size === "object" ? pageOverride.size : pageOverride.size ? (() => {
2705
- const sizes = {
2706
- A4: { width: 11906, height: 16838 },
2707
- A3: { width: 16838, height: 23811 },
2708
- LETTER: { width: 12240, height: 15840 },
2709
- LEGAL: { width: 12240, height: 20160 }
2710
- };
2711
- return sizes[pageOverride.size];
2712
- })() : {}
2713
- },
2704
+ size: pageOverride.size ? getPageDimensions(pageOverride.size) : basePageSetup.size,
2714
2705
  margin: {
2715
2706
  ...basePageSetup.margin,
2716
2707
  ...pageOverride.margins || {}
@@ -2812,8 +2803,8 @@ Suggestion: ${suggestion}`
2812
2803
  // src/core/render.ts
2813
2804
  import {
2814
2805
  Document,
2815
- Paragraph as Paragraph7,
2816
- TextRun as TextRun6,
2806
+ Paragraph as Paragraph9,
2807
+ TextRun as TextRun9,
2817
2808
  AlignmentType as AlignmentType5,
2818
2809
  BookmarkStart as BookmarkStart2,
2819
2810
  BookmarkEnd as BookmarkEnd2
@@ -3232,17 +3223,19 @@ function convertBorders(borders, theme) {
3232
3223
  color: resolveColor(side.color, theme),
3233
3224
  ...side.space !== void 0 ? { space: side.space } : {}
3234
3225
  } : void 0;
3235
- const top = mapSide(borders.top);
3236
- const bottom = mapSide(borders.bottom);
3237
- const left = mapSide(borders.left);
3238
- const right = mapSide(borders.right);
3239
- const anyDefined = top || bottom || left || right;
3240
- return anyDefined ? {
3241
- ...top && { top },
3242
- ...bottom && { bottom },
3243
- ...left && { left },
3244
- ...right && { right }
3245
- } : void 0;
3226
+ const sides = [
3227
+ "top",
3228
+ "bottom",
3229
+ "left",
3230
+ "right",
3231
+ "between"
3232
+ ];
3233
+ const converted = {};
3234
+ for (const side of sides) {
3235
+ const value = mapSide(borders[side]);
3236
+ if (value) converted[side] = value;
3237
+ }
3238
+ return Object.keys(converted).length > 0 ? converted : void 0;
3246
3239
  }
3247
3240
  function createWordStyles(themeNameOrObject = "minimal", language) {
3248
3241
  const theme = typeof themeNameOrObject === "string" ? getTheme(themeNameOrObject) || getTheme("minimal") : themeNameOrObject;
@@ -3611,7 +3604,7 @@ function createWordStyles(themeNameOrObject = "minimal", language) {
3611
3604
  }
3612
3605
  const tocMatch = /^TOC([1-9])$/.exec(styleKey);
3613
3606
  const customStyle = tocMatch ? styleValue : resolveStyleWithBaseStyle(theme, styleKey) || styleValue;
3614
- const styleId = tocMatch ? `JTD_TOC${tocMatch[1]}` : styleKey;
3607
+ const styleId = tocMatch ? `TOC${tocMatch[1]}` : styleKey;
3615
3608
  const styleName = tocMatch ? `TOC ${tocMatch[1]}` : styleKey.replace(/([A-Z])/g, " $1").trim();
3616
3609
  const mapBaseStyleId = (base) => {
3617
3610
  if (!base) return "Normal";
@@ -3683,8 +3676,8 @@ function createWordStyles(themeNameOrObject = "minimal", language) {
3683
3676
  continue;
3684
3677
  }
3685
3678
  paragraphStyles.push({
3686
- // Use a namespaced ID but canonical display name
3687
- id: `JTD_TOC${i}`,
3679
+ // Canonical id and display name see the styleId note above.
3680
+ id: `TOC${i}`,
3688
3681
  name: `TOC ${i}`,
3689
3682
  basedOn: "Normal",
3690
3683
  next: "Normal",
@@ -3707,20 +3700,43 @@ function createWordStyles(themeNameOrObject = "minimal", language) {
3707
3700
  }
3708
3701
  });
3709
3702
  }
3703
+ const noteStyle = resolveStyleWithBaseStyle(theme, "normal") || theme.styles?.normal;
3704
+ const bodyRun = convertRunProperties(
3705
+ mergeFontAndStyleProperties(resolveFontProperties(theme, noteStyle?.font), {
3706
+ size: noteStyle?.size,
3707
+ color: noteStyle?.color
3708
+ }),
3709
+ theme,
3710
+ theme.colors.text
3711
+ );
3712
+ const noteRun = {
3713
+ ...bodyRun,
3714
+ ...typeof bodyRun.size === "number" && {
3715
+ size: Math.max(12, bodyRun.size - 4)
3716
+ }
3717
+ };
3710
3718
  return {
3711
3719
  paragraphStyles,
3712
3720
  // Cast needed due to docx typing
3713
- // Document-default proofing language (w:docDefaults/w:rPrDefault). Runs that
3714
- // don't set their own w:lang inherit this, so Word spell-checks the whole
3715
- // document in the requested language unless a component overrides it.
3716
- ...language && {
3717
- default: {
3721
+ default: {
3722
+ // Document-default proofing language (w:docDefaults/w:rPrDefault). Runs
3723
+ // that don't set their own w:lang inherit this, so Word spell-checks the
3724
+ // whole document in the requested language unless a component overrides
3725
+ // it.
3726
+ ...language && {
3718
3727
  document: {
3719
3728
  run: {
3720
3729
  language: { value: language }
3721
3730
  }
3722
3731
  }
3723
- }
3732
+ },
3733
+ // Run properties only: docx's own note paragraph defaults (single line
3734
+ // spacing, no space after) are already what a note wants, and an empty
3735
+ // spacing override would drop its line rule.
3736
+ footnoteText: { run: noteRun },
3737
+ footnoteReference: { run: { ...noteRun, superScript: true } },
3738
+ endnoteText: { run: noteRun },
3739
+ endnoteReference: { run: { ...noteRun, superScript: true } }
3724
3740
  }
3725
3741
  };
3726
3742
  }
@@ -3842,7 +3858,15 @@ import {
3842
3858
  } from "docx";
3843
3859
 
3844
3860
  // src/utils/textParser.ts
3845
- import { TextRun, ExternalHyperlink, InternalHyperlink, Tab } from "docx";
3861
+ import {
3862
+ TextRun,
3863
+ ExternalHyperlink,
3864
+ InternalHyperlink,
3865
+ FootnoteReferenceRun,
3866
+ EndnoteReferenceRun,
3867
+ Tab
3868
+ } from "docx";
3869
+ var FOOTNOTE_MARKER_REGEX = /\[\^([^\]\s]+)\]/;
3846
3870
  function escapeRegExp(s) {
3847
3871
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3848
3872
  }
@@ -4019,8 +4043,8 @@ function buildTextRuns(text, commonProps, opts = {}) {
4019
4043
  return runs;
4020
4044
  }
4021
4045
  function createTextRunsWithNewlines(text, baseStyle, options, overrideStyle) {
4022
- return buildTextRuns(
4023
- text,
4046
+ const runs = (segment) => buildTextRuns(
4047
+ segment,
4024
4048
  buildRunCommonProps(baseStyle, {
4025
4049
  bold: overrideStyle?.bold,
4026
4050
  italics: overrideStyle?.italics,
@@ -4028,6 +4052,34 @@ function createTextRunsWithNewlines(text, baseStyle, options, overrideStyle) {
4028
4052
  }),
4029
4053
  { noProof: baseStyle.noProof, noProofWords: options.noProofWords }
4030
4054
  );
4055
+ if (!options.noteRef) return runs(text);
4056
+ return splitNoteMarkers(text, options.noteRef, runs);
4057
+ }
4058
+ function splitNoteMarkers(text, noteRef, runs) {
4059
+ const regex = new RegExp(FOOTNOTE_MARKER_REGEX.source, "g");
4060
+ const out = [];
4061
+ let lastIndex = 0;
4062
+ let pending = "";
4063
+ let match;
4064
+ while ((match = regex.exec(text)) !== null) {
4065
+ const note = noteRef(match[1]);
4066
+ if (note === void 0) {
4067
+ pending += text.slice(lastIndex, regex.lastIndex);
4068
+ lastIndex = regex.lastIndex;
4069
+ continue;
4070
+ }
4071
+ const before = pending + text.slice(lastIndex, match.index);
4072
+ pending = "";
4073
+ if (before) out.push(...runs(before));
4074
+ out.push(
4075
+ note.endnote ? new EndnoteReferenceRun(note.id) : new FootnoteReferenceRun(note.id)
4076
+ );
4077
+ lastIndex = regex.lastIndex;
4078
+ }
4079
+ if (out.length === 0) return runs(text);
4080
+ const trailing = pending + text.slice(lastIndex);
4081
+ if (trailing) out.push(...runs(trailing));
4082
+ return out;
4031
4083
  }
4032
4084
  function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
4033
4085
  const normalizedText = normalizeUnicodeText(text);
@@ -4283,6 +4335,52 @@ initializeBuiltinPlaceholders();
4283
4335
 
4284
4336
  // src/utils/revisionUtils.ts
4285
4337
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
4338
+
4339
+ // src/utils/componentAnnotations.ts
4340
+ function isRecord(value) {
4341
+ return typeof value === "object" && value !== null;
4342
+ }
4343
+ function hasKey(value, key) {
4344
+ return isRecord(value) && Boolean(value[key]);
4345
+ }
4346
+ function cellHasAnnotation(cell, key) {
4347
+ if (!isRecord(cell)) return false;
4348
+ if (hasKey(cell, key)) return true;
4349
+ const content = cell.content;
4350
+ return isRecord(content) && componentHasAnnotation(content, key);
4351
+ }
4352
+ function componentHasAnnotation(component, key) {
4353
+ const props = component.props;
4354
+ if (props) {
4355
+ if (props[key]) return true;
4356
+ const items = props.items;
4357
+ if (Array.isArray(items) && items.some((item) => hasKey(item, key))) {
4358
+ return true;
4359
+ }
4360
+ const rows = props.rows;
4361
+ if (Array.isArray(rows) && rows.some((row) => hasKey(row, key))) {
4362
+ return true;
4363
+ }
4364
+ const columns = props.columns;
4365
+ if (Array.isArray(columns) && columns.some((column) => {
4366
+ if (!isRecord(column)) return false;
4367
+ if (cellHasAnnotation(column.header, key)) return true;
4368
+ const cells = column.cells;
4369
+ return Array.isArray(cells) && cells.some((cell) => cellHasAnnotation(cell, key));
4370
+ })) {
4371
+ return true;
4372
+ }
4373
+ }
4374
+ const children = component.children;
4375
+ if (Array.isArray(children)) {
4376
+ return children.some(
4377
+ (child) => isRecord(child) && componentHasAnnotation(child, key)
4378
+ );
4379
+ }
4380
+ return false;
4381
+ }
4382
+
4383
+ // src/utils/revisionUtils.ts
4286
4384
  var DEFAULT_REVISION_AUTHOR = "json-to-office";
4287
4385
  var DEFAULT_REVISION_DATE = "1970-01-01T00:00:00Z";
4288
4386
  var RevisionIdRegistry = class {
@@ -4367,25 +4465,25 @@ function createRevisionRuns(revision, baseStyle) {
4367
4465
  return runs;
4368
4466
  }
4369
4467
  function componentHasRevision(component) {
4370
- const props = component.props;
4371
- if (props) {
4372
- if (props.revision) return true;
4373
- const items = props.items;
4374
- if (Array.isArray(items)) {
4375
- if (items.some(
4376
- (item) => typeof item === "object" && item !== null && "revision" in item && item.revision
4377
- )) {
4378
- return true;
4379
- }
4380
- }
4381
- }
4382
- const children = component.children;
4383
- if (Array.isArray(children)) {
4384
- return children.some(
4385
- (child) => typeof child === "object" && child !== null && componentHasRevision(child)
4386
- );
4387
- }
4388
- return false;
4468
+ return componentHasAnnotation(component, "revision");
4469
+ }
4470
+ function createRevisionMark(mark) {
4471
+ const attributes = {
4472
+ id: globalRevisionIdRegistry.next(),
4473
+ author: mark.author || DEFAULT_REVISION_AUTHOR,
4474
+ date: mark.date || DEFAULT_REVISION_DATE
4475
+ };
4476
+ return mark.type === "insert" ? { insertion: attributes } : { deletion: attributes };
4477
+ }
4478
+ function createMarkedTextRuns(text, mark, baseStyle) {
4479
+ return createRevisionRuns(
4480
+ {
4481
+ author: mark.author,
4482
+ date: mark.date,
4483
+ segments: [{ type: mark.type, text }]
4484
+ },
4485
+ baseStyle
4486
+ );
4389
4487
  }
4390
4488
 
4391
4489
  // src/core/cached-render.ts
@@ -4446,14 +4544,23 @@ function initializeComponentCache(cache) {
4446
4544
  cacheKeyGen = new CacheKeyGenerator();
4447
4545
  }
4448
4546
  }
4547
+ var DYNAMIC_CONTEXT_COMPONENTS = /* @__PURE__ */ new Set([
4548
+ "toc",
4549
+ "section",
4550
+ "visual",
4551
+ "heading",
4552
+ "list",
4553
+ "paragraph"
4554
+ ]);
4555
+ function componentBypassReason(component) {
4556
+ if (componentHasRevision(component)) return "revision-ids";
4557
+ if (componentHasAnnotation(component, "comment")) return "comment-ids";
4558
+ if ("id" in component) return "bookmark-id";
4559
+ if (DYNAMIC_CONTEXT_COMPONENTS.has(component.name)) return "dynamic-context";
4560
+ return null;
4561
+ }
4449
4562
  async function renderComponentWithCache(component, theme, themeName, context, bypassCache = false) {
4450
- const bypassReason = componentHasRevision(
4451
- component
4452
- ) ? "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
4453
- // numbering in the current document scope. Cached paragraphs can
4454
- // otherwise reference definitions that only existed in a previous
4455
- // render.
4456
- component.name === "list" || component.name === "paragraph" ? "dynamic-context" : null;
4563
+ const bypassReason = componentBypassReason(component);
4457
4564
  if (!componentCache) {
4458
4565
  initializeComponentCache();
4459
4566
  }
@@ -4499,8 +4606,8 @@ async function renderComponentWithCache(component, theme, themeName, context, by
4499
4606
 
4500
4607
  // src/core/content.ts
4501
4608
  import {
4502
- Paragraph,
4503
- TextRun as TextRun4,
4609
+ Paragraph as Paragraph3,
4610
+ TextRun as TextRun7,
4504
4611
  Table,
4505
4612
  TableRow,
4506
4613
  TableCell,
@@ -4595,6 +4702,252 @@ var BookmarkRegistry = class {
4595
4702
  };
4596
4703
  var globalBookmarkRegistry = new BookmarkRegistry();
4597
4704
 
4705
+ // src/utils/commentAnchors.ts
4706
+ import {
4707
+ CommentRangeEnd,
4708
+ CommentRangeStart,
4709
+ CommentReference,
4710
+ TextRun as TextRun5
4711
+ } from "docx";
4712
+
4713
+ // src/utils/commentRegistry.ts
4714
+ import { Paragraph, TextRun as TextRun4 } from "docx";
4715
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
4716
+ var DEFAULT_COMMENT_AUTHOR = "json-to-office";
4717
+ var DEFAULT_COMMENT_DATE = "1970-01-01T00:00:00Z";
4718
+ function createState() {
4719
+ return { counter: 0, comments: [], hasResolved: false };
4720
+ }
4721
+ function deriveInitials(author) {
4722
+ const initials = author.split(/[\s._-]+/).filter(Boolean).map((word) => word[0]).join("").toUpperCase().slice(0, 3);
4723
+ return initials || author.slice(0, 2).toUpperCase();
4724
+ }
4725
+ function bodyParagraphs(text) {
4726
+ return normalizeUnicodeText(text).split("\n").map((line) => new Paragraph({ children: [new TextRun4({ text: line })] }));
4727
+ }
4728
+ var CommentRegistry = class {
4729
+ fallback = createState();
4730
+ scopes = new AsyncLocalStorage4();
4731
+ get state() {
4732
+ return this.scopes.getStore() ?? this.fallback;
4733
+ }
4734
+ /** Run work with an isolated registry that follows its async call chain. */
4735
+ runScoped(callback) {
4736
+ return this.scopes.run(createState(), callback);
4737
+ }
4738
+ /**
4739
+ * Register a comment thread and return every id its anchors must carry — the
4740
+ * root first, then each reply in order. Word anchors every comment in a
4741
+ * thread over the same range, so all of them need a range and a reference.
4742
+ *
4743
+ * Ids are unique and monotonic within a render. `parentId` is derived here
4744
+ * rather than authored; docx turns it into the `w15:paraIdParent` links in
4745
+ * `word/commentsExtended.xml`.
4746
+ */
4747
+ register(comment) {
4748
+ const state = this.state;
4749
+ const resolved = comment.resolved;
4750
+ const rootId = ++state.counter;
4751
+ state.comments.push({
4752
+ ...this.toOptions(comment, rootId),
4753
+ ...resolved !== void 0 && { resolved }
4754
+ });
4755
+ const ids = [rootId];
4756
+ for (const reply of comment.replies ?? []) {
4757
+ const replyId = ++state.counter;
4758
+ state.comments.push({
4759
+ ...this.toOptions(reply, replyId),
4760
+ parentId: rootId,
4761
+ // Word resolves a thread as a whole, so the flag rides every member.
4762
+ ...resolved !== void 0 && { resolved }
4763
+ });
4764
+ ids.push(replyId);
4765
+ }
4766
+ if (resolved !== void 0) state.hasResolved = true;
4767
+ return ids;
4768
+ }
4769
+ toOptions(comment, id) {
4770
+ const author = comment.author || DEFAULT_COMMENT_AUTHOR;
4771
+ return {
4772
+ id,
4773
+ author,
4774
+ initials: comment.initials || deriveInitials(author),
4775
+ date: new Date(comment.date || DEFAULT_COMMENT_DATE),
4776
+ children: bodyParagraphs(comment.text)
4777
+ };
4778
+ }
4779
+ /**
4780
+ * Every comment registered in this scope, in id order.
4781
+ *
4782
+ * docx writes `word/commentsExtended.xml` — and therefore any `w15:done` —
4783
+ * only when at least one comment in the document carries a `parentId`. A
4784
+ * document whose only resolved comment has no replies would silently lose
4785
+ * that state, so say so rather than dropping it quietly.
4786
+ */
4787
+ getAll() {
4788
+ const state = this.state;
4789
+ if (state.hasResolved && !state.comments.some((comment) => comment.parentId !== void 0)) {
4790
+ console.warn(
4791
+ "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."
4792
+ );
4793
+ }
4794
+ return [...state.comments];
4795
+ }
4796
+ /** Test-only: reset the current scope's counter and bodies. */
4797
+ clear() {
4798
+ const state = this.scopes.getStore();
4799
+ if (state) {
4800
+ state.counter = 0;
4801
+ state.comments.length = 0;
4802
+ state.hasResolved = false;
4803
+ } else {
4804
+ this.fallback = createState();
4805
+ }
4806
+ }
4807
+ };
4808
+ var globalCommentRegistry = new CommentRegistry();
4809
+
4810
+ // src/utils/commentAnchors.ts
4811
+ function openCommentRange(comment) {
4812
+ if (!comment) return void 0;
4813
+ const ids = globalCommentRegistry.register(comment);
4814
+ return { ids, start: ids.map((id) => new CommentRangeStart(id)) };
4815
+ }
4816
+ function closeCommentRange(ids) {
4817
+ return ids.flatMap((id) => [
4818
+ new CommentRangeEnd(id),
4819
+ new TextRun5({ children: [new CommentReference(id)] })
4820
+ ]);
4821
+ }
4822
+
4823
+ // src/utils/noteRegistry.ts
4824
+ import { Paragraph as Paragraph2, TextRun as TextRun6 } from "docx";
4825
+ import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4826
+ function createState2() {
4827
+ return {
4828
+ footnoteCounter: 0,
4829
+ endnoteCounter: 0,
4830
+ footnotes: {},
4831
+ endnotes: {}
4832
+ };
4833
+ }
4834
+ function bodyParagraphs2(text, style) {
4835
+ return normalizeUnicodeText(text).split("\n").map(
4836
+ (line) => new Paragraph2({
4837
+ style,
4838
+ children: [new TextRun6({ text: line })]
4839
+ })
4840
+ );
4841
+ }
4842
+ var NoteRegistry = class {
4843
+ fallback = createState2();
4844
+ scopes = new AsyncLocalStorage5();
4845
+ get state() {
4846
+ return this.scopes.getStore() ?? this.fallback;
4847
+ }
4848
+ /** Run work with an isolated registry that follows its async call chain. */
4849
+ runScoped(callback) {
4850
+ return this.scopes.run(createState2(), callback);
4851
+ }
4852
+ /**
4853
+ * Register a footnote body and return the id its reference must use. Ids are
4854
+ * unique and monotonic within a render.
4855
+ */
4856
+ registerFootnote(text) {
4857
+ const state = this.state;
4858
+ const id = ++state.footnoteCounter;
4859
+ state.footnotes[String(id)] = {
4860
+ children: bodyParagraphs2(text, "FootnoteText")
4861
+ };
4862
+ return id;
4863
+ }
4864
+ /** Register an endnote body and return the id its reference must use. */
4865
+ registerEndnote(text) {
4866
+ const state = this.state;
4867
+ const id = ++state.endnoteCounter;
4868
+ state.endnotes[String(id)] = {
4869
+ children: bodyParagraphs2(text, "EndnoteText")
4870
+ };
4871
+ return id;
4872
+ }
4873
+ /** Every footnote registered in this scope, keyed by id. */
4874
+ getFootnotes() {
4875
+ return { ...this.state.footnotes };
4876
+ }
4877
+ /** Every endnote registered in this scope, keyed by id. */
4878
+ getEndnotes() {
4879
+ return { ...this.state.endnotes };
4880
+ }
4881
+ /** Test-only: reset the current scope's counters and bodies. */
4882
+ clear() {
4883
+ const state = this.scopes.getStore();
4884
+ if (state) {
4885
+ state.footnoteCounter = 0;
4886
+ state.endnoteCounter = 0;
4887
+ for (const key of Object.keys(state.footnotes)) {
4888
+ delete state.footnotes[key];
4889
+ }
4890
+ for (const key of Object.keys(state.endnotes)) {
4891
+ delete state.endnotes[key];
4892
+ }
4893
+ } else {
4894
+ this.fallback = createState2();
4895
+ }
4896
+ }
4897
+ };
4898
+ var globalNoteRegistry = new NoteRegistry();
4899
+
4900
+ // src/utils/noteResolver.ts
4901
+ function collectDeclared(footnotes, endnotes) {
4902
+ const declared = /* @__PURE__ */ new Map();
4903
+ const declare = (note, endnote) => {
4904
+ const existing = declared.get(note.id);
4905
+ if (existing) {
4906
+ console.warn(
4907
+ 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.`
4908
+ );
4909
+ return;
4910
+ }
4911
+ declared.set(note.id, { text: note.text, endnote });
4912
+ };
4913
+ for (const note of footnotes ?? []) declare(note, false);
4914
+ for (const note of endnotes ?? []) declare(note, true);
4915
+ return declared;
4916
+ }
4917
+ function createNoteResolver(footnotes, endnotes) {
4918
+ const declared = collectDeclared(footnotes, endnotes);
4919
+ if (declared.size === 0) return void 0;
4920
+ const registered = /* @__PURE__ */ new Map();
4921
+ return {
4922
+ resolve(id) {
4923
+ const existing = registered.get(id);
4924
+ if (existing !== void 0) return existing;
4925
+ const note = declared.get(id);
4926
+ if (note === void 0) {
4927
+ console.warn(
4928
+ `Note marker "[^${id}]" has no matching entry in this paragraph's footnotes or endnotes (declared: ${[...declared.keys()].join(", ")}). Rendering the marker as literal text.`
4929
+ );
4930
+ return void 0;
4931
+ }
4932
+ const resolved = {
4933
+ id: note.endnote ? globalNoteRegistry.registerEndnote(note.text) : globalNoteRegistry.registerFootnote(note.text),
4934
+ endnote: note.endnote
4935
+ };
4936
+ registered.set(id, resolved);
4937
+ return resolved;
4938
+ },
4939
+ reportUnemitted(text) {
4940
+ for (const [id, note] of declared) {
4941
+ if (registered.has(id)) continue;
4942
+ const kind = note.endnote ? "Endnote" : "Footnote";
4943
+ console.warn(
4944
+ 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.`
4945
+ );
4946
+ }
4947
+ }
4948
+ };
4949
+ }
4950
+
4598
4951
  // src/core/content.ts
4599
4952
  init_styleHelpers();
4600
4953
  import { synthesizeFamilyName } from "@json-to-office/shared";
@@ -4618,6 +4971,20 @@ function resolveNoProofWords(theme, optionWords) {
4618
4971
  const merged = [...themeWords || [], ...optionWords || []];
4619
4972
  return merged.length > 0 ? Array.from(new Set(merged)) : void 0;
4620
4973
  }
4974
+ function reportNotesUnsupportedInRevision(footnotes, endnotes, revision) {
4975
+ const declared = [...footnotes ?? [], ...endnotes ?? []];
4976
+ if (declared.length === 0) return;
4977
+ const text = revision.segments.map((segment) => segment.text).join("");
4978
+ const referenced = declared.filter((note) => text.includes(`[^${note.id}]`));
4979
+ console.warn(
4980
+ `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."
4981
+ );
4982
+ }
4983
+ function wrapInComment(children, comment) {
4984
+ const anchor = openCommentRange(comment);
4985
+ if (!anchor) return children;
4986
+ return [...anchor.start, ...children, ...closeCommentRange(anchor.ids)];
4987
+ }
4621
4988
  function createText(content, theme, themeName, options = {}) {
4622
4989
  const normalizedContent = normalizeUnicodeText(content);
4623
4990
  const style = options.style || "Normal";
@@ -4637,6 +5004,10 @@ function createText(content, theme, themeName, options = {}) {
4637
5004
  if (options.columnBreak) {
4638
5005
  children.push(new ColumnBreak());
4639
5006
  }
5007
+ const commentAnchor = openCommentRange(options.comment);
5008
+ if (commentAnchor) {
5009
+ children.push(...commentAnchor.start);
5010
+ }
4640
5011
  const hasWeightRequest = options.fontWeight != null || options.bold === true;
4641
5012
  const effectiveFamily = options.fontFamily ?? (hasWeightRequest ? resolveFontFamily(theme, "body") : void 0);
4642
5013
  const weighted = applyFontWeightAlias({
@@ -4673,6 +5044,11 @@ function createText(content, theme, themeName, options = {}) {
4673
5044
  ...options.noProof !== void 0 && { noProof: options.noProof }
4674
5045
  };
4675
5046
  if (options.revision) {
5047
+ reportNotesUnsupportedInRevision(
5048
+ options.footnotes,
5049
+ options.endnotes,
5050
+ options.revision
5051
+ );
4676
5052
  const revisionRuns = createRevisionRuns(options.revision, baseTextStyle);
4677
5053
  if (options.bookmarkId) {
4678
5054
  globalBookmarkRegistry.register(
@@ -4690,11 +5066,17 @@ function createText(content, theme, themeName, options = {}) {
4690
5066
  children.push(...revisionRuns);
4691
5067
  }
4692
5068
  } else {
5069
+ const noteResolver = createNoteResolver(
5070
+ options.footnotes,
5071
+ options.endnotes
5072
+ );
4693
5073
  const textRuns = parseTextWithDecorators(normalizedContent, baseTextStyle, {
4694
5074
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
4695
5075
  enableHyperlinks: true,
4696
- noProofWords: resolveNoProofWords(theme, options.noProofWords)
5076
+ noProofWords: resolveNoProofWords(theme, options.noProofWords),
5077
+ noteRef: noteResolver?.resolve
4697
5078
  });
5079
+ noteResolver?.reportUnemitted(normalizedContent);
4698
5080
  if (options.bookmarkId) {
4699
5081
  globalBookmarkRegistry.register(
4700
5082
  options.bookmarkId,
@@ -4711,9 +5093,12 @@ function createText(content, theme, themeName, options = {}) {
4711
5093
  children.push(...textRuns);
4712
5094
  }
4713
5095
  }
5096
+ if (commentAnchor) {
5097
+ children.push(...closeCommentRange(commentAnchor.ids));
5098
+ }
4714
5099
  const isFloating = !!options.floating;
4715
5100
  const frameOptions = isFloating && options.floating ? mapFrameOptions(options.floating, theme, themeName) : void 0;
4716
- return new Paragraph({
5101
+ return new Paragraph3({
4717
5102
  children,
4718
5103
  style,
4719
5104
  alignment: options.alignment ? getAlignment(options.alignment) : void 0,
@@ -4808,6 +5193,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4808
5193
  if (options.columnBreak) {
4809
5194
  children.push(new ColumnBreak());
4810
5195
  }
5196
+ const commentAnchor = openCommentRange(options.comment);
5197
+ if (commentAnchor) {
5198
+ children.push(...commentAnchor.start);
5199
+ }
4811
5200
  const hasDecorators = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText);
4812
5201
  const headingHasWeightRequest = options.fontWeight != null || options.bold === true;
4813
5202
  const headingEffectiveFamily = options.fontFamily ?? (headingHasWeightRequest ? resolveFontFamily(theme, "heading") : void 0);
@@ -4844,7 +5233,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4844
5233
  const headingNoProofWords = resolveNoProofWords(theme, options.noProofWords);
4845
5234
  const makeHeadingRuns = (value) => splitByNoProofWords(
4846
5235
  value,
4847
- (segment, matched) => new TextRun4({
5236
+ (segment, matched) => new TextRun7({
4848
5237
  text: segment,
4849
5238
  ...baseTextStyle,
4850
5239
  ...matched && { noProof: true }
@@ -4903,7 +5292,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4903
5292
  children.push(...makeHeadingRuns(normalizedText));
4904
5293
  }
4905
5294
  }
4906
- return new Paragraph({
5295
+ if (commentAnchor) {
5296
+ children.push(...closeCommentRange(commentAnchor.ids));
5297
+ }
5298
+ return new Paragraph3({
4907
5299
  children,
4908
5300
  style: styleId,
4909
5301
  alignment: getAlignment(options.alignment || "left"),
@@ -4970,7 +5362,7 @@ async function createImage(path3, theme, themeName, options = {}) {
4970
5362
  spacing.after = pointsToTwips(options.spacing.after);
4971
5363
  }
4972
5364
  elements.push(
4973
- new Paragraph({
5365
+ new Paragraph3({
4974
5366
  children: [imageRun],
4975
5367
  alignment,
4976
5368
  ...Object.keys(spacing).length > 0 && { spacing },
@@ -4987,7 +5379,7 @@ async function createImage(path3, theme, themeName, options = {}) {
4987
5379
  const hasDecorators = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(options.caption);
4988
5380
  if (!hasDecorators) {
4989
5381
  elements.push(
4990
- new Paragraph({
5382
+ new Paragraph3({
4991
5383
  text: normalizeUnicodeText(options.caption),
4992
5384
  style: "Normal",
4993
5385
  alignment: AlignmentType2.LEFT
@@ -5003,7 +5395,7 @@ async function createImage(path3, theme, themeName, options = {}) {
5003
5395
  }
5004
5396
  );
5005
5397
  elements.push(
5006
- new Paragraph({
5398
+ new Paragraph3({
5007
5399
  children: textRuns,
5008
5400
  style: "Normal",
5009
5401
  // Use Normal style for consistent font inheritance
@@ -5020,13 +5412,13 @@ function createStatistic(data, options = {}) {
5020
5412
  const normalizedNumber = normalizeUnicodeText(data.number);
5021
5413
  const normalizedDescription = normalizeUnicodeText(data.description);
5022
5414
  return [
5023
- new Paragraph({
5415
+ new Paragraph3({
5024
5416
  text: normalizedNumber,
5025
5417
  style: "StatisticNumber",
5026
5418
  alignment,
5027
5419
  spacing: options.spacing
5028
5420
  }),
5029
- new Paragraph({
5421
+ new Paragraph3({
5030
5422
  text: normalizedDescription,
5031
5423
  style: "StatisticDescription",
5032
5424
  alignment
@@ -5038,6 +5430,15 @@ function createList(items, _theme, _themeName, options = {}) {
5038
5430
  return [];
5039
5431
  }
5040
5432
  const paragraphs = [];
5433
+ const noteResolver = createNoteResolver(options.footnotes, options.endnotes);
5434
+ const rendersAt = items.map((item) => {
5435
+ const text = typeof item === "string" ? item : item.text;
5436
+ const revision = typeof item === "object" ? item.revision : void 0;
5437
+ return Boolean(text.trim()) || Boolean(revision);
5438
+ });
5439
+ const firstRendered = rendersAt.indexOf(true);
5440
+ const lastRendered = rendersAt.lastIndexOf(true);
5441
+ const commentAnchor = firstRendered === -1 ? void 0 : openCommentRange(options.comment);
5041
5442
  items.forEach((item, index) => {
5042
5443
  const itemText = typeof item === "string" ? item : item.text;
5043
5444
  const itemLevel = typeof item === "object" ? item.level || 0 : 0;
@@ -5049,7 +5450,8 @@ function createList(items, _theme, _themeName, options = {}) {
5049
5450
  itemText,
5050
5451
  {},
5051
5452
  {
5052
- enableHyperlinks: true
5453
+ enableHyperlinks: true,
5454
+ noteRef: noteResolver?.resolve
5053
5455
  }
5054
5456
  );
5055
5457
  const spacing = {};
@@ -5061,10 +5463,15 @@ function createList(items, _theme, _themeName, options = {}) {
5061
5463
  } else if (options.spacing?.item) {
5062
5464
  spacing.after = pointsToTwips(options.spacing.item);
5063
5465
  }
5064
- const paragraph = new Paragraph({
5466
+ const paragraphChildren = [
5467
+ ...commentAnchor && index === firstRendered ? commentAnchor.start : [],
5468
+ ...textRuns,
5469
+ ...commentAnchor && index === lastRendered ? closeCommentRange(commentAnchor.ids) : []
5470
+ ];
5471
+ const paragraph = new Paragraph3({
5065
5472
  style: "Normal",
5066
5473
  // Apply Normal style for font inheritance
5067
- children: textRuns,
5474
+ children: paragraphChildren,
5068
5475
  alignment: options.alignment ? getAlignment(options.alignment) : AlignmentType2.LEFT,
5069
5476
  spacing,
5070
5477
  // Use proper docx numbering instead of prepending text
@@ -5077,6 +5484,9 @@ function createList(items, _theme, _themeName, options = {}) {
5077
5484
  });
5078
5485
  paragraphs.push(paragraph);
5079
5486
  });
5487
+ noteResolver?.reportUnemitted(
5488
+ items.map((item) => typeof item === "string" ? item : item.text).join("\n")
5489
+ );
5080
5490
  return paragraphs;
5081
5491
  }
5082
5492
  async function createTable(columns, tableConfig, theme, themeName, _options = {}) {
@@ -5410,10 +5820,10 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5410
5820
  return false;
5411
5821
  }
5412
5822
  };
5413
- const processCellContent = async (cell, cellDefaults, baseCellStyle) => {
5823
+ const processCellContent = async (cell, cellDefaults, baseCellStyle, comment, revision, rowMark) => {
5414
5824
  let cellChildren = [];
5415
5825
  if (!cell) {
5416
- return cellChildren;
5826
+ return wrapInComment(cellChildren, comment);
5417
5827
  }
5418
5828
  const cellWeighted = applyFontWeightAlias({
5419
5829
  fontFamily: cellDefaults.font?.family || baseCellStyle.font,
@@ -5457,11 +5867,10 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5457
5867
  color: resolveColor(paragraphFont.color, theme)
5458
5868
  }
5459
5869
  };
5460
- cellChildren = parseTextWithDecorators(
5461
- textComp.props.text,
5462
- paragraphStyle,
5463
- { enableHyperlinks: true }
5464
- );
5870
+ const paragraphRevision = revision ?? textComp.props.revision;
5871
+ cellChildren = paragraphRevision ? createRevisionRuns(paragraphRevision, paragraphStyle) : rowMark ? createMarkedTextRuns(textComp.props.text, rowMark, paragraphStyle) : parseTextWithDecorators(textComp.props.text, paragraphStyle, {
5872
+ enableHyperlinks: true
5873
+ });
5465
5874
  } else if (isImageComponent(cell)) {
5466
5875
  const imageComp = cell;
5467
5876
  try {
@@ -5496,7 +5905,7 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5496
5905
  } catch (error) {
5497
5906
  const imageSource = imageComp.props.svg?.trim() ? "inline-svg" : imageComp.props.base64 || imageComp.props.path || "unknown";
5498
5907
  cellChildren = [
5499
- new TextRun4({
5908
+ new TextRun7({
5500
5909
  text: `[IMAGE: ${imageSource.substring(0, 50)}${imageSource.length > 50 ? "..." : ""}]`,
5501
5910
  font: mergedStyle.font,
5502
5911
  size: mergedStyle.size,
@@ -5506,7 +5915,7 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5506
5915
  }
5507
5916
  } else {
5508
5917
  cellChildren = [
5509
- new TextRun4({
5918
+ new TextRun7({
5510
5919
  text: `[Unsupported component type: ${cell.name}]`,
5511
5920
  font: mergedStyle.font,
5512
5921
  size: mergedStyle.size,
@@ -5515,11 +5924,11 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5515
5924
  ];
5516
5925
  }
5517
5926
  } else {
5518
- cellChildren = parseTextWithDecorators(cell, mergedStyle, {
5927
+ cellChildren = revision ? createRevisionRuns(revision, mergedStyle) : rowMark ? createMarkedTextRuns(cell, rowMark, mergedStyle) : parseTextWithDecorators(cell, mergedStyle, {
5519
5928
  enableHyperlinks: true
5520
5929
  });
5521
5930
  }
5522
- return cellChildren;
5931
+ return wrapInComment(cellChildren, comment);
5523
5932
  };
5524
5933
  const numColumns = columns.length;
5525
5934
  const headerHeight = columns.reduce(
@@ -5576,7 +5985,9 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5576
5985
  const cellChildren = await processCellContent(
5577
5986
  headerCell?.content,
5578
5987
  mergedDefaults,
5579
- tableStyle.tableHeader
5988
+ tableStyle.tableHeader,
5989
+ headerCell?.comment,
5990
+ headerCell?.revision
5580
5991
  );
5581
5992
  const horizontalAlignment = mergedDefaults.horizontalAlignment;
5582
5993
  const verticalAlignment = getVerticalAlignment(
@@ -5584,7 +5995,7 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5584
5995
  );
5585
5996
  return new TableCell({
5586
5997
  children: [
5587
- new Paragraph({
5998
+ new Paragraph3({
5588
5999
  ...tableConfig.keepInOnePage && { keepNext: true },
5589
6000
  spacing: tableStyle.headerParagraph,
5590
6001
  alignment: getAlignment(horizontalAlignment),
@@ -5627,6 +6038,10 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5627
6038
  const dataRows = await Promise.all(
5628
6039
  Array.from({ length: numRows }, async (_, rowIndex) => {
5629
6040
  const isLastRow = rowIndex === numRows - 1;
6041
+ const rowProps = tableConfig.rows?.[rowIndex];
6042
+ const rowRevision = rowProps?.revision;
6043
+ const rowRevisionAttributes = rowRevision ? createRevisionMark(rowRevision) : void 0;
6044
+ const paragraphMarks = rowRevision ? columns.map(() => ({ run: createRevisionMark(rowRevision) })) : void 0;
5630
6045
  const rowHeight = columns.reduce(
5631
6046
  (maxHeight, column, colIndex) => {
5632
6047
  const cell = column.cells?.[rowIndex];
@@ -5656,6 +6071,14 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5656
6071
  );
5657
6072
  return new TableRow({
5658
6073
  height: rowHeight !== void 0 ? { value: rowHeight * 20, rule: "atLeast" } : void 0,
6074
+ ...rowProps?.cantSplit !== void 0 && {
6075
+ cantSplit: rowProps.cantSplit
6076
+ },
6077
+ ...rowProps?.tableHeader !== void 0 && {
6078
+ tableHeader: rowProps.tableHeader
6079
+ },
6080
+ // w:trPr/w:ins | w:del — the row itself was inserted or deleted.
6081
+ ...rowRevisionAttributes,
5659
6082
  children: await Promise.all(
5660
6083
  columns.map(async (column, colIndex) => {
5661
6084
  const cell = column.cells?.[rowIndex];
@@ -5679,10 +6102,11 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5679
6102
  );
5680
6103
  return new TableCell({
5681
6104
  children: [
5682
- new Paragraph({
6105
+ new Paragraph3({
5683
6106
  ...tableConfig.keepInOnePage && !isLastRow || isLastRow && tableConfig.keepNext ? { keepNext: true } : {},
5684
6107
  spacing: tableStyle.cellParagraph,
5685
6108
  alignment: AlignmentType2.LEFT,
6109
+ ...paragraphMarks?.[colIndex],
5686
6110
  children: []
5687
6111
  })
5688
6112
  ],
@@ -5747,7 +6171,10 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5747
6171
  const cellChildren = await processCellContent(
5748
6172
  cell.content,
5749
6173
  mergedDefaults,
5750
- tableStyle.tableCell
6174
+ tableStyle.tableCell,
6175
+ cell.comment,
6176
+ cell.revision,
6177
+ rowRevision
5751
6178
  );
5752
6179
  const horizontalAlignment = mergedDefaults.horizontalAlignment;
5753
6180
  const verticalAlignment = getVerticalAlignment(
@@ -5755,10 +6182,11 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5755
6182
  );
5756
6183
  return new TableCell({
5757
6184
  children: [
5758
- new Paragraph({
6185
+ new Paragraph3({
5759
6186
  ...tableConfig.keepInOnePage && !isLastRow || isLastRow && tableConfig.keepNext ? { keepNext: true } : {},
5760
6187
  spacing: tableStyle.cellParagraph,
5761
6188
  alignment: getAlignment(horizontalAlignment),
6189
+ ...paragraphMarks?.[colIndex],
5762
6190
  children: cellChildren
5763
6191
  })
5764
6192
  ],
@@ -5917,7 +6345,9 @@ function renderHeadingComponent(component, theme, themeName) {
5917
6345
  // Bookmark ID for internal linking
5918
6346
  bookmarkId,
5919
6347
  // Tracked-change segments (rendered as native Word revisions)
5920
- revision: config.revision
6348
+ revision: config.revision,
6349
+ // Review comment anchored to this heading's text
6350
+ comment: config.comment
5921
6351
  }
5922
6352
  );
5923
6353
  return [header];
@@ -5925,7 +6355,7 @@ function renderHeadingComponent(component, theme, themeName) {
5925
6355
 
5926
6356
  // src/utils/numberingConfig.ts
5927
6357
  import { AlignmentType as AlignmentType3, convertInchesToTwip, LevelFormat } from "docx";
5928
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
6358
+ import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
5929
6359
  var LEVEL_FORMAT_MAP = {
5930
6360
  decimal: LevelFormat.DECIMAL,
5931
6361
  upperRoman: LevelFormat.UPPER_ROMAN,
@@ -6003,6 +6433,21 @@ function getAlignment2(alignment) {
6003
6433
  if (!alignment) return AlignmentType3.LEFT;
6004
6434
  return ALIGNMENT_MAP[alignment] || AlignmentType3.LEFT;
6005
6435
  }
6436
+ function createMarkerRunStyle(font) {
6437
+ if (!font) return void 0;
6438
+ const run = {
6439
+ ...font.family && { font: font.family },
6440
+ // docx run sizes are half-points.
6441
+ ...font.size !== void 0 && { size: font.size * 2 },
6442
+ ...font.color && { color: font.color },
6443
+ ...font.bold !== void 0 && { bold: font.bold },
6444
+ ...font.italic !== void 0 && { italics: font.italic },
6445
+ ...font.underline !== void 0 && {
6446
+ underline: font.underline ? { type: "single" } : void 0
6447
+ }
6448
+ };
6449
+ return Object.keys(run).length > 0 ? run : void 0;
6450
+ }
6006
6451
  function createNumberingConfig(config) {
6007
6452
  const levels = [];
6008
6453
  for (const levelConfig of config.levels) {
@@ -6011,6 +6456,7 @@ function createNumberingConfig(config) {
6011
6456
  const text = levelConfig.text || (format2 === LevelFormat.BULLET ? "\u2022" : `%${levelConfig.level + 1}.`);
6012
6457
  const baseIndent = levelConfig.indent?.left !== void 0 ? levelConfig.indent.left / 72 : 0.5 * (levelConfig.level + 1);
6013
6458
  const hangingIndent = levelConfig.indent?.hanging !== void 0 ? levelConfig.indent.hanging / 72 : 0.25;
6459
+ const markerRun = createMarkerRunStyle(levelConfig.font);
6014
6460
  const level = {
6015
6461
  level: levelConfig.level,
6016
6462
  format: format2,
@@ -6022,7 +6468,10 @@ function createNumberingConfig(config) {
6022
6468
  left: convertInchesToTwip(baseIndent),
6023
6469
  hanging: convertInchesToTwip(hangingIndent)
6024
6470
  }
6025
- }
6471
+ },
6472
+ // The marker glyph carries its own run properties; without this it
6473
+ // inherits whatever the list paragraph resolves to.
6474
+ ...markerRun && { run: markerRun }
6026
6475
  },
6027
6476
  // Add start number if specified
6028
6477
  ...levelConfig.start !== void 0 && { start: levelConfig.start }
@@ -6039,7 +6488,7 @@ var NumberingRegistry = class {
6039
6488
  configs: /* @__PURE__ */ new Map(),
6040
6489
  counter: 0
6041
6490
  };
6042
- scopes = new AsyncLocalStorage4();
6491
+ scopes = new AsyncLocalStorage6();
6043
6492
  get state() {
6044
6493
  return this.scopes.getStore() ?? this.fallback;
6045
6494
  }
@@ -6151,7 +6600,10 @@ function renderParagraphComponent(component, theme, themeName) {
6151
6600
  return createList(listData.items, theme, themeName, {
6152
6601
  numberingReference: reference,
6153
6602
  spacing: resolvedConfig.spacing,
6154
- alignment: resolvedConfig.alignment
6603
+ alignment: resolvedConfig.alignment,
6604
+ comment: resolvedConfig.comment,
6605
+ footnotes: resolvedConfig.footnotes,
6606
+ endnotes: resolvedConfig.endnotes
6155
6607
  });
6156
6608
  }
6157
6609
  const styleFromTheme = (() => {
@@ -6215,12 +6667,18 @@ function renderParagraphComponent(component, theme, themeName) {
6215
6667
  // Pass bookmark ID for internal linking
6216
6668
  bookmarkId: resolvedConfig.id,
6217
6669
  // Tracked-change segments (rendered as native Word revisions)
6218
- revision: resolvedConfig.revision
6670
+ revision: resolvedConfig.revision,
6671
+ // Review comment anchored to this paragraph's text
6672
+ comment: resolvedConfig.comment,
6673
+ // Note bodies for the `[^id]` markers in this paragraph
6674
+ footnotes: resolvedConfig.footnotes,
6675
+ endnotes: resolvedConfig.endnotes
6219
6676
  });
6220
6677
  return [text];
6221
6678
  }
6222
6679
 
6223
6680
  // src/components/list.ts
6681
+ init_colorUtils();
6224
6682
  function createLevelsFromSimplifiedProps(props) {
6225
6683
  const levels = [];
6226
6684
  let format2;
@@ -6267,6 +6725,22 @@ function createLevelsFromSimplifiedProps(props) {
6267
6725
  }
6268
6726
  return levels;
6269
6727
  }
6728
+ function resolveMarkerFonts(levels, theme) {
6729
+ return levels.map((level) => {
6730
+ const font = level.font;
6731
+ if (!font?.color) return level;
6732
+ return {
6733
+ ...level,
6734
+ font: { ...font, color: resolveColor(font.color, theme) }
6735
+ };
6736
+ });
6737
+ }
6738
+ function applyListStart(levels, start) {
6739
+ if (start === void 0) return levels;
6740
+ return levels.map(
6741
+ (level) => level.level === 0 && level.start === void 0 ? { ...level, start } : level
6742
+ );
6743
+ }
6270
6744
  function getMaxLevelFromItems(items) {
6271
6745
  if (!items || !Array.isArray(items)) {
6272
6746
  return 0;
@@ -6329,7 +6803,10 @@ function renderListComponent(component, theme, themeName) {
6329
6803
  let levels;
6330
6804
  if (resolvedConfig.levels && resolvedConfig.levels.length > 0) {
6331
6805
  levels = fillMissingLevels(
6332
- resolvedConfig.levels,
6806
+ applyListStart(
6807
+ resolvedConfig.levels,
6808
+ resolvedConfig.start
6809
+ ),
6333
6810
  maxLevel
6334
6811
  );
6335
6812
  } else {
@@ -6338,7 +6815,7 @@ function renderListComponent(component, theme, themeName) {
6338
6815
  }
6339
6816
  const config = {
6340
6817
  reference,
6341
- levels
6818
+ levels: resolveMarkerFonts(levels, theme)
6342
6819
  };
6343
6820
  const numberingConfig = createNumberingConfig(config);
6344
6821
  globalNumberingRegistry.register(numberingConfig);
@@ -6346,7 +6823,9 @@ function renderListComponent(component, theme, themeName) {
6346
6823
  return createList(resolvedConfig.items, theme, themeName, {
6347
6824
  numberingReference: reference,
6348
6825
  spacing: resolvedConfig.spacing,
6349
- alignment: resolvedConfig.alignment
6826
+ alignment: resolvedConfig.alignment,
6827
+ // Review comment spanning the whole list
6828
+ comment: resolvedConfig.comment
6350
6829
  });
6351
6830
  }
6352
6831
 
@@ -6437,10 +6916,10 @@ function convertBorders2(bordersConfig, theme) {
6437
6916
  // src/styles/utils/cellUtils.ts
6438
6917
  init_styleHelpers();
6439
6918
  init_colorUtils();
6440
- import { Paragraph as Paragraph2 } from "docx";
6919
+ import { Paragraph as Paragraph4 } from "docx";
6441
6920
  function buildCellOptions(children, styleCfg, theme) {
6442
6921
  const cellOpts = {
6443
- children: children.length ? children : [new Paragraph2({})],
6922
+ children: children.length ? children : [new Paragraph4({})],
6444
6923
  margins: {
6445
6924
  top: styleCfg?.padding?.top ? pointsToTwips(styleCfg.padding.top) : 0,
6446
6925
  right: styleCfg?.padding?.right ? pointsToTwips(styleCfg.padding.right) : 0,
@@ -6643,25 +7122,66 @@ async function renderTableComponent(component, theme, themeName) {
6643
7122
  }
6644
7123
 
6645
7124
  // src/components/section.ts
6646
- import { Paragraph as Paragraph4, BookmarkStart, BookmarkEnd } from "docx";
6647
- function generateSectionBookmarkId(context) {
6648
- const custom = context.custom ??= {};
6649
- const state = custom.sectionBookmarks ??= {
6650
- next: 1
6651
- };
6652
- const ordinal = state.next++;
6653
- const linkId = 1e6 + ordinal;
6654
- return {
6655
- id: `_NestedSection_${ordinal}`,
6656
- linkId
6657
- };
6658
- }
7125
+ import { Paragraph as Paragraph6, BookmarkStart, BookmarkEnd } from "docx";
7126
+
7127
+ // src/core/sectionBookmarks.ts
7128
+ import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
7129
+ var NESTED_LINK_ID_BASE = 1e6;
7130
+ function createState3() {
7131
+ return { nextNested: 1, resolved: /* @__PURE__ */ new WeakMap() };
7132
+ }
7133
+ var SectionBookmarkRegistry = class {
7134
+ fallback = createState3();
7135
+ scopes = new AsyncLocalStorage7();
7136
+ get state() {
7137
+ return this.scopes.getStore() ?? this.fallback;
7138
+ }
7139
+ /** Run work with an isolated registry that follows its async call chain. */
7140
+ runScoped(callback) {
7141
+ return this.scopes.run(createState3(), callback);
7142
+ }
7143
+ /**
7144
+ * The bookmark for a layout section, derived from its ordinal.
7145
+ *
7146
+ * Every layout chunk of one user-defined section shares an ordinal (see
7147
+ * `computeSectionOrdinals`), so they all resolve to the same bookmark — which
7148
+ * is the point: the start lands in the first chunk and the end in the last.
7149
+ */
7150
+ forLayoutSection(ordinal) {
7151
+ return { id: `_Section_${ordinal}`, linkId: ordinal };
7152
+ }
7153
+ /**
7154
+ * The bookmark for a `section` component, allocated on first sight and
7155
+ * remembered so re-resolving the same component never allocates twice.
7156
+ */
7157
+ forSectionComponent(component) {
7158
+ const state = this.state;
7159
+ const existing = state.resolved.get(component);
7160
+ if (existing) return existing;
7161
+ const ordinal = state.nextNested++;
7162
+ const bookmark = {
7163
+ id: `_NestedSection_${ordinal}`,
7164
+ linkId: NESTED_LINK_ID_BASE + ordinal
7165
+ };
7166
+ state.resolved.set(component, bookmark);
7167
+ return bookmark;
7168
+ }
7169
+ /** Test-only: reset the unscoped fallback counters. */
7170
+ clear() {
7171
+ if (!this.scopes.getStore()) {
7172
+ this.fallback = createState3();
7173
+ }
7174
+ }
7175
+ };
7176
+ var globalSectionBookmarkRegistry = new SectionBookmarkRegistry();
7177
+
7178
+ // src/components/section.ts
6659
7179
  async function renderSectionComponent(component, theme, themeName, context) {
6660
7180
  if (!isSectionComponent(component)) return [];
6661
7181
  const elements = [];
6662
- const { id: sectionBookmarkId, linkId: bookmarkLinkId } = generateSectionBookmarkId(context);
7182
+ const { id: sectionBookmarkId, linkId: bookmarkLinkId } = globalSectionBookmarkRegistry.forSectionComponent(component);
6663
7183
  elements.push(
6664
- new Paragraph4({
7184
+ new Paragraph6({
6665
7185
  children: [new BookmarkStart(sectionBookmarkId, bookmarkLinkId)],
6666
7186
  spacing: {
6667
7187
  before: 0,
@@ -6689,7 +7209,7 @@ async function renderSectionComponent(component, theme, themeName, context) {
6689
7209
  }
6690
7210
  }
6691
7211
  elements.push(
6692
- new Paragraph4({
7212
+ new Paragraph6({
6693
7213
  children: [new BookmarkEnd(bookmarkLinkId)],
6694
7214
  spacing: {
6695
7215
  before: 0,
@@ -6703,7 +7223,7 @@ async function renderSectionComponent(component, theme, themeName, context) {
6703
7223
 
6704
7224
  // src/components/columns.ts
6705
7225
  import {
6706
- Paragraph as Paragraph5,
7226
+ Paragraph as Paragraph7,
6707
7227
  Table as Table5,
6708
7228
  TableRow as TableRow3,
6709
7229
  TableCell as TableCell3,
@@ -6795,7 +7315,7 @@ async function renderColumnsAsTable(component, theme, themeName, context) {
6795
7315
  columnElements.push(...rendered);
6796
7316
  }
6797
7317
  if (columnElements.length === 0) {
6798
- columnElements.push(new Paragraph5({}));
7318
+ columnElements.push(new Paragraph7({}));
6799
7319
  }
6800
7320
  cells.push(
6801
7321
  new TableCell3({
@@ -6846,12 +7366,34 @@ function renderStatisticComponent(component, _theme) {
6846
7366
 
6847
7367
  // src/components/toc/index.ts
6848
7368
  import {
6849
- Paragraph as Paragraph6,
7369
+ Paragraph as Paragraph8,
6850
7370
  TableOfContents,
6851
7371
  AlignmentType as AlignmentType4,
6852
- TextRun as TextRun5,
7372
+ TextRun as TextRun8,
6853
7373
  StyleLevel
6854
7374
  } from "docx";
7375
+ function toStyleDisplayName(styleId) {
7376
+ return styleId.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
7377
+ }
7378
+ function selectCachedEntries(collected, options) {
7379
+ if (!collected || collected.length === 0) return [];
7380
+ const { depthStart, depthEnd, sectionBookmarkId, styleLevels } = options;
7381
+ const entries = [];
7382
+ for (const entry of collected) {
7383
+ if (sectionBookmarkId && entry.sectionBookmarkId !== sectionBookmarkId) {
7384
+ continue;
7385
+ }
7386
+ if (entry.styleId !== void 0) {
7387
+ const level = styleLevels.get(entry.styleId) ?? styleLevels.get(toStyleDisplayName(entry.styleId));
7388
+ if (level === void 0) continue;
7389
+ entries.push({ title: entry.title, level });
7390
+ continue;
7391
+ }
7392
+ if (entry.level < depthStart || entry.level > depthEnd) continue;
7393
+ entries.push({ title: entry.title, level: entry.level });
7394
+ }
7395
+ return entries;
7396
+ }
6855
7397
  function parseDepthRange(rawDepth, fieldName, defaultFrom = 1, defaultTo = 3) {
6856
7398
  if (typeof rawDepth !== "object" || rawDepth === null) {
6857
7399
  throw new Error(
@@ -6907,9 +7449,9 @@ function renderTocComponent(component, theme, context) {
6907
7449
  const paragraphs = [];
6908
7450
  if (componentProps.title) {
6909
7451
  paragraphs.push(
6910
- new Paragraph6({
7452
+ new Paragraph8({
6911
7453
  children: [
6912
- new TextRun5({
7454
+ new TextRun8({
6913
7455
  text: componentProps.title,
6914
7456
  bold: true,
6915
7457
  size: 28
@@ -6927,11 +7469,14 @@ function renderTocComponent(component, theme, context) {
6927
7469
  );
6928
7470
  }
6929
7471
  const stylesWithLevels = [];
7472
+ const styleLevels = /* @__PURE__ */ new Map();
6930
7473
  if (componentProps.styles && componentProps.styles.length > 0) {
6931
7474
  for (const styleMapping of componentProps.styles) {
6932
7475
  const styleId = styleMapping.styleId;
6933
7476
  const isCustomStyle = !!theme.styles && Object.prototype.hasOwnProperty.call(theme.styles, styleId);
6934
- const styleDisplayName = isCustomStyle ? styleId.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim() : styleId;
7477
+ const styleDisplayName = isCustomStyle ? toStyleDisplayName(styleId) : styleId;
7478
+ styleLevels.set(styleId, styleMapping.level);
7479
+ styleLevels.set(styleDisplayName, styleMapping.level);
6935
7480
  stylesWithLevels.push(
6936
7481
  new StyleLevel(styleDisplayName, styleMapping.level)
6937
7482
  );
@@ -6939,6 +7484,12 @@ function renderTocComponent(component, theme, context) {
6939
7484
  }
6940
7485
  const effectiveDepthStart = depthStart;
6941
7486
  const effectiveDepthEnd = depthEnd;
7487
+ const cachedEntries = selectCachedEntries(context?.tocHeadings, {
7488
+ depthStart,
7489
+ depthEnd,
7490
+ sectionBookmarkId,
7491
+ styleLevels
7492
+ });
6942
7493
  const tocOptions = {
6943
7494
  hyperlink: true,
6944
7495
  // Enable clickable hyperlinks (\h switch)
@@ -6977,7 +7528,10 @@ function renderTocComponent(component, theme, context) {
6977
7528
  // default to tab
6978
7529
  };
6979
7530
  paragraphs.push(
6980
- new TableOfContents(componentProps.title ?? "Table of Contents", tocOptions)
7531
+ new TableOfContents(componentProps.title ?? "Table of Contents", {
7532
+ ...tocOptions,
7533
+ ...cachedEntries.length > 0 && { cachedEntries }
7534
+ })
6981
7535
  );
6982
7536
  return paragraphs;
6983
7537
  }
@@ -7504,6 +8058,78 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
7504
8058
  return map;
7505
8059
  }
7506
8060
 
8061
+ // src/core/sectionOrdinals.ts
8062
+ function computeSectionOrdinals(sections) {
8063
+ let counter = 0;
8064
+ return sections.map((section, index) => {
8065
+ if (!section.belongsToUserSection) {
8066
+ if (section.isUserSection) counter++;
8067
+ return { closeBookmark: false };
8068
+ }
8069
+ const ordinal = section.isUserSection ? counter + 1 : counter;
8070
+ if (section.isUserSection) counter++;
8071
+ const next = sections[index + 1];
8072
+ const closeBookmark = !next || !next.belongsToUserSection || next.isUserSection;
8073
+ return { ordinal, closeBookmark };
8074
+ });
8075
+ }
8076
+
8077
+ // src/core/collectTocHeadings.ts
8078
+ import { getStandardComponent } from "@json-to-office/shared-docx";
8079
+ function normalizeEntryTitle(text) {
8080
+ return normalizeUnicodeText(text).replace(/(\*\*\*|___)([\s\S]*?)\1/g, "$2").replace(/(\*\*|__)([\s\S]*?)\1/g, "$2").replace(/(\*|_)([\s\S]*?)\1/g, "$2").trim();
8081
+ }
8082
+ function isContainer(name) {
8083
+ return getStandardComponent(name)?.hasChildren === true;
8084
+ }
8085
+ function isEnabled(component) {
8086
+ return !("enabled" in component && component.enabled === false);
8087
+ }
8088
+ function styleEntryKey(props) {
8089
+ const themeStyle = props.themeStyle;
8090
+ if (typeof themeStyle !== "string" || !themeStyle) return void 0;
8091
+ if (/^heading[1-6]$/i.test(themeStyle)) return void 0;
8092
+ if (["normal", "title", "subtitle"].includes(themeStyle.toLowerCase())) {
8093
+ return void 0;
8094
+ }
8095
+ return themeStyle;
8096
+ }
8097
+ function collectTocHeadings(sections) {
8098
+ const entries = [];
8099
+ const ordinals = computeSectionOrdinals(sections);
8100
+ sections.forEach((section, index) => {
8101
+ const ordinal = ordinals[index]?.ordinal;
8102
+ const sectionBookmarkId = ordinal ? globalSectionBookmarkRegistry.forLayoutSection(ordinal).id : void 0;
8103
+ const visit = (component) => {
8104
+ if (!isEnabled(component)) return;
8105
+ const props = component.props ?? {};
8106
+ if (component.name === "heading") {
8107
+ const text = typeof props.text === "string" ? props.text : "";
8108
+ const title = normalizeEntryTitle(text);
8109
+ if (title) {
8110
+ const level = typeof props.level === "number" ? props.level : 1;
8111
+ entries.push({ title, level, sectionBookmarkId });
8112
+ }
8113
+ return;
8114
+ }
8115
+ if (component.name === "paragraph") {
8116
+ const styleId = styleEntryKey(props);
8117
+ const text = typeof props.text === "string" ? props.text : "";
8118
+ const title = normalizeEntryTitle(text);
8119
+ if (styleId && title) {
8120
+ entries.push({ title, level: 1, styleId, sectionBookmarkId });
8121
+ }
8122
+ return;
8123
+ }
8124
+ if (!isContainer(component.name)) return;
8125
+ const children = component.children;
8126
+ if (Array.isArray(children)) children.forEach(visit);
8127
+ };
8128
+ section.components.forEach(visit);
8129
+ });
8130
+ return entries;
8131
+ }
8132
+
7507
8133
  // src/core/render.ts
7508
8134
  function getAlignment3(alignment) {
7509
8135
  switch (alignment) {
@@ -7541,7 +8167,23 @@ async function renderDocument(structure, layout, options) {
7541
8167
  () => globalBookmarkRegistry.runScoped(
7542
8168
  () => globalRevisionIdRegistry.runScoped(
7543
8169
  () => globalNumberingRegistry.runScoped(
7544
- () => renderDocumentScoped(structure, layout, options)
8170
+ () => globalSectionBookmarkRegistry.runScoped(
8171
+ () => (
8172
+ // Comment ids are a separate OOXML namespace from w:ins/w:del,
8173
+ // but they need the same per-render isolation: outside this nest
8174
+ // concurrent generations would interleave counters and an anchor
8175
+ // would point at another document's comment body.
8176
+ globalCommentRegistry.runScoped(
8177
+ () => (
8178
+ // Footnote ids are document-scoped too: a reference resolved
8179
+ // against another render's counter points at the wrong body.
8180
+ globalNoteRegistry.runScoped(
8181
+ () => renderDocumentScoped(structure, layout, options)
8182
+ )
8183
+ )
8184
+ )
8185
+ )
8186
+ )
7545
8187
  )
7546
8188
  )
7547
8189
  )
@@ -7576,19 +8218,23 @@ async function renderDocumentScoped(structure, layout, options) {
7576
8218
  error instanceof Error ? error.message : error
7577
8219
  );
7578
8220
  }
7579
- let sectionBookmarkCounter = 0;
8221
+ try {
8222
+ const tocHeadings = collectTocHeadings(layout.sections);
8223
+ if (tocHeadings.length > 0) {
8224
+ context.tocHeadings = tocHeadings;
8225
+ }
8226
+ } catch (error) {
8227
+ console.warn(
8228
+ "[core-docx] TOC entry collection failed; the TOC field will rely on the reader refreshing it:",
8229
+ error instanceof Error ? error.message : error
8230
+ );
8231
+ }
8232
+ const sectionOrdinals = computeSectionOrdinals(layout.sections);
7580
8233
  let previousHeader = void 0;
7581
8234
  let previousFooter = void 0;
7582
8235
  for (let idx = 0; idx < layout.sections.length; idx++) {
7583
8236
  const layoutSection = layout.sections[idx];
7584
- let sectionOrdinal = void 0;
7585
- if (layoutSection.belongsToUserSection) {
7586
- if (layoutSection.isUserSection) {
7587
- sectionOrdinal = sectionBookmarkCounter + 1;
7588
- } else {
7589
- sectionOrdinal = sectionBookmarkCounter;
7590
- }
7591
- }
8237
+ const { ordinal: sectionOrdinal, closeBookmark } = sectionOrdinals[idx];
7592
8238
  let headerToUse;
7593
8239
  if (layoutSection.header === "linkToPrevious") {
7594
8240
  headerToUse = previousHeader;
@@ -7612,13 +8258,6 @@ async function renderDocumentScoped(structure, layout, options) {
7612
8258
  header: headerToUse,
7613
8259
  footer: footerToUse
7614
8260
  };
7615
- let closeBookmark = false;
7616
- if (layoutSection.belongsToUserSection && sectionOrdinal !== void 0) {
7617
- const next = layout.sections[idx + 1];
7618
- if (!next || !next.belongsToUserSection || next.isUserSection) {
7619
- closeBookmark = true;
7620
- }
7621
- }
7622
8261
  const rendered = await renderSection(
7623
8262
  sectionToRender,
7624
8263
  structure.theme,
@@ -7628,14 +8267,14 @@ async function renderDocumentScoped(structure, layout, options) {
7628
8267
  closeBookmark,
7629
8268
  options?.bypassCache === true
7630
8269
  );
7631
- if (layoutSection.isUserSection) {
7632
- sectionBookmarkCounter++;
7633
- }
7634
8270
  if (rendered.children.length > 0) {
7635
8271
  sections.push(rendered);
7636
8272
  }
7637
8273
  }
7638
8274
  const numberingConfigs = globalNumberingRegistry.getAll();
8275
+ const comments = globalCommentRegistry.getAll();
8276
+ const footnotes = globalNoteRegistry.getFootnotes();
8277
+ const endnotes = globalNoteRegistry.getEndnotes();
7639
8278
  return new Document({
7640
8279
  styles: createWordStyles(structure.theme, structure.language),
7641
8280
  sections,
@@ -7646,6 +8285,12 @@ async function renderDocumentScoped(structure, layout, options) {
7646
8285
  // Word opens the document in review mode (further edits are tracked)
7647
8286
  ...structure.trackRevisions && { trackRevisions: true }
7648
8287
  },
8288
+ // word/comments.xml, emitted only when something was actually commented
8289
+ ...comments.length > 0 && { comments: { children: comments } },
8290
+ // word/footnotes.xml and word/endnotes.xml bodies, keyed by the id their
8291
+ // references carry
8292
+ ...Object.keys(footnotes).length > 0 && { footnotes },
8293
+ ...Object.keys(endnotes).length > 0 && { endnotes },
7649
8294
  // Add numbering configurations if any lists were rendered
7650
8295
  ...numberingConfigs.length > 0 && {
7651
8296
  numbering: {
@@ -7692,9 +8337,9 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7692
8337
  let imageSource = resolveImageSource(imageComp.props);
7693
8338
  if (!imageSource) {
7694
8339
  elements.push(
7695
- new Paragraph7({
8340
+ new Paragraph9({
7696
8341
  children: [
7697
- new TextRun6({
8342
+ new TextRun9({
7698
8343
  text: "[IMAGE: Missing path, base64, or svg property]",
7699
8344
  font: getThemeFonts(theme).body.family,
7700
8345
  size: 20,
@@ -7762,7 +8407,7 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7762
8407
  ...floatingOptions && { floating: floatingOptions }
7763
8408
  });
7764
8409
  elements.push(
7765
- new Paragraph7({
8410
+ new Paragraph9({
7766
8411
  children: [imageRun],
7767
8412
  alignment: imageComp.props.alignment ? getAlignment3(imageComp.props.alignment) : void 0,
7768
8413
  style: "Normal"
@@ -7775,9 +8420,9 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7775
8420
  error instanceof Error ? error.message : error
7776
8421
  );
7777
8422
  elements.push(
7778
- new Paragraph7({
8423
+ new Paragraph9({
7779
8424
  children: [
7780
- new TextRun6({
8425
+ new TextRun9({
7781
8426
  text: `[IMAGE: ${sourcePreview}]`,
7782
8427
  font: getThemeFonts(theme).body.family,
7783
8428
  size: 20,
@@ -7807,8 +8452,9 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
7807
8452
  async function renderSection(section, theme, themeName, context, sectionOrdinal, closeBookmark, bypassCache = false) {
7808
8453
  const elements = [];
7809
8454
  const isFirstLayoutOfUserSection = section.isUserSection;
7810
- const sharedLinkId = section.belongsToUserSection && sectionOrdinal ? sectionOrdinal : void 0;
7811
- const sectionBookmarkId = sharedLinkId !== void 0 ? `_Section_${sharedLinkId}` : void 0;
8455
+ const bookmark = section.belongsToUserSection && sectionOrdinal ? globalSectionBookmarkRegistry.forLayoutSection(sectionOrdinal) : void 0;
8456
+ const sharedLinkId = bookmark?.linkId;
8457
+ const sectionBookmarkId = bookmark?.id;
7812
8458
  const sectionContext = {
7813
8459
  ...context,
7814
8460
  section: {
@@ -7821,7 +8467,7 @@ async function renderSection(section, theme, themeName, context, sectionOrdinal,
7821
8467
  };
7822
8468
  if (sectionBookmarkId && isFirstLayoutOfUserSection && sharedLinkId !== void 0) {
7823
8469
  elements.push(
7824
- new Paragraph7({
8470
+ new Paragraph9({
7825
8471
  children: [new BookmarkStart2(sectionBookmarkId, sharedLinkId)],
7826
8472
  spacing: {
7827
8473
  before: 0,
@@ -7846,7 +8492,7 @@ async function renderSection(section, theme, themeName, context, sectionOrdinal,
7846
8492
  }
7847
8493
  if (closeBookmark && sharedLinkId !== void 0) {
7848
8494
  elements.push(
7849
- new Paragraph7({
8495
+ new Paragraph9({
7850
8496
  children: [new BookmarkEnd2(sharedLinkId)]
7851
8497
  })
7852
8498
  );
@@ -8022,9 +8668,9 @@ function fixFloatingImageIdsInBuffer(buffer) {
8022
8668
  throw new Error("document.xml not found in DOCX");
8023
8669
  }
8024
8670
  let idCounter = 1;
8025
- const documentXml = documentEntry.getData().toString("utf8").replace(/<wp:docPr\s+id="(\d+)"/g, () => {
8671
+ const documentXml = documentEntry.getData().toString("utf8").replace(/(<wp:docPr\b[^>]*?\s)id="\d+"/g, (_match, prefix) => {
8026
8672
  const newId = idCounter++;
8027
- return `<wp:docPr id="${newId}"`;
8673
+ return `${prefix}id="${newId}"`;
8028
8674
  });
8029
8675
  zip.updateFile(documentEntry, Buffer.from(documentXml, "utf8"));
8030
8676
  return zip.toBuffer();