@json-to-office/core-docx 0.36.1 → 0.38.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 (39) 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/text-box.d.ts +1 -1
  4. package/dist/components/text-box.d.ts.map +1 -1
  5. package/dist/components/toc/index.d.ts.map +1 -1
  6. package/dist/core/cached-render.d.ts +2 -2
  7. package/dist/core/cached-render.d.ts.map +1 -1
  8. package/dist/core/collectTocHeadings.d.ts +27 -1
  9. package/dist/core/collectTocHeadings.d.ts.map +1 -1
  10. package/dist/core/content.d.ts +10 -0
  11. package/dist/core/content.d.ts.map +1 -1
  12. package/dist/core/render.d.ts +2 -2
  13. package/dist/core/render.d.ts.map +1 -1
  14. package/dist/index.js +655 -201
  15. package/dist/index.js.map +1 -1
  16. package/dist/plugin/example/index.js +653 -201
  17. package/dist/plugin/example/index.js.map +1 -1
  18. package/dist/templates/themes/apex.docx.theme.json +0 -3
  19. package/dist/templates/themes/corporate.docx.theme.json +0 -3
  20. package/dist/templates/themes/devportal.docx.theme.json +0 -3
  21. package/dist/templates/themes/index.d.ts +4 -0
  22. package/dist/templates/themes/index.d.ts.map +1 -1
  23. package/dist/templates/themes/minimal.docx.theme.json +0 -3
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/dist/utils/bookmarkRegistry.d.ts +34 -0
  26. package/dist/utils/bookmarkRegistry.d.ts.map +1 -1
  27. package/dist/utils/listLevels.d.ts +38 -0
  28. package/dist/utils/listLevels.d.ts.map +1 -0
  29. package/dist/utils/numberFormatting.d.ts +11 -0
  30. package/dist/utils/numberFormatting.d.ts.map +1 -0
  31. package/dist/utils/numberedItemsRegistry.d.ts +38 -0
  32. package/dist/utils/numberedItemsRegistry.d.ts.map +1 -0
  33. package/dist/utils/numberingConfig.d.ts +31 -0
  34. package/dist/utils/numberingConfig.d.ts.map +1 -1
  35. package/dist/utils/placeholderProcessor.d.ts +2 -2
  36. package/dist/utils/placeholderProcessor.d.ts.map +1 -1
  37. package/dist/utils/textParser.d.ts +6 -0
  38. package/dist/utils/textParser.d.ts.map +1 -1
  39. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -383,9 +383,6 @@ var init_minimal_docx_theme = __esm({
383
383
  },
384
384
  section: {
385
385
  pageBreak: false
386
- },
387
- heading: {
388
- numbering: false
389
386
  }
390
387
  }
391
388
  };
@@ -508,9 +505,6 @@ var init_corporate_docx_theme = __esm({
508
505
  },
509
506
  section: {
510
507
  pageBreak: false
511
- },
512
- heading: {
513
- numbering: false
514
508
  }
515
509
  }
516
510
  };
@@ -753,9 +747,6 @@ var init_apex_docx_theme = __esm({
753
747
  },
754
748
  section: {
755
749
  pageBreak: false
756
- },
757
- heading: {
758
- numbering: false
759
750
  }
760
751
  }
761
752
  };
@@ -889,9 +880,6 @@ var init_devportal_docx_theme = __esm({
889
880
  },
890
881
  section: {
891
882
  pageBreak: false
892
- },
893
- heading: {
894
- numbering: false
895
883
  }
896
884
  }
897
885
  };
@@ -2635,8 +2623,8 @@ import {
2635
2623
  Paragraph as Paragraph9,
2636
2624
  TextRun as TextRun9,
2637
2625
  AlignmentType as AlignmentType5,
2638
- BookmarkStart as BookmarkStart2,
2639
- BookmarkEnd as BookmarkEnd2
2626
+ BookmarkStart as BookmarkStart3,
2627
+ BookmarkEnd as BookmarkEnd3
2640
2628
  } from "docx";
2641
2629
 
2642
2630
  // src/utils/imageUtils.ts
@@ -3752,8 +3740,51 @@ import {
3752
3740
  InternalHyperlink,
3753
3741
  FootnoteReferenceRun,
3754
3742
  EndnoteReferenceRun,
3743
+ NumberedItemReference,
3744
+ NumberedItemReferenceFormat,
3755
3745
  Tab
3756
3746
  } from "docx";
3747
+
3748
+ // src/utils/numberedItemsRegistry.ts
3749
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
3750
+ var NumberedItemsRegistry = class {
3751
+ fallback = {
3752
+ items: /* @__PURE__ */ new Map(),
3753
+ seeded: false
3754
+ };
3755
+ scopes = new AsyncLocalStorage2();
3756
+ get state() {
3757
+ return this.scopes.getStore() ?? this.fallback;
3758
+ }
3759
+ /** Run work with an isolated registry that follows its async call chain. */
3760
+ runScoped(callback) {
3761
+ return this.scopes.run({ items: /* @__PURE__ */ new Map(), seeded: false }, callback);
3762
+ }
3763
+ /** Replace the contents with the pre-pass result. */
3764
+ seed(items) {
3765
+ const state = this.state;
3766
+ state.items = new Map(items);
3767
+ state.seeded = true;
3768
+ }
3769
+ /**
3770
+ * False outside a render (a unit test calling `createText` directly). An
3771
+ * unresolved reference is then expected rather than an authoring mistake, so
3772
+ * the caller stays quiet about it.
3773
+ */
3774
+ isSeeded() {
3775
+ return this.state.seeded;
3776
+ }
3777
+ get(id) {
3778
+ return this.state.items.get(id);
3779
+ }
3780
+ clear() {
3781
+ this.state.items.clear();
3782
+ this.state.seeded = false;
3783
+ }
3784
+ };
3785
+ var globalNumberedItemsRegistry = new NumberedItemsRegistry();
3786
+
3787
+ // src/utils/textParser.ts
3757
3788
  var FOOTNOTE_MARKER_REGEX = /\[\^([^\]\s]+)\]/;
3758
3789
  function escapeRegExp(s) {
3759
3790
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3969,13 +4000,59 @@ function splitNoteMarkers(text, noteRef, runs) {
3969
4000
  if (trailing) out.push(...runs(trailing));
3970
4001
  return out;
3971
4002
  }
4003
+ var CROSS_REFERENCE_PATTERN = "\\[@([^\\]\\s:]+)(?::(relative|no_context|full_context|none))?\\]";
4004
+ var INLINE_TOKEN_REGEX = `\\[([^\\]]+)\\]\\(([^)]+)\\)|${CROSS_REFERENCE_PATTERN}`;
4005
+ function hasCrossReference(text) {
4006
+ return new RegExp(CROSS_REFERENCE_PATTERN).test(text);
4007
+ }
4008
+ var REFERENCE_FORMATS = {
4009
+ relative: NumberedItemReferenceFormat.RELATIVE,
4010
+ no_context: NumberedItemReferenceFormat.NO_CONTEXT,
4011
+ full_context: NumberedItemReferenceFormat.FULL_CONTEXT,
4012
+ none: NumberedItemReferenceFormat.NONE
4013
+ };
4014
+ function createCrossReference(id, format2, token, baseStyle, options) {
4015
+ const info = globalNumberedItemsRegistry.get(id);
4016
+ if (!info) {
4017
+ if (globalNumberedItemsRegistry.isSeeded()) {
4018
+ console.warn(
4019
+ `[core-docx] Cross-reference ${token} has no target: no heading or list item declares the id "${id}". Rendering the token as literal text.`
4020
+ );
4021
+ }
4022
+ return buildTextRuns(
4023
+ token,
4024
+ buildRunCommonProps(baseStyle, { boldColor: options.boldColor }),
4025
+ { noProof: baseStyle.noProof, noProofWords: options.noProofWords }
4026
+ );
4027
+ }
4028
+ if (format2 === "none") {
4029
+ return [
4030
+ new NumberedItemReference(id, info.text, {
4031
+ hyperlink: true,
4032
+ referenceFormat: REFERENCE_FORMATS.none
4033
+ })
4034
+ ];
4035
+ }
4036
+ const cachedValue = format2 === "no_context" ? info.own : info.full;
4037
+ if (cachedValue === void 0) {
4038
+ console.warn(
4039
+ `[core-docx] Cross-reference ${token} targets an unnumbered ${info.kind} ("${id}"), so the field carries no cached number and reads blank until the reader updates fields. Use [@${id}:none] to reference its text instead.`
4040
+ );
4041
+ }
4042
+ return [
4043
+ new NumberedItemReference(id, cachedValue, {
4044
+ hyperlink: true,
4045
+ referenceFormat: REFERENCE_FORMATS[format2]
4046
+ })
4047
+ ];
4048
+ }
3972
4049
  function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
3973
4050
  const normalizedText = normalizeUnicodeText(text);
3974
4051
  const runs = [];
3975
- const hyperlinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
4052
+ const tokenRegex = new RegExp(INLINE_TOKEN_REGEX, "g");
3976
4053
  let lastIndex = 0;
3977
4054
  let match;
3978
- while ((match = hyperlinkRegex.exec(normalizedText)) !== null) {
4055
+ while ((match = tokenRegex.exec(normalizedText)) !== null) {
3979
4056
  if (match.index > lastIndex) {
3980
4057
  const plainText = normalizedText.substring(lastIndex, match.index);
3981
4058
  if (plainText) {
@@ -3987,6 +4064,19 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
3987
4064
  runs.push(...plainRuns);
3988
4065
  }
3989
4066
  }
4067
+ lastIndex = match.index + match[0].length;
4068
+ if (match[1] === void 0) {
4069
+ runs.push(
4070
+ ...createCrossReference(
4071
+ match[3],
4072
+ match[4] ?? "relative",
4073
+ match[0],
4074
+ baseStyle,
4075
+ options
4076
+ )
4077
+ );
4078
+ continue;
4079
+ }
3990
4080
  const linkText = match[1];
3991
4081
  const linkUrl = match[2];
3992
4082
  const isInternal = linkUrl.startsWith("#");
@@ -4013,7 +4103,6 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
4013
4103
  })
4014
4104
  );
4015
4105
  }
4016
- lastIndex = match.index + match[0].length;
4017
4106
  }
4018
4107
  if (lastIndex < normalizedText.length) {
4019
4108
  const remainingText = normalizedText.substring(lastIndex);
@@ -4222,7 +4311,7 @@ function initializeBuiltinPlaceholders() {
4222
4311
  initializeBuiltinPlaceholders();
4223
4312
 
4224
4313
  // src/utils/revisionUtils.ts
4225
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
4314
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
4226
4315
 
4227
4316
  // src/utils/componentAnnotations.ts
4228
4317
  function isRecord(value) {
@@ -4273,7 +4362,7 @@ var DEFAULT_REVISION_AUTHOR = "json-to-office";
4273
4362
  var DEFAULT_REVISION_DATE = "1970-01-01T00:00:00Z";
4274
4363
  var RevisionIdRegistry = class {
4275
4364
  fallbackCounter = 0;
4276
- scopes = new AsyncLocalStorage2();
4365
+ scopes = new AsyncLocalStorage3();
4277
4366
  runScoped(callback) {
4278
4367
  return this.scopes.run({ counter: 0 }, callback);
4279
4368
  }
@@ -4548,23 +4637,38 @@ import {
4548
4637
  PageNumber as PageNumber2,
4549
4638
  ColumnBreak,
4550
4639
  TableLayoutType,
4551
- VerticalAlign,
4552
- Bookmark
4640
+ VerticalAlign
4553
4641
  } from "docx";
4554
4642
  init_styles();
4555
4643
  init_defaults();
4556
4644
 
4557
4645
  // src/utils/bookmarkRegistry.ts
4558
- import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
4646
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
4647
+ import { BookmarkStart, BookmarkEnd } from "docx";
4648
+ var CONTENT_LINK_ID_BASE = 2e6;
4649
+ function createState() {
4650
+ return { bookmarks: /* @__PURE__ */ new Map(), nextLinkId: CONTENT_LINK_ID_BASE + 1 };
4651
+ }
4652
+ function slugifyBookmarkText(text) {
4653
+ return text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").substring(0, 40);
4654
+ }
4655
+ function dedupeBookmarkId(base, taken) {
4656
+ let id = base;
4657
+ let attempt = 0;
4658
+ while (taken(id) && attempt < 100) {
4659
+ id = `${base}-${++attempt}`;
4660
+ }
4661
+ return id;
4662
+ }
4559
4663
  var BookmarkRegistry = class {
4560
- fallback = { bookmarks: /* @__PURE__ */ new Map() };
4561
- scopes = new AsyncLocalStorage3();
4664
+ fallback = createState();
4665
+ scopes = new AsyncLocalStorage4();
4562
4666
  get state() {
4563
4667
  return this.scopes.getStore() ?? this.fallback;
4564
4668
  }
4565
4669
  /** Run work with an isolated registry that follows its async call chain. */
4566
4670
  runScoped(callback) {
4567
- return this.scopes.run({ bookmarks: /* @__PURE__ */ new Map() }, callback);
4671
+ return this.scopes.run(createState(), callback);
4568
4672
  }
4569
4673
  /**
4570
4674
  * Register a bookmark
@@ -4577,18 +4681,28 @@ var BookmarkRegistry = class {
4577
4681
  }
4578
4682
  this.state.bookmarks.set(id, { id, title, type });
4579
4683
  }
4684
+ /**
4685
+ * A `w:id` no other bookmark in this document will use.
4686
+ *
4687
+ * docx's own `Bookmark` cannot supply one: its constructor builds a fresh
4688
+ * id generator per instance (`bookmarkUniqueNumericIdGen()`), so every
4689
+ * bookmark it emits carries `w:id="1"`. Reported upstream as dolanmiu/docx
4690
+ * #3478, still unreleased as of 9.7.1. Duplicated ids leave the start/end
4691
+ * pairing ambiguous, which is why a `REF` field could not read a target's
4692
+ * text even though navigating to it by name worked.
4693
+ */
4694
+ allocateLinkId() {
4695
+ return this.state.nextLinkId++;
4696
+ }
4580
4697
  /**
4581
4698
  * Generate a unique bookmark ID from text
4582
4699
  * Converts text to a URL-friendly format
4583
4700
  */
4584
4701
  generateId(text, _type = "bookmark") {
4585
- const baseId = text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").substring(0, 40);
4586
- let id = baseId;
4587
- let attempt = 0;
4588
- while (this.state.bookmarks.has(id) && attempt < 100) {
4589
- id = `${baseId}-${++attempt}`;
4590
- }
4591
- return id;
4702
+ return dedupeBookmarkId(
4703
+ slugifyBookmarkText(text),
4704
+ (id) => this.state.bookmarks.has(id)
4705
+ );
4592
4706
  }
4593
4707
  /**
4594
4708
  * Check if a bookmark exists
@@ -4629,6 +4743,14 @@ var BookmarkRegistry = class {
4629
4743
  }
4630
4744
  };
4631
4745
  var globalBookmarkRegistry = new BookmarkRegistry();
4746
+ function createBookmarkedContent(name, children) {
4747
+ const linkId = globalBookmarkRegistry.allocateLinkId();
4748
+ return [
4749
+ new BookmarkStart(name, linkId),
4750
+ ...children,
4751
+ new BookmarkEnd(linkId)
4752
+ ];
4753
+ }
4632
4754
 
4633
4755
  // src/utils/commentAnchors.ts
4634
4756
  import {
@@ -4640,10 +4762,10 @@ import {
4640
4762
 
4641
4763
  // src/utils/commentRegistry.ts
4642
4764
  import { Paragraph, TextRun as TextRun4 } from "docx";
4643
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
4765
+ import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4644
4766
  var DEFAULT_COMMENT_AUTHOR = "json-to-office";
4645
4767
  var DEFAULT_COMMENT_DATE = "1970-01-01T00:00:00Z";
4646
- function createState() {
4768
+ function createState2() {
4647
4769
  return { counter: 0, comments: [], hasResolved: false };
4648
4770
  }
4649
4771
  function deriveInitials(author) {
@@ -4654,14 +4776,14 @@ function bodyParagraphs(text) {
4654
4776
  return normalizeUnicodeText(text).split("\n").map((line) => new Paragraph({ children: [new TextRun4({ text: line })] }));
4655
4777
  }
4656
4778
  var CommentRegistry = class {
4657
- fallback = createState();
4658
- scopes = new AsyncLocalStorage4();
4779
+ fallback = createState2();
4780
+ scopes = new AsyncLocalStorage5();
4659
4781
  get state() {
4660
4782
  return this.scopes.getStore() ?? this.fallback;
4661
4783
  }
4662
4784
  /** Run work with an isolated registry that follows its async call chain. */
4663
4785
  runScoped(callback) {
4664
- return this.scopes.run(createState(), callback);
4786
+ return this.scopes.run(createState2(), callback);
4665
4787
  }
4666
4788
  /**
4667
4789
  * Register a comment thread and return every id its anchors must carry — the
@@ -4729,7 +4851,7 @@ var CommentRegistry = class {
4729
4851
  state.comments.length = 0;
4730
4852
  state.hasResolved = false;
4731
4853
  } else {
4732
- this.fallback = createState();
4854
+ this.fallback = createState2();
4733
4855
  }
4734
4856
  }
4735
4857
  };
@@ -4750,8 +4872,8 @@ function closeCommentRange(ids) {
4750
4872
 
4751
4873
  // src/utils/noteRegistry.ts
4752
4874
  import { Paragraph as Paragraph2, TextRun as TextRun6 } from "docx";
4753
- import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4754
- function createState2() {
4875
+ import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
4876
+ function createState3() {
4755
4877
  return {
4756
4878
  footnoteCounter: 0,
4757
4879
  endnoteCounter: 0,
@@ -4768,14 +4890,14 @@ function bodyParagraphs2(text, style) {
4768
4890
  );
4769
4891
  }
4770
4892
  var NoteRegistry = class {
4771
- fallback = createState2();
4772
- scopes = new AsyncLocalStorage5();
4893
+ fallback = createState3();
4894
+ scopes = new AsyncLocalStorage6();
4773
4895
  get state() {
4774
4896
  return this.scopes.getStore() ?? this.fallback;
4775
4897
  }
4776
4898
  /** Run work with an isolated registry that follows its async call chain. */
4777
4899
  runScoped(callback) {
4778
- return this.scopes.run(createState2(), callback);
4900
+ return this.scopes.run(createState3(), callback);
4779
4901
  }
4780
4902
  /**
4781
4903
  * Register a footnote body and return the id its reference must use. Ids are
@@ -4819,7 +4941,7 @@ var NoteRegistry = class {
4819
4941
  delete state.endnotes[key];
4820
4942
  }
4821
4943
  } else {
4822
- this.fallback = createState2();
4944
+ this.fallback = createState3();
4823
4945
  }
4824
4946
  }
4825
4947
  };
@@ -4985,10 +5107,7 @@ function createText(content, theme, themeName, options = {}) {
4985
5107
  "paragraph"
4986
5108
  );
4987
5109
  children.push(
4988
- new Bookmark({
4989
- id: options.bookmarkId,
4990
- children: revisionRuns
4991
- })
5110
+ ...createBookmarkedContent(options.bookmarkId, revisionRuns)
4992
5111
  );
4993
5112
  } else {
4994
5113
  children.push(...revisionRuns);
@@ -5011,12 +5130,7 @@ function createText(content, theme, themeName, options = {}) {
5011
5130
  normalizedContent,
5012
5131
  "paragraph"
5013
5132
  );
5014
- children.push(
5015
- new Bookmark({
5016
- id: options.bookmarkId,
5017
- children: textRuns
5018
- })
5019
- );
5133
+ children.push(...createBookmarkedContent(options.bookmarkId, textRuns));
5020
5134
  } else {
5021
5135
  children.push(...textRuns);
5022
5136
  }
@@ -5125,7 +5239,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5125
5239
  if (commentAnchor) {
5126
5240
  children.push(...commentAnchor.start);
5127
5241
  }
5128
- const hasDecorators = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText);
5242
+ const hasInlineSyntax = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText) || hasCrossReference(normalizedText);
5129
5243
  const headingHasWeightRequest = options.fontWeight != null || options.bold === true;
5130
5244
  const headingEffectiveFamily = options.fontFamily ?? (headingHasWeightRequest ? resolveFontFamily(theme, "heading") : void 0);
5131
5245
  const headingWeighted = applyFontWeightAlias({
@@ -5177,10 +5291,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5177
5291
  "heading"
5178
5292
  );
5179
5293
  children.push(
5180
- new Bookmark({
5181
- id: options.bookmarkId,
5182
- children: revisionRuns
5183
- })
5294
+ ...createBookmarkedContent(options.bookmarkId, revisionRuns)
5184
5295
  );
5185
5296
  } else {
5186
5297
  children.push(...revisionRuns);
@@ -5192,7 +5303,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5192
5303
  "heading"
5193
5304
  );
5194
5305
  const headingTextChildren = [];
5195
- if (hasDecorators) {
5306
+ if (hasInlineSyntax) {
5196
5307
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
5197
5308
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
5198
5309
  enableHyperlinks: true,
@@ -5203,13 +5314,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5203
5314
  headingTextChildren.push(...makeHeadingRuns(normalizedText));
5204
5315
  }
5205
5316
  children.push(
5206
- new Bookmark({
5207
- id: options.bookmarkId,
5208
- children: headingTextChildren
5209
- })
5317
+ ...createBookmarkedContent(options.bookmarkId, headingTextChildren)
5210
5318
  );
5211
5319
  } else {
5212
- if (hasDecorators) {
5320
+ if (hasInlineSyntax) {
5213
5321
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
5214
5322
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
5215
5323
  enableHyperlinks: true,
@@ -5231,7 +5339,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5231
5339
  spacing: hasExplicitSpacing ? spacing : void 0,
5232
5340
  ...options.keepNext !== void 0 && { keepNext: options.keepNext },
5233
5341
  ...options.keepLines !== void 0 && { keepLines: options.keepLines },
5234
- ...options.indent && { indent: options.indent }
5342
+ ...options.indent && { indent: options.indent },
5343
+ // docx creates the concrete numbering instance for the reference itself,
5344
+ // in ParagraphProperties.prepForXml.
5345
+ ...options.numbering !== void 0 && { numbering: options.numbering }
5235
5346
  });
5236
5347
  }
5237
5348
  async function createImage(path4, theme, themeName, options = {}) {
@@ -5391,9 +5502,15 @@ function createList(items, _theme, _themeName, options = {}) {
5391
5502
  } else if (options.spacing?.item) {
5392
5503
  spacing.after = pointsToTwips(options.spacing.item);
5393
5504
  }
5505
+ const itemId = typeof item === "object" ? item.id : void 0;
5506
+ let itemContent = textRuns;
5507
+ if (itemId) {
5508
+ globalBookmarkRegistry.register(itemId, itemText, "list-item");
5509
+ itemContent = createBookmarkedContent(itemId, textRuns);
5510
+ }
5394
5511
  const paragraphChildren = [
5395
5512
  ...commentAnchor && index === firstRendered ? commentAnchor.start : [],
5396
- ...textRuns,
5513
+ ...itemContent,
5397
5514
  ...commentAnchor && index === lastRendered ? closeCommentRange(commentAnchor.ids) : []
5398
5515
  ];
5399
5516
  const paragraph = new Paragraph3({
@@ -6236,54 +6353,14 @@ function createFooterElement(children, _options) {
6236
6353
  });
6237
6354
  }
6238
6355
 
6239
- // src/components/heading.ts
6240
- function renderHeadingComponent(component, theme, themeName) {
6241
- if (!isHeadingComponent(component)) return [];
6242
- const config = component.props;
6243
- const bookmarkId = component.id || globalBookmarkRegistry.generateId(config.text, "heading");
6244
- const header = createHeading(
6245
- config.text,
6246
- config.level || 1,
6247
- theme,
6248
- themeName,
6249
- {
6250
- alignment: config.alignment,
6251
- spacing: config.spacing,
6252
- lineSpacing: config.lineSpacing,
6253
- columnBreak: config.columnBreak,
6254
- // Local font overrides
6255
- fontFamily: config.font?.family,
6256
- fontSize: config.font?.size,
6257
- fontColor: config.font?.color,
6258
- bold: config.font?.bold,
6259
- fontWeight: config.font?.fontWeight,
6260
- italic: config.font?.italic,
6261
- underline: config.font?.underline,
6262
- scale: config.font?.scale,
6263
- characterSpacing: config.font?.characterSpacing,
6264
- // Proofing: local language override + no-proof toggle + known-words list
6265
- language: config.language,
6266
- noProof: config.noProof,
6267
- noProofWords: config.noProofWords,
6268
- // Pagination control
6269
- keepNext: config.keepNext,
6270
- keepLines: config.keepLines,
6271
- // Paragraph indentation (w:ind) in twips
6272
- indent: config.indent,
6273
- // Bookmark ID for internal linking
6274
- bookmarkId,
6275
- // Tracked-change segments (rendered as native Word revisions)
6276
- revision: config.revision,
6277
- // Review comment anchored to this heading's text
6278
- comment: config.comment
6279
- }
6280
- );
6281
- return [header];
6282
- }
6283
-
6284
6356
  // src/utils/numberingConfig.ts
6285
- import { AlignmentType as AlignmentType3, convertInchesToTwip, LevelFormat } from "docx";
6286
- import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
6357
+ import {
6358
+ AlignmentType as AlignmentType3,
6359
+ convertInchesToTwip,
6360
+ LevelFormat,
6361
+ LevelSuffix
6362
+ } from "docx";
6363
+ import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
6287
6364
  var LEVEL_FORMAT_MAP = {
6288
6365
  decimal: LevelFormat.DECIMAL,
6289
6366
  upperRoman: LevelFormat.UPPER_ROMAN,
@@ -6411,12 +6488,37 @@ function createNumberingConfig(config) {
6411
6488
  levels
6412
6489
  };
6413
6490
  }
6491
+ var HEADING_NUMBERING_REFERENCE = "jto-heading-numbering";
6492
+ function createHeadingNumberingConfig() {
6493
+ const levels = [];
6494
+ for (let level = 0; level < 6; level++) {
6495
+ const text = Array.from({ length: level + 1 }, (_, i) => `%${i + 1}`).join(".") + ".";
6496
+ levels.push({
6497
+ level,
6498
+ format: LevelFormat.DECIMAL,
6499
+ text,
6500
+ alignment: AlignmentType3.LEFT,
6501
+ start: 1,
6502
+ // A space, not the default tab: a tab would push the heading text to the
6503
+ // next tab stop and misalign it against unnumbered headings.
6504
+ suffix: LevelSuffix.SPACE,
6505
+ style: {
6506
+ paragraph: { indent: { left: 0, hanging: 0 } },
6507
+ style: `Heading${level + 1}`
6508
+ }
6509
+ });
6510
+ }
6511
+ return { reference: HEADING_NUMBERING_REFERENCE, levels };
6512
+ }
6513
+ function headingNumberLabel(number) {
6514
+ return `${number}.`;
6515
+ }
6414
6516
  var NumberingRegistry = class {
6415
6517
  fallback = {
6416
6518
  configs: /* @__PURE__ */ new Map(),
6417
6519
  counter: 0
6418
6520
  };
6419
- scopes = new AsyncLocalStorage6();
6521
+ scopes = new AsyncLocalStorage7();
6420
6522
  get state() {
6421
6523
  return this.scopes.getStore() ?? this.fallback;
6422
6524
  }
@@ -6466,6 +6568,63 @@ var NumberingRegistry = class {
6466
6568
  };
6467
6569
  var globalNumberingRegistry = new NumberingRegistry();
6468
6570
 
6571
+ // src/components/heading.ts
6572
+ var MAX_HEADING_LEVEL = 6;
6573
+ function headingNumbering(numbering, level) {
6574
+ if (numbering === false) return false;
6575
+ if (numbering !== true) return void 0;
6576
+ if (!globalNumberingRegistry.has(HEADING_NUMBERING_REFERENCE)) {
6577
+ globalNumberingRegistry.register(createHeadingNumberingConfig());
6578
+ }
6579
+ const styleLevel = level >= 1 && level <= MAX_HEADING_LEVEL ? level : 1;
6580
+ return { reference: HEADING_NUMBERING_REFERENCE, level: styleLevel - 1 };
6581
+ }
6582
+ function renderHeadingComponent(component, theme, themeName) {
6583
+ if (!isHeadingComponent(component)) return [];
6584
+ const config = component.props;
6585
+ const bookmarkId = component.id || globalBookmarkRegistry.generateId(config.text, "heading");
6586
+ const header = createHeading(
6587
+ config.text,
6588
+ config.level || 1,
6589
+ theme,
6590
+ themeName,
6591
+ {
6592
+ alignment: config.alignment,
6593
+ spacing: config.spacing,
6594
+ lineSpacing: config.lineSpacing,
6595
+ columnBreak: config.columnBreak,
6596
+ // Local font overrides
6597
+ fontFamily: config.font?.family,
6598
+ fontSize: config.font?.size,
6599
+ fontColor: config.font?.color,
6600
+ bold: config.font?.bold,
6601
+ fontWeight: config.font?.fontWeight,
6602
+ italic: config.font?.italic,
6603
+ underline: config.font?.underline,
6604
+ scale: config.font?.scale,
6605
+ characterSpacing: config.font?.characterSpacing,
6606
+ // Proofing: local language override + no-proof toggle + known-words list
6607
+ language: config.language,
6608
+ noProof: config.noProof,
6609
+ noProofWords: config.noProofWords,
6610
+ // Pagination control
6611
+ keepNext: config.keepNext,
6612
+ keepLines: config.keepLines,
6613
+ // Paragraph indentation (w:ind) in twips
6614
+ indent: config.indent,
6615
+ // Bookmark ID for internal linking
6616
+ bookmarkId,
6617
+ // Auto-numbering (1., 1.1., …) through the shared heading definition
6618
+ numbering: headingNumbering(config.numbering, config.level || 1),
6619
+ // Tracked-change segments (rendered as native Word revisions)
6620
+ revision: config.revision,
6621
+ // Review comment anchored to this heading's text
6622
+ comment: config.comment
6623
+ }
6624
+ );
6625
+ return [header];
6626
+ }
6627
+
6469
6628
  // src/components/paragraph.ts
6470
6629
  function parseMarkdownList(text) {
6471
6630
  const lines = text.split("\n");
@@ -6605,8 +6764,7 @@ function renderParagraphComponent(component, theme, themeName) {
6605
6764
  return [text];
6606
6765
  }
6607
6766
 
6608
- // src/components/list.ts
6609
- init_colorUtils();
6767
+ // src/utils/listLevels.ts
6610
6768
  function createLevelsFromSimplifiedProps(props) {
6611
6769
  const levels = [];
6612
6770
  let format2;
@@ -6653,18 +6811,8 @@ function createLevelsFromSimplifiedProps(props) {
6653
6811
  }
6654
6812
  return levels;
6655
6813
  }
6656
- function resolveMarkerFonts(levels, theme) {
6657
- return levels.map((level) => {
6658
- const font = level.font;
6659
- if (!font?.color) return level;
6660
- return {
6661
- ...level,
6662
- font: { ...font, color: resolveColor(font.color, theme) }
6663
- };
6664
- });
6665
- }
6666
6814
  function applyListStart(levels, start) {
6667
- if (start === void 0) return levels;
6815
+ if (start === void 0) return [...levels];
6668
6816
  return levels.map(
6669
6817
  (level) => level.level === 0 && level.start === void 0 ? { ...level, start } : level
6670
6818
  );
@@ -6722,28 +6870,37 @@ function fillMissingLevels(levels, maxLevel) {
6722
6870
  }
6723
6871
  return result;
6724
6872
  }
6873
+ function resolveListLevels(props) {
6874
+ const maxLevel = getMaxLevelFromItems(props.items);
6875
+ if (props.levels && props.levels.length > 0) {
6876
+ return fillMissingLevels(
6877
+ applyListStart(props.levels, props.start),
6878
+ maxLevel
6879
+ );
6880
+ }
6881
+ return fillMissingLevels(createLevelsFromSimplifiedProps(props), maxLevel);
6882
+ }
6883
+
6884
+ // src/components/list.ts
6885
+ init_colorUtils();
6886
+ function resolveMarkerFonts(levels, theme) {
6887
+ return levels.map((level) => {
6888
+ const font = level.font;
6889
+ if (!font?.color) return level;
6890
+ return {
6891
+ ...level,
6892
+ font: { ...font, color: resolveColor(font.color, theme) }
6893
+ };
6894
+ });
6895
+ }
6725
6896
  function renderListComponent(component, theme, themeName) {
6726
6897
  if (!isListComponent(component)) return [];
6727
6898
  const resolvedConfig = component.props;
6728
- const maxLevel = getMaxLevelFromItems(resolvedConfig.items);
6729
6899
  const reference = resolvedConfig.reference || globalNumberingRegistry.generateReference("list");
6730
6900
  if (!globalNumberingRegistry.has(reference)) {
6731
- let levels;
6732
- if (resolvedConfig.levels && resolvedConfig.levels.length > 0) {
6733
- levels = fillMissingLevels(
6734
- applyListStart(
6735
- resolvedConfig.levels,
6736
- resolvedConfig.start
6737
- ),
6738
- maxLevel
6739
- );
6740
- } else {
6741
- const baseLevels = createLevelsFromSimplifiedProps(resolvedConfig);
6742
- levels = fillMissingLevels(baseLevels, maxLevel);
6743
- }
6744
6901
  const config = {
6745
6902
  reference,
6746
- levels: resolveMarkerFonts(levels, theme)
6903
+ levels: resolveMarkerFonts(resolveListLevels(resolvedConfig), theme)
6747
6904
  };
6748
6905
  const numberingConfig = createNumberingConfig(config);
6749
6906
  globalNumberingRegistry.register(numberingConfig);
@@ -6783,6 +6940,7 @@ async function renderImageComponent(component, theme, themeName) {
6783
6940
 
6784
6941
  // src/components/text-box.ts
6785
6942
  import {
6943
+ Paragraph as Paragraph5,
6786
6944
  Table as Table3,
6787
6945
  TableRow as TableRow2,
6788
6946
  TableCell as TableCell2,
@@ -6791,7 +6949,8 @@ import {
6791
6949
  RelativeHorizontalPosition,
6792
6950
  RelativeVerticalPosition,
6793
6951
  OverlapType,
6794
- TableLayoutType as TableLayoutType2
6952
+ TableLayoutType as TableLayoutType2,
6953
+ WpsShapeRun
6795
6954
  } from "docx";
6796
6955
 
6797
6956
  // src/styles/utils/borderUtils.ts
@@ -6868,6 +7027,8 @@ function buildCellOptions(children, styleCfg, theme) {
6868
7027
  }
6869
7028
 
6870
7029
  // src/components/text-box.ts
7030
+ init_colorUtils();
7031
+ init_docxImagePositioning();
6871
7032
  init_widthUtils();
6872
7033
  function mapTableFloatOptions(floating, theme, themeName) {
6873
7034
  if (!floating) return void 0;
@@ -6923,26 +7084,23 @@ function mapTableFloatOptions(floating, theme, themeName) {
6923
7084
  opt.overlap = OverlapType.OVERLAP;
6924
7085
  return opt;
6925
7086
  }
6926
- async function renderTextBoxComponent(component, theme, themeName, _context) {
6927
- if (!isTextBoxComponent(component)) return [];
6928
- const tb = component;
7087
+ async function renderTextBoxChildren(tb, theme, themeName, context) {
7088
+ const childContext = {
7089
+ ...context,
7090
+ parent: tb
7091
+ };
7092
+ const rendered = [];
7093
+ for (const child of tb.children || []) {
7094
+ rendered.push(
7095
+ ...await renderComponent(child, theme, themeName, childContext)
7096
+ );
7097
+ }
7098
+ return rendered;
7099
+ }
7100
+ async function renderTextBoxAsTable(tb, theme, themeName, _context, prerendered) {
6929
7101
  const isInline = !tb.props.floating;
6930
- const childComponents = tb.children || [];
6931
7102
  if (isInline) {
6932
- const cellChildren2 = [];
6933
- const childContext2 = {
6934
- ..._context,
6935
- parent: tb
6936
- };
6937
- for (const child of childComponents) {
6938
- const rendered = await renderComponent(
6939
- child,
6940
- theme,
6941
- themeName,
6942
- childContext2
6943
- );
6944
- cellChildren2.push(...rendered);
6945
- }
7103
+ const cellChildren2 = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
6946
7104
  const styleCfg2 = tb.props.style;
6947
7105
  const cellOpts2 = buildCellOptions(cellChildren2, styleCfg2, theme);
6948
7106
  const row2 = new TableRow2({ children: [new TableCell2(cellOpts2)] });
@@ -6955,20 +7113,7 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
6955
7113
  });
6956
7114
  return [table2];
6957
7115
  }
6958
- const cellChildren = [];
6959
- const childContext = {
6960
- ..._context,
6961
- parent: tb
6962
- };
6963
- for (const child of childComponents) {
6964
- const rendered = await renderComponent(
6965
- child,
6966
- theme,
6967
- themeName,
6968
- childContext
6969
- );
6970
- cellChildren.push(...rendered);
6971
- }
7116
+ const cellChildren = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
6972
7117
  const styleCfg = tb.props.style;
6973
7118
  const cellOpts = buildCellOptions(cellChildren, styleCfg, theme);
6974
7119
  const row = new TableRow2({
@@ -7000,6 +7145,162 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
7000
7145
  });
7001
7146
  return [table];
7002
7147
  }
7148
+ var PIXELS_TO_EMU = 9525;
7149
+ var TWIPS_PER_PIXEL = 15;
7150
+ function resolveShapeSize(value, axis, theme, themeName) {
7151
+ if (typeof value === "number") {
7152
+ return { pixels: Math.round(value), resolvedPercentage: false };
7153
+ }
7154
+ if (typeof value !== "string") return { resolvedPercentage: false };
7155
+ const fraction = parseFloat(value) / 100;
7156
+ if (!Number.isFinite(fraction) || fraction <= 0) {
7157
+ return { resolvedPercentage: false };
7158
+ }
7159
+ const availableTwips = axis === "width" ? getAvailableWidthTwips(theme, themeName) : getAvailableHeightTwips(theme, themeName);
7160
+ return {
7161
+ pixels: Math.round(availableTwips * fraction / TWIPS_PER_PIXEL),
7162
+ resolvedPercentage: true
7163
+ };
7164
+ }
7165
+ function shapeColor(value, theme) {
7166
+ return resolveColor(value, theme).replace(/^#/, "");
7167
+ }
7168
+ function shapeOutline(style, theme) {
7169
+ const border = style?.border;
7170
+ if (!border) return { ignoredSides: [], unsupportedStyles: [] };
7171
+ const order = [
7172
+ "top",
7173
+ "left",
7174
+ "bottom",
7175
+ "right"
7176
+ ];
7177
+ const declared = order.map((side) => [side, border[side]]).filter(
7178
+ (entry) => Boolean(entry[1])
7179
+ );
7180
+ if (declared.length === 0) return { ignoredSides: [], unsupportedStyles: [] };
7181
+ const unsupportedStyles = [
7182
+ ...new Set(
7183
+ declared.map(([, config]) => config.style).filter(
7184
+ (value) => value === "dashed" || value === "dotted" || value === "double"
7185
+ )
7186
+ )
7187
+ ];
7188
+ if (unsupportedStyles.length > 0) {
7189
+ return { ignoredSides: [], unsupportedStyles };
7190
+ }
7191
+ const [, used] = declared[0];
7192
+ const differs = ({ style: s, width, color }) => s !== used.style || width !== used.width || color !== used.color;
7193
+ const ignoredSides = declared.slice(1).filter(([, config]) => differs(config)).map(([side]) => side);
7194
+ if (used.style === "none") return { ignoredSides, unsupportedStyles };
7195
+ return {
7196
+ outline: {
7197
+ type: "solidFill",
7198
+ solidFillType: "rgb",
7199
+ value: used.color ? shapeColor(used.color, theme) : "000000",
7200
+ ...used.width !== void 0 && {
7201
+ width: Math.round(used.width * PIXELS_TO_EMU)
7202
+ }
7203
+ },
7204
+ ignoredSides,
7205
+ unsupportedStyles
7206
+ };
7207
+ }
7208
+ function shapeBodyProperties(style) {
7209
+ const padding = style?.padding;
7210
+ if (!padding) return void 0;
7211
+ const toEmu = (value) => value === void 0 ? void 0 : Math.round(value * PIXELS_TO_EMU);
7212
+ return {
7213
+ margins: {
7214
+ ...padding.top !== void 0 && { top: toEmu(padding.top) },
7215
+ ...padding.bottom !== void 0 && { bottom: toEmu(padding.bottom) },
7216
+ ...padding.left !== void 0 && { left: toEmu(padding.left) },
7217
+ ...padding.right !== void 0 && { right: toEmu(padding.right) }
7218
+ }
7219
+ };
7220
+ }
7221
+ async function renderTextBoxAsShape(tb, theme, themeName, context) {
7222
+ const width = resolveShapeSize(tb.props.width, "width", theme, themeName);
7223
+ const height = resolveShapeSize(tb.props.height, "height", theme, themeName);
7224
+ if (width.pixels === void 0 || height.pixels === void 0) {
7225
+ console.warn(
7226
+ '[core-docx] text-box renderAs "shape" needs an explicit width and height (a shape has no autofit); falling back to table rendering.'
7227
+ );
7228
+ return { kind: "fallback" };
7229
+ }
7230
+ const style = tb.props.style;
7231
+ const { outline, ignoredSides, unsupportedStyles } = shapeOutline(
7232
+ style,
7233
+ theme
7234
+ );
7235
+ if (unsupportedStyles.length > 0) {
7236
+ console.warn(
7237
+ `[core-docx] text-box renderAs "shape" cannot draw a ${unsupportedStyles.join("/")} border (a shape outline has no dash pattern); falling back to table rendering, which draws it.`
7238
+ );
7239
+ return { kind: "fallback" };
7240
+ }
7241
+ const rendered = await renderTextBoxChildren(tb, theme, themeName, context);
7242
+ if (rendered.some((element) => !(element instanceof Paragraph5))) {
7243
+ console.warn(
7244
+ '[core-docx] text-box renderAs "shape" requires paragraph-only content; falling back to table rendering.'
7245
+ );
7246
+ return { kind: "fallback", rendered };
7247
+ }
7248
+ const children = rendered;
7249
+ if (width.resolvedPercentage || height.resolvedPercentage) {
7250
+ console.warn(
7251
+ '[core-docx] text-box renderAs "shape" resolves percentage sizes at generation time, against the current page content box; the shape will not reflow if the page size changes.'
7252
+ );
7253
+ }
7254
+ const fill = style?.shading?.fill;
7255
+ if (ignoredSides.length > 0) {
7256
+ console.warn(
7257
+ `[core-docx] text-box renderAs "shape" has one uniform outline; using the first declared border side and ignoring ${ignoredSides.join(", ")}.`
7258
+ );
7259
+ }
7260
+ const dropOutline = Boolean(fill) && Boolean(outline);
7261
+ if (dropOutline) {
7262
+ console.warn(
7263
+ '[core-docx] text-box renderAs "shape" cannot carry a fill and a border at once (docx emits invalid shape properties); keeping the fill and dropping the border.'
7264
+ );
7265
+ }
7266
+ const bodyProperties = shapeBodyProperties(style);
7267
+ const run = new WpsShapeRun({
7268
+ type: "wps",
7269
+ children,
7270
+ transformation: { width: width.pixels, height: height.pixels },
7271
+ ...fill && {
7272
+ solidFill: { type: "rgb", value: shapeColor(fill, theme) }
7273
+ },
7274
+ ...outline && !dropOutline && { outline },
7275
+ ...bodyProperties && { bodyProperties },
7276
+ // Absent `floating` makes it a `wp:inline` drawing.
7277
+ ...tb.props.floating && {
7278
+ floating: mapFloatingOptions(tb.props.floating, theme, themeName)
7279
+ }
7280
+ });
7281
+ return {
7282
+ kind: "shape",
7283
+ paragraphs: [
7284
+ new Paragraph5({ children: [run], spacing: { before: 0, after: 0 } })
7285
+ ]
7286
+ };
7287
+ }
7288
+ async function renderTextBoxComponent(component, theme, themeName, context) {
7289
+ if (!isTextBoxComponent(component)) return [];
7290
+ const tb = component;
7291
+ if (tb.props.renderAs === "shape") {
7292
+ const attempt = await renderTextBoxAsShape(tb, theme, themeName, context);
7293
+ if (attempt.kind === "shape") return attempt.paragraphs;
7294
+ return renderTextBoxAsTable(
7295
+ tb,
7296
+ theme,
7297
+ themeName,
7298
+ context,
7299
+ attempt.rendered
7300
+ );
7301
+ }
7302
+ return renderTextBoxAsTable(tb, theme, themeName, context);
7303
+ }
7003
7304
 
7004
7305
  // src/components/table.ts
7005
7306
  async function renderTableComponent(component, theme, themeName) {
@@ -7050,23 +7351,23 @@ async function renderTableComponent(component, theme, themeName) {
7050
7351
  }
7051
7352
 
7052
7353
  // src/components/section.ts
7053
- import { Paragraph as Paragraph6, BookmarkStart, BookmarkEnd } from "docx";
7354
+ import { Paragraph as Paragraph6, BookmarkStart as BookmarkStart2, BookmarkEnd as BookmarkEnd2 } from "docx";
7054
7355
 
7055
7356
  // src/core/sectionBookmarks.ts
7056
- import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
7357
+ import { AsyncLocalStorage as AsyncLocalStorage8 } from "async_hooks";
7057
7358
  var NESTED_LINK_ID_BASE = 1e6;
7058
- function createState3() {
7359
+ function createState4() {
7059
7360
  return { nextNested: 1, resolved: /* @__PURE__ */ new WeakMap() };
7060
7361
  }
7061
7362
  var SectionBookmarkRegistry = class {
7062
- fallback = createState3();
7063
- scopes = new AsyncLocalStorage7();
7363
+ fallback = createState4();
7364
+ scopes = new AsyncLocalStorage8();
7064
7365
  get state() {
7065
7366
  return this.scopes.getStore() ?? this.fallback;
7066
7367
  }
7067
7368
  /** Run work with an isolated registry that follows its async call chain. */
7068
7369
  runScoped(callback) {
7069
- return this.scopes.run(createState3(), callback);
7370
+ return this.scopes.run(createState4(), callback);
7070
7371
  }
7071
7372
  /**
7072
7373
  * The bookmark for a layout section, derived from its ordinal.
@@ -7097,7 +7398,7 @@ var SectionBookmarkRegistry = class {
7097
7398
  /** Test-only: reset the unscoped fallback counters. */
7098
7399
  clear() {
7099
7400
  if (!this.scopes.getStore()) {
7100
- this.fallback = createState3();
7401
+ this.fallback = createState4();
7101
7402
  }
7102
7403
  }
7103
7404
  };
@@ -7110,7 +7411,7 @@ async function renderSectionComponent(component, theme, themeName, context) {
7110
7411
  const { id: sectionBookmarkId, linkId: bookmarkLinkId } = globalSectionBookmarkRegistry.forSectionComponent(component);
7111
7412
  elements.push(
7112
7413
  new Paragraph6({
7113
- children: [new BookmarkStart(sectionBookmarkId, bookmarkLinkId)],
7414
+ children: [new BookmarkStart2(sectionBookmarkId, bookmarkLinkId)],
7114
7415
  spacing: {
7115
7416
  before: 0,
7116
7417
  after: 0,
@@ -7138,7 +7439,7 @@ async function renderSectionComponent(component, theme, themeName, context) {
7138
7439
  }
7139
7440
  elements.push(
7140
7441
  new Paragraph6({
7141
- children: [new BookmarkEnd(bookmarkLinkId)],
7442
+ children: [new BookmarkEnd2(bookmarkLinkId)],
7142
7443
  spacing: {
7143
7444
  before: 0,
7144
7445
  after: 0,
@@ -7318,7 +7619,10 @@ function selectCachedEntries(collected, options) {
7318
7619
  continue;
7319
7620
  }
7320
7621
  if (entry.level < depthStart || entry.level > depthEnd) continue;
7321
- entries.push({ title: entry.title, level: entry.level });
7622
+ entries.push({
7623
+ title: entry.number ? `${headingNumberLabel(entry.number)} ${entry.title}` : entry.title,
7624
+ level: entry.level
7625
+ });
7322
7626
  }
7323
7627
  return entries;
7324
7628
  }
@@ -8077,6 +8381,61 @@ function computeSectionOrdinals(sections) {
8077
8381
 
8078
8382
  // src/core/collectTocHeadings.ts
8079
8383
  import { getStandardComponent } from "@json-to-office/shared-docx";
8384
+
8385
+ // src/utils/numberFormatting.ts
8386
+ var ROMAN_NUMERALS = [
8387
+ [1e3, "m"],
8388
+ [900, "cm"],
8389
+ [500, "d"],
8390
+ [400, "cd"],
8391
+ [100, "c"],
8392
+ [90, "xc"],
8393
+ [50, "l"],
8394
+ [40, "xl"],
8395
+ [10, "x"],
8396
+ [9, "ix"],
8397
+ [5, "v"],
8398
+ [4, "iv"],
8399
+ [1, "i"]
8400
+ ];
8401
+ function toRoman(value) {
8402
+ let remaining = value;
8403
+ let out = "";
8404
+ for (const [amount, glyph] of ROMAN_NUMERALS) {
8405
+ while (remaining >= amount) {
8406
+ out += glyph;
8407
+ remaining -= amount;
8408
+ }
8409
+ }
8410
+ return out;
8411
+ }
8412
+ function toLetters(value) {
8413
+ const index = (value - 1) % 26;
8414
+ const repeats = Math.floor((value - 1) / 26) + 1;
8415
+ return String.fromCharCode(97 + index).repeat(repeats);
8416
+ }
8417
+ function formatNumberForLevel(value, format2) {
8418
+ if (!Number.isFinite(value) || value < 1) return void 0;
8419
+ const n = Math.floor(value);
8420
+ switch (format2) {
8421
+ case "decimal":
8422
+ return String(n);
8423
+ case "lowerLetter":
8424
+ return toLetters(n);
8425
+ case "upperLetter":
8426
+ return toLetters(n).toUpperCase();
8427
+ case "lowerRoman":
8428
+ return toRoman(n);
8429
+ case "upperRoman":
8430
+ return toRoman(n).toUpperCase();
8431
+ default:
8432
+ return void 0;
8433
+ }
8434
+ }
8435
+
8436
+ // src/core/collectTocHeadings.ts
8437
+ var MAX_HEADING_LEVEL2 = 6;
8438
+ var MAX_LIST_LEVEL = 9;
8080
8439
  function normalizeEntryTitle(text) {
8081
8440
  return normalizeUnicodeText(text).replace(/(\*\*\*|___)([\s\S]*?)\1/g, "$2").replace(/(\*\*|__)([\s\S]*?)\1/g, "$2").replace(/(\*|_)([\s\S]*?)\1/g, "$2").trim();
8082
8441
  }
@@ -8095,9 +8454,97 @@ function styleEntryKey(props) {
8095
8454
  }
8096
8455
  return themeStyle;
8097
8456
  }
8098
- function collectTocHeadings(sections) {
8457
+ function levelStart(levels, level) {
8458
+ return levels[level]?.start ?? 1;
8459
+ }
8460
+ function collectDocumentOutline(sections) {
8099
8461
  const entries = [];
8462
+ const numberedItems = /* @__PURE__ */ new Map();
8100
8463
  const ordinals = computeSectionOrdinals(sections);
8464
+ const takenIds = /* @__PURE__ */ new Set();
8465
+ const headingCounters = new Array(MAX_HEADING_LEVEL2).fill(0);
8466
+ const listCounters = /* @__PURE__ */ new Map();
8467
+ const visitHeading = (component, props, sectionBookmarkId) => {
8468
+ const text = typeof props.text === "string" ? props.text : "";
8469
+ const title = normalizeEntryTitle(text);
8470
+ const level = typeof props.level === "number" ? props.level : 1;
8471
+ const styleLevel = level >= 1 && level <= MAX_HEADING_LEVEL2 ? level : 1;
8472
+ let full;
8473
+ let own;
8474
+ if (props.numbering === true) {
8475
+ headingCounters[styleLevel - 1] += 1;
8476
+ for (let deeper = styleLevel; deeper < MAX_HEADING_LEVEL2; deeper++) {
8477
+ headingCounters[deeper] = 0;
8478
+ }
8479
+ full = headingCounters.slice(0, styleLevel).join(".");
8480
+ own = String(headingCounters[styleLevel - 1]);
8481
+ }
8482
+ const explicitId = component.id;
8483
+ const bookmarkId = typeof explicitId === "string" && explicitId ? explicitId : dedupeBookmarkId(slugifyBookmarkText(text), (id) => takenIds.has(id));
8484
+ takenIds.add(bookmarkId);
8485
+ numberedItems.set(bookmarkId, {
8486
+ kind: "heading",
8487
+ text: title,
8488
+ ...full !== void 0 && { full, own }
8489
+ });
8490
+ if (title) {
8491
+ entries.push({
8492
+ title,
8493
+ level,
8494
+ sectionBookmarkId,
8495
+ ...full !== void 0 && { number: full }
8496
+ });
8497
+ }
8498
+ };
8499
+ const visitList = (props) => {
8500
+ const items = Array.isArray(props.items) ? props.items : [];
8501
+ if (items.length === 0) return;
8502
+ const reference = typeof props.reference === "string" && props.reference ? props.reference : void 0;
8503
+ const freshState = () => {
8504
+ const levels = resolveListLevels(
8505
+ props
8506
+ );
8507
+ return {
8508
+ levels,
8509
+ counters: Array.from(
8510
+ { length: MAX_LIST_LEVEL },
8511
+ (_, level) => levelStart(levels, level) - 1
8512
+ )
8513
+ };
8514
+ };
8515
+ let state;
8516
+ if (reference === void 0) {
8517
+ state = freshState();
8518
+ } else {
8519
+ state = listCounters.get(reference) ?? freshState();
8520
+ listCounters.set(reference, state);
8521
+ }
8522
+ for (const item of items) {
8523
+ const isObject = typeof item === "object" && item !== null;
8524
+ const raw = isObject ? item.text : item;
8525
+ const text = typeof raw === "string" ? raw : "";
8526
+ if (!text.trim() && !(isObject && item.revision)) continue;
8527
+ const rawLevel = isObject ? item.level : void 0;
8528
+ const level = typeof rawLevel === "number" && rawLevel >= 0 ? rawLevel : 0;
8529
+ if (level >= MAX_LIST_LEVEL) continue;
8530
+ state.counters[level] += 1;
8531
+ for (let deeper = level + 1; deeper < MAX_LIST_LEVEL; deeper++) {
8532
+ state.counters[deeper] = levelStart(state.levels, deeper) - 1;
8533
+ }
8534
+ const id = isObject ? item.id : void 0;
8535
+ if (typeof id !== "string" || !id) continue;
8536
+ takenIds.add(id);
8537
+ const number = formatNumberForLevel(
8538
+ state.counters[level],
8539
+ state.levels[level]?.format
8540
+ );
8541
+ numberedItems.set(id, {
8542
+ kind: "list-item",
8543
+ text: normalizeUnicodeText(text).trim(),
8544
+ ...number !== void 0 && { full: number, own: number }
8545
+ });
8546
+ }
8547
+ };
8101
8548
  sections.forEach((section, index) => {
8102
8549
  const ordinal = ordinals[index]?.ordinal;
8103
8550
  const sectionBookmarkId = ordinal ? globalSectionBookmarkRegistry.forLayoutSection(ordinal).id : void 0;
@@ -8105,15 +8552,11 @@ function collectTocHeadings(sections) {
8105
8552
  if (!isEnabled(component)) return;
8106
8553
  const props = component.props ?? {};
8107
8554
  if (component.name === "heading") {
8108
- const text = typeof props.text === "string" ? props.text : "";
8109
- const title = normalizeEntryTitle(text);
8110
- if (title) {
8111
- const level = typeof props.level === "number" ? props.level : 1;
8112
- entries.push({ title, level, sectionBookmarkId });
8113
- }
8555
+ visitHeading(component, props, sectionBookmarkId);
8114
8556
  return;
8115
8557
  }
8116
8558
  if (component.name === "paragraph") {
8559
+ if (typeof props.id === "string" && props.id) takenIds.add(props.id);
8117
8560
  const styleId = styleEntryKey(props);
8118
8561
  const text = typeof props.text === "string" ? props.text : "";
8119
8562
  const title = normalizeEntryTitle(text);
@@ -8122,13 +8565,17 @@ function collectTocHeadings(sections) {
8122
8565
  }
8123
8566
  return;
8124
8567
  }
8568
+ if (component.name === "list") {
8569
+ visitList(props);
8570
+ return;
8571
+ }
8125
8572
  if (!isContainer(component.name)) return;
8126
8573
  const children = component.children;
8127
8574
  if (Array.isArray(children)) children.forEach(visit);
8128
8575
  };
8129
8576
  section.components.forEach(visit);
8130
8577
  });
8131
- return entries;
8578
+ return { entries, numberedItems };
8132
8579
  }
8133
8580
 
8134
8581
  // src/core/render.ts
@@ -8181,7 +8628,13 @@ async function renderDocument(structure, layout, options) {
8181
8628
  // Footnote ids are document-scoped too: a reference resolved
8182
8629
  // against another render's counter points at the wrong body.
8183
8630
  globalNoteRegistry.runScoped(
8184
- () => renderDocumentScoped(structure, layout, options)
8631
+ () => (
8632
+ // Cross-reference targets are keyed by bookmark id, which
8633
+ // is only unique within one document.
8634
+ globalNumberedItemsRegistry.runScoped(
8635
+ () => renderDocumentScoped(structure, layout, options)
8636
+ )
8637
+ )
8185
8638
  )
8186
8639
  )
8187
8640
  )
@@ -8224,13 +8677,14 @@ async function renderDocumentScoped(structure, layout, options) {
8224
8677
  );
8225
8678
  }
8226
8679
  try {
8227
- const tocHeadings = collectTocHeadings(layout.sections);
8228
- if (tocHeadings.length > 0) {
8229
- context.tocHeadings = tocHeadings;
8680
+ const outline = collectDocumentOutline(layout.sections);
8681
+ if (outline.entries.length > 0) {
8682
+ context.tocHeadings = outline.entries;
8230
8683
  }
8684
+ globalNumberedItemsRegistry.seed(outline.numberedItems);
8231
8685
  } catch (error) {
8232
8686
  console.warn(
8233
- "[core-docx] TOC entry collection failed; the TOC field will rely on the reader refreshing it:",
8687
+ "[core-docx] Document outline collection failed; the TOC field will rely on the reader refreshing it:",
8234
8688
  error instanceof Error ? error.message : error
8235
8689
  );
8236
8690
  }
@@ -8473,7 +8927,7 @@ async function renderSection(section, theme, themeName, context, sectionOrdinal,
8473
8927
  if (sectionBookmarkId && isFirstLayoutOfUserSection && sharedLinkId !== void 0) {
8474
8928
  elements.push(
8475
8929
  new Paragraph9({
8476
- children: [new BookmarkStart2(sectionBookmarkId, sharedLinkId)],
8930
+ children: [new BookmarkStart3(sectionBookmarkId, sharedLinkId)],
8477
8931
  spacing: {
8478
8932
  before: 0,
8479
8933
  after: 0,
@@ -8498,7 +8952,7 @@ async function renderSection(section, theme, themeName, context, sectionOrdinal,
8498
8952
  if (closeBookmark && sharedLinkId !== void 0) {
8499
8953
  elements.push(
8500
8954
  new Paragraph9({
8501
- children: [new BookmarkEnd2(sharedLinkId)]
8955
+ children: [new BookmarkEnd3(sharedLinkId)]
8502
8956
  })
8503
8957
  );
8504
8958
  }