@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
@@ -377,9 +377,6 @@ var init_minimal_docx_theme = __esm({
377
377
  },
378
378
  section: {
379
379
  pageBreak: false
380
- },
381
- heading: {
382
- numbering: false
383
380
  }
384
381
  }
385
382
  };
@@ -502,9 +499,6 @@ var init_corporate_docx_theme = __esm({
502
499
  },
503
500
  section: {
504
501
  pageBreak: false
505
- },
506
- heading: {
507
- numbering: false
508
502
  }
509
503
  }
510
504
  };
@@ -747,9 +741,6 @@ var init_apex_docx_theme = __esm({
747
741
  },
748
742
  section: {
749
743
  pageBreak: false
750
- },
751
- heading: {
752
- numbering: false
753
744
  }
754
745
  }
755
746
  };
@@ -883,9 +874,6 @@ var init_devportal_docx_theme = __esm({
883
874
  },
884
875
  section: {
885
876
  pageBreak: false
886
- },
887
- heading: {
888
- numbering: false
889
877
  }
890
878
  }
891
879
  };
@@ -2116,8 +2104,7 @@ import {
2116
2104
  PageNumber as PageNumber2,
2117
2105
  ColumnBreak,
2118
2106
  TableLayoutType,
2119
- VerticalAlign,
2120
- Bookmark
2107
+ VerticalAlign
2121
2108
  } from "docx";
2122
2109
 
2123
2110
  // src/utils/imageUtils.ts
@@ -2458,6 +2445,8 @@ import {
2458
2445
  InternalHyperlink as InternalHyperlink2,
2459
2446
  FootnoteReferenceRun as FootnoteReferenceRun2,
2460
2447
  EndnoteReferenceRun as EndnoteReferenceRun2,
2448
+ NumberedItemReference,
2449
+ NumberedItemReferenceFormat,
2461
2450
  Tab
2462
2451
  } from "docx";
2463
2452
 
@@ -2658,6 +2647,45 @@ function initializeBuiltinPlaceholders() {
2658
2647
  }
2659
2648
  initializeBuiltinPlaceholders();
2660
2649
 
2650
+ // src/utils/numberedItemsRegistry.ts
2651
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
2652
+ var NumberedItemsRegistry = class {
2653
+ fallback = {
2654
+ items: /* @__PURE__ */ new Map(),
2655
+ seeded: false
2656
+ };
2657
+ scopes = new AsyncLocalStorage2();
2658
+ get state() {
2659
+ return this.scopes.getStore() ?? this.fallback;
2660
+ }
2661
+ /** Run work with an isolated registry that follows its async call chain. */
2662
+ runScoped(callback) {
2663
+ return this.scopes.run({ items: /* @__PURE__ */ new Map(), seeded: false }, callback);
2664
+ }
2665
+ /** Replace the contents with the pre-pass result. */
2666
+ seed(items) {
2667
+ const state = this.state;
2668
+ state.items = new Map(items);
2669
+ state.seeded = true;
2670
+ }
2671
+ /**
2672
+ * False outside a render (a unit test calling `createText` directly). An
2673
+ * unresolved reference is then expected rather than an authoring mistake, so
2674
+ * the caller stays quiet about it.
2675
+ */
2676
+ isSeeded() {
2677
+ return this.state.seeded;
2678
+ }
2679
+ get(id) {
2680
+ return this.state.items.get(id);
2681
+ }
2682
+ clear() {
2683
+ this.state.items.clear();
2684
+ this.state.seeded = false;
2685
+ }
2686
+ };
2687
+ var globalNumberedItemsRegistry = new NumberedItemsRegistry();
2688
+
2661
2689
  // src/utils/textParser.ts
2662
2690
  var FOOTNOTE_MARKER_REGEX = /\[\^([^\]\s]+)\]/;
2663
2691
  function escapeRegExp(s) {
@@ -2874,13 +2902,59 @@ function splitNoteMarkers(text, noteRef, runs) {
2874
2902
  if (trailing) out.push(...runs(trailing));
2875
2903
  return out;
2876
2904
  }
2905
+ var CROSS_REFERENCE_PATTERN = "\\[@([^\\]\\s:]+)(?::(relative|no_context|full_context|none))?\\]";
2906
+ var INLINE_TOKEN_REGEX = `\\[([^\\]]+)\\]\\(([^)]+)\\)|${CROSS_REFERENCE_PATTERN}`;
2907
+ function hasCrossReference(text) {
2908
+ return new RegExp(CROSS_REFERENCE_PATTERN).test(text);
2909
+ }
2910
+ var REFERENCE_FORMATS = {
2911
+ relative: NumberedItemReferenceFormat.RELATIVE,
2912
+ no_context: NumberedItemReferenceFormat.NO_CONTEXT,
2913
+ full_context: NumberedItemReferenceFormat.FULL_CONTEXT,
2914
+ none: NumberedItemReferenceFormat.NONE
2915
+ };
2916
+ function createCrossReference(id, format2, token, baseStyle, options) {
2917
+ const info = globalNumberedItemsRegistry.get(id);
2918
+ if (!info) {
2919
+ if (globalNumberedItemsRegistry.isSeeded()) {
2920
+ console.warn(
2921
+ `[core-docx] Cross-reference ${token} has no target: no heading or list item declares the id "${id}". Rendering the token as literal text.`
2922
+ );
2923
+ }
2924
+ return buildTextRuns(
2925
+ token,
2926
+ buildRunCommonProps(baseStyle, { boldColor: options.boldColor }),
2927
+ { noProof: baseStyle.noProof, noProofWords: options.noProofWords }
2928
+ );
2929
+ }
2930
+ if (format2 === "none") {
2931
+ return [
2932
+ new NumberedItemReference(id, info.text, {
2933
+ hyperlink: true,
2934
+ referenceFormat: REFERENCE_FORMATS.none
2935
+ })
2936
+ ];
2937
+ }
2938
+ const cachedValue = format2 === "no_context" ? info.own : info.full;
2939
+ if (cachedValue === void 0) {
2940
+ console.warn(
2941
+ `[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.`
2942
+ );
2943
+ }
2944
+ return [
2945
+ new NumberedItemReference(id, cachedValue, {
2946
+ hyperlink: true,
2947
+ referenceFormat: REFERENCE_FORMATS[format2]
2948
+ })
2949
+ ];
2950
+ }
2877
2951
  function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
2878
2952
  const normalizedText = normalizeUnicodeText(text);
2879
2953
  const runs = [];
2880
- const hyperlinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
2954
+ const tokenRegex = new RegExp(INLINE_TOKEN_REGEX, "g");
2881
2955
  let lastIndex = 0;
2882
2956
  let match;
2883
- while ((match = hyperlinkRegex.exec(normalizedText)) !== null) {
2957
+ while ((match = tokenRegex.exec(normalizedText)) !== null) {
2884
2958
  if (match.index > lastIndex) {
2885
2959
  const plainText = normalizedText.substring(lastIndex, match.index);
2886
2960
  if (plainText) {
@@ -2892,6 +2966,19 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
2892
2966
  runs.push(...plainRuns);
2893
2967
  }
2894
2968
  }
2969
+ lastIndex = match.index + match[0].length;
2970
+ if (match[1] === void 0) {
2971
+ runs.push(
2972
+ ...createCrossReference(
2973
+ match[3],
2974
+ match[4] ?? "relative",
2975
+ match[0],
2976
+ baseStyle,
2977
+ options
2978
+ )
2979
+ );
2980
+ continue;
2981
+ }
2895
2982
  const linkText = match[1];
2896
2983
  const linkUrl = match[2];
2897
2984
  const isInternal = linkUrl.startsWith("#");
@@ -2918,7 +3005,6 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
2918
3005
  })
2919
3006
  );
2920
3007
  }
2921
- lastIndex = match.index + match[0].length;
2922
3008
  }
2923
3009
  if (lastIndex < normalizedText.length) {
2924
3010
  const remainingText = normalizedText.substring(lastIndex);
@@ -3612,16 +3698,32 @@ function getStyleIdForLevel(level) {
3612
3698
  }
3613
3699
 
3614
3700
  // src/utils/bookmarkRegistry.ts
3615
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
3701
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3702
+ import { BookmarkStart, BookmarkEnd } from "docx";
3703
+ var CONTENT_LINK_ID_BASE = 2e6;
3704
+ function createState() {
3705
+ return { bookmarks: /* @__PURE__ */ new Map(), nextLinkId: CONTENT_LINK_ID_BASE + 1 };
3706
+ }
3707
+ function slugifyBookmarkText(text) {
3708
+ return text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").substring(0, 40);
3709
+ }
3710
+ function dedupeBookmarkId(base, taken) {
3711
+ let id = base;
3712
+ let attempt = 0;
3713
+ while (taken(id) && attempt < 100) {
3714
+ id = `${base}-${++attempt}`;
3715
+ }
3716
+ return id;
3717
+ }
3616
3718
  var BookmarkRegistry = class {
3617
- fallback = { bookmarks: /* @__PURE__ */ new Map() };
3618
- scopes = new AsyncLocalStorage2();
3719
+ fallback = createState();
3720
+ scopes = new AsyncLocalStorage3();
3619
3721
  get state() {
3620
3722
  return this.scopes.getStore() ?? this.fallback;
3621
3723
  }
3622
3724
  /** Run work with an isolated registry that follows its async call chain. */
3623
3725
  runScoped(callback) {
3624
- return this.scopes.run({ bookmarks: /* @__PURE__ */ new Map() }, callback);
3726
+ return this.scopes.run(createState(), callback);
3625
3727
  }
3626
3728
  /**
3627
3729
  * Register a bookmark
@@ -3634,18 +3736,28 @@ var BookmarkRegistry = class {
3634
3736
  }
3635
3737
  this.state.bookmarks.set(id, { id, title, type });
3636
3738
  }
3739
+ /**
3740
+ * A `w:id` no other bookmark in this document will use.
3741
+ *
3742
+ * docx's own `Bookmark` cannot supply one: its constructor builds a fresh
3743
+ * id generator per instance (`bookmarkUniqueNumericIdGen()`), so every
3744
+ * bookmark it emits carries `w:id="1"`. Reported upstream as dolanmiu/docx
3745
+ * #3478, still unreleased as of 9.7.1. Duplicated ids leave the start/end
3746
+ * pairing ambiguous, which is why a `REF` field could not read a target's
3747
+ * text even though navigating to it by name worked.
3748
+ */
3749
+ allocateLinkId() {
3750
+ return this.state.nextLinkId++;
3751
+ }
3637
3752
  /**
3638
3753
  * Generate a unique bookmark ID from text
3639
3754
  * Converts text to a URL-friendly format
3640
3755
  */
3641
3756
  generateId(text, _type = "bookmark") {
3642
- const baseId = text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").substring(0, 40);
3643
- let id = baseId;
3644
- let attempt = 0;
3645
- while (this.state.bookmarks.has(id) && attempt < 100) {
3646
- id = `${baseId}-${++attempt}`;
3647
- }
3648
- return id;
3757
+ return dedupeBookmarkId(
3758
+ slugifyBookmarkText(text),
3759
+ (id) => this.state.bookmarks.has(id)
3760
+ );
3649
3761
  }
3650
3762
  /**
3651
3763
  * Check if a bookmark exists
@@ -3686,10 +3798,18 @@ var BookmarkRegistry = class {
3686
3798
  }
3687
3799
  };
3688
3800
  var globalBookmarkRegistry = new BookmarkRegistry();
3801
+ function createBookmarkedContent(name, children) {
3802
+ const linkId = globalBookmarkRegistry.allocateLinkId();
3803
+ return [
3804
+ new BookmarkStart(name, linkId),
3805
+ ...children,
3806
+ new BookmarkEnd(linkId)
3807
+ ];
3808
+ }
3689
3809
 
3690
3810
  // src/utils/revisionUtils.ts
3691
3811
  import { TextRun as TextRun3, InsertedTextRun, DeletedTextRun } from "docx";
3692
- import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3812
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
3693
3813
 
3694
3814
  // src/utils/componentAnnotations.ts
3695
3815
  function isRecord(value) {
@@ -3740,7 +3860,7 @@ var DEFAULT_REVISION_AUTHOR = "json-to-office";
3740
3860
  var DEFAULT_REVISION_DATE = "1970-01-01T00:00:00Z";
3741
3861
  var RevisionIdRegistry = class {
3742
3862
  fallbackCounter = 0;
3743
- scopes = new AsyncLocalStorage3();
3863
+ scopes = new AsyncLocalStorage4();
3744
3864
  runScoped(callback) {
3745
3865
  return this.scopes.run({ counter: 0 }, callback);
3746
3866
  }
@@ -3851,10 +3971,10 @@ import {
3851
3971
 
3852
3972
  // src/utils/commentRegistry.ts
3853
3973
  import { Paragraph, TextRun as TextRun4 } from "docx";
3854
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
3974
+ import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
3855
3975
  var DEFAULT_COMMENT_AUTHOR = "json-to-office";
3856
3976
  var DEFAULT_COMMENT_DATE = "1970-01-01T00:00:00Z";
3857
- function createState() {
3977
+ function createState2() {
3858
3978
  return { counter: 0, comments: [], hasResolved: false };
3859
3979
  }
3860
3980
  function deriveInitials(author) {
@@ -3865,14 +3985,14 @@ function bodyParagraphs(text) {
3865
3985
  return normalizeUnicodeText(text).split("\n").map((line) => new Paragraph({ children: [new TextRun4({ text: line })] }));
3866
3986
  }
3867
3987
  var CommentRegistry = class {
3868
- fallback = createState();
3869
- scopes = new AsyncLocalStorage4();
3988
+ fallback = createState2();
3989
+ scopes = new AsyncLocalStorage5();
3870
3990
  get state() {
3871
3991
  return this.scopes.getStore() ?? this.fallback;
3872
3992
  }
3873
3993
  /** Run work with an isolated registry that follows its async call chain. */
3874
3994
  runScoped(callback) {
3875
- return this.scopes.run(createState(), callback);
3995
+ return this.scopes.run(createState2(), callback);
3876
3996
  }
3877
3997
  /**
3878
3998
  * Register a comment thread and return every id its anchors must carry — the
@@ -3940,7 +4060,7 @@ var CommentRegistry = class {
3940
4060
  state.comments.length = 0;
3941
4061
  state.hasResolved = false;
3942
4062
  } else {
3943
- this.fallback = createState();
4063
+ this.fallback = createState2();
3944
4064
  }
3945
4065
  }
3946
4066
  };
@@ -3961,8 +4081,8 @@ function closeCommentRange(ids) {
3961
4081
 
3962
4082
  // src/utils/noteRegistry.ts
3963
4083
  import { Paragraph as Paragraph2, TextRun as TextRun6 } from "docx";
3964
- import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
3965
- function createState2() {
4084
+ import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
4085
+ function createState3() {
3966
4086
  return {
3967
4087
  footnoteCounter: 0,
3968
4088
  endnoteCounter: 0,
@@ -3979,14 +4099,14 @@ function bodyParagraphs2(text, style) {
3979
4099
  );
3980
4100
  }
3981
4101
  var NoteRegistry = class {
3982
- fallback = createState2();
3983
- scopes = new AsyncLocalStorage5();
4102
+ fallback = createState3();
4103
+ scopes = new AsyncLocalStorage6();
3984
4104
  get state() {
3985
4105
  return this.scopes.getStore() ?? this.fallback;
3986
4106
  }
3987
4107
  /** Run work with an isolated registry that follows its async call chain. */
3988
4108
  runScoped(callback) {
3989
- return this.scopes.run(createState2(), callback);
4109
+ return this.scopes.run(createState3(), callback);
3990
4110
  }
3991
4111
  /**
3992
4112
  * Register a footnote body and return the id its reference must use. Ids are
@@ -4030,7 +4150,7 @@ var NoteRegistry = class {
4030
4150
  delete state.endnotes[key];
4031
4151
  }
4032
4152
  } else {
4033
- this.fallback = createState2();
4153
+ this.fallback = createState3();
4034
4154
  }
4035
4155
  }
4036
4156
  };
@@ -4196,10 +4316,7 @@ function createText(content, theme, themeName, options = {}) {
4196
4316
  "paragraph"
4197
4317
  );
4198
4318
  children.push(
4199
- new Bookmark({
4200
- id: options.bookmarkId,
4201
- children: revisionRuns
4202
- })
4319
+ ...createBookmarkedContent(options.bookmarkId, revisionRuns)
4203
4320
  );
4204
4321
  } else {
4205
4322
  children.push(...revisionRuns);
@@ -4222,12 +4339,7 @@ function createText(content, theme, themeName, options = {}) {
4222
4339
  normalizedContent,
4223
4340
  "paragraph"
4224
4341
  );
4225
- children.push(
4226
- new Bookmark({
4227
- id: options.bookmarkId,
4228
- children: textRuns
4229
- })
4230
- );
4342
+ children.push(...createBookmarkedContent(options.bookmarkId, textRuns));
4231
4343
  } else {
4232
4344
  children.push(...textRuns);
4233
4345
  }
@@ -4336,7 +4448,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4336
4448
  if (commentAnchor) {
4337
4449
  children.push(...commentAnchor.start);
4338
4450
  }
4339
- const hasDecorators = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText);
4451
+ const hasInlineSyntax = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText) || hasCrossReference(normalizedText);
4340
4452
  const headingHasWeightRequest = options.fontWeight != null || options.bold === true;
4341
4453
  const headingEffectiveFamily = options.fontFamily ?? (headingHasWeightRequest ? resolveFontFamily(theme, "heading") : void 0);
4342
4454
  const headingWeighted = applyFontWeightAlias({
@@ -4388,10 +4500,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4388
4500
  "heading"
4389
4501
  );
4390
4502
  children.push(
4391
- new Bookmark({
4392
- id: options.bookmarkId,
4393
- children: revisionRuns
4394
- })
4503
+ ...createBookmarkedContent(options.bookmarkId, revisionRuns)
4395
4504
  );
4396
4505
  } else {
4397
4506
  children.push(...revisionRuns);
@@ -4403,7 +4512,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4403
4512
  "heading"
4404
4513
  );
4405
4514
  const headingTextChildren = [];
4406
- if (hasDecorators) {
4515
+ if (hasInlineSyntax) {
4407
4516
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
4408
4517
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
4409
4518
  enableHyperlinks: true,
@@ -4414,13 +4523,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4414
4523
  headingTextChildren.push(...makeHeadingRuns(normalizedText));
4415
4524
  }
4416
4525
  children.push(
4417
- new Bookmark({
4418
- id: options.bookmarkId,
4419
- children: headingTextChildren
4420
- })
4526
+ ...createBookmarkedContent(options.bookmarkId, headingTextChildren)
4421
4527
  );
4422
4528
  } else {
4423
- if (hasDecorators) {
4529
+ if (hasInlineSyntax) {
4424
4530
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
4425
4531
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
4426
4532
  enableHyperlinks: true,
@@ -4442,7 +4548,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4442
4548
  spacing: hasExplicitSpacing ? spacing : void 0,
4443
4549
  ...options.keepNext !== void 0 && { keepNext: options.keepNext },
4444
4550
  ...options.keepLines !== void 0 && { keepLines: options.keepLines },
4445
- ...options.indent && { indent: options.indent }
4551
+ ...options.indent && { indent: options.indent },
4552
+ // docx creates the concrete numbering instance for the reference itself,
4553
+ // in ParagraphProperties.prepForXml.
4554
+ ...options.numbering !== void 0 && { numbering: options.numbering }
4446
4555
  });
4447
4556
  }
4448
4557
  async function createImage(path3, theme, themeName, options = {}) {
@@ -4602,9 +4711,15 @@ function createList(items, _theme, _themeName, options = {}) {
4602
4711
  } else if (options.spacing?.item) {
4603
4712
  spacing.after = pointsToTwips(options.spacing.item);
4604
4713
  }
4714
+ const itemId = typeof item === "object" ? item.id : void 0;
4715
+ let itemContent = textRuns;
4716
+ if (itemId) {
4717
+ globalBookmarkRegistry.register(itemId, itemText, "list-item");
4718
+ itemContent = createBookmarkedContent(itemId, textRuns);
4719
+ }
4605
4720
  const paragraphChildren = [
4606
4721
  ...commentAnchor && index === firstRendered ? commentAnchor.start : [],
4607
- ...textRuns,
4722
+ ...itemContent,
4608
4723
  ...commentAnchor && index === lastRendered ? closeCommentRange(commentAnchor.ids) : []
4609
4724
  ];
4610
4725
  const paragraph = new Paragraph3({
@@ -6595,8 +6710,8 @@ import {
6595
6710
  Paragraph as Paragraph9,
6596
6711
  TextRun as TextRun9,
6597
6712
  AlignmentType as AlignmentType5,
6598
- BookmarkStart as BookmarkStart2,
6599
- BookmarkEnd as BookmarkEnd2
6713
+ BookmarkStart as BookmarkStart3,
6714
+ BookmarkEnd as BookmarkEnd3
6600
6715
  } from "docx";
6601
6716
  init_defaults();
6602
6717
  init_styleHelpers();
@@ -6807,54 +6922,14 @@ async function renderComponentWithCache(component, theme, themeName, context, by
6807
6922
  return rendered;
6808
6923
  }
6809
6924
 
6810
- // src/components/heading.ts
6811
- function renderHeadingComponent(component, theme, themeName) {
6812
- if (!isHeadingComponent(component)) return [];
6813
- const config = component.props;
6814
- const bookmarkId = component.id || globalBookmarkRegistry.generateId(config.text, "heading");
6815
- const header = createHeading(
6816
- config.text,
6817
- config.level || 1,
6818
- theme,
6819
- themeName,
6820
- {
6821
- alignment: config.alignment,
6822
- spacing: config.spacing,
6823
- lineSpacing: config.lineSpacing,
6824
- columnBreak: config.columnBreak,
6825
- // Local font overrides
6826
- fontFamily: config.font?.family,
6827
- fontSize: config.font?.size,
6828
- fontColor: config.font?.color,
6829
- bold: config.font?.bold,
6830
- fontWeight: config.font?.fontWeight,
6831
- italic: config.font?.italic,
6832
- underline: config.font?.underline,
6833
- scale: config.font?.scale,
6834
- characterSpacing: config.font?.characterSpacing,
6835
- // Proofing: local language override + no-proof toggle + known-words list
6836
- language: config.language,
6837
- noProof: config.noProof,
6838
- noProofWords: config.noProofWords,
6839
- // Pagination control
6840
- keepNext: config.keepNext,
6841
- keepLines: config.keepLines,
6842
- // Paragraph indentation (w:ind) in twips
6843
- indent: config.indent,
6844
- // Bookmark ID for internal linking
6845
- bookmarkId,
6846
- // Tracked-change segments (rendered as native Word revisions)
6847
- revision: config.revision,
6848
- // Review comment anchored to this heading's text
6849
- comment: config.comment
6850
- }
6851
- );
6852
- return [header];
6853
- }
6854
-
6855
6925
  // src/utils/numberingConfig.ts
6856
- import { AlignmentType as AlignmentType3, convertInchesToTwip, LevelFormat } from "docx";
6857
- import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
6926
+ import {
6927
+ AlignmentType as AlignmentType3,
6928
+ convertInchesToTwip,
6929
+ LevelFormat,
6930
+ LevelSuffix
6931
+ } from "docx";
6932
+ import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
6858
6933
  var LEVEL_FORMAT_MAP = {
6859
6934
  decimal: LevelFormat.DECIMAL,
6860
6935
  upperRoman: LevelFormat.UPPER_ROMAN,
@@ -6982,12 +7057,37 @@ function createNumberingConfig(config) {
6982
7057
  levels
6983
7058
  };
6984
7059
  }
7060
+ var HEADING_NUMBERING_REFERENCE = "jto-heading-numbering";
7061
+ function createHeadingNumberingConfig() {
7062
+ const levels = [];
7063
+ for (let level = 0; level < 6; level++) {
7064
+ const text = Array.from({ length: level + 1 }, (_, i) => `%${i + 1}`).join(".") + ".";
7065
+ levels.push({
7066
+ level,
7067
+ format: LevelFormat.DECIMAL,
7068
+ text,
7069
+ alignment: AlignmentType3.LEFT,
7070
+ start: 1,
7071
+ // A space, not the default tab: a tab would push the heading text to the
7072
+ // next tab stop and misalign it against unnumbered headings.
7073
+ suffix: LevelSuffix.SPACE,
7074
+ style: {
7075
+ paragraph: { indent: { left: 0, hanging: 0 } },
7076
+ style: `Heading${level + 1}`
7077
+ }
7078
+ });
7079
+ }
7080
+ return { reference: HEADING_NUMBERING_REFERENCE, levels };
7081
+ }
7082
+ function headingNumberLabel(number) {
7083
+ return `${number}.`;
7084
+ }
6985
7085
  var NumberingRegistry = class {
6986
7086
  fallback = {
6987
7087
  configs: /* @__PURE__ */ new Map(),
6988
7088
  counter: 0
6989
7089
  };
6990
- scopes = new AsyncLocalStorage6();
7090
+ scopes = new AsyncLocalStorage7();
6991
7091
  get state() {
6992
7092
  return this.scopes.getStore() ?? this.fallback;
6993
7093
  }
@@ -7037,6 +7137,63 @@ var NumberingRegistry = class {
7037
7137
  };
7038
7138
  var globalNumberingRegistry = new NumberingRegistry();
7039
7139
 
7140
+ // src/components/heading.ts
7141
+ var MAX_HEADING_LEVEL = 6;
7142
+ function headingNumbering(numbering, level) {
7143
+ if (numbering === false) return false;
7144
+ if (numbering !== true) return void 0;
7145
+ if (!globalNumberingRegistry.has(HEADING_NUMBERING_REFERENCE)) {
7146
+ globalNumberingRegistry.register(createHeadingNumberingConfig());
7147
+ }
7148
+ const styleLevel = level >= 1 && level <= MAX_HEADING_LEVEL ? level : 1;
7149
+ return { reference: HEADING_NUMBERING_REFERENCE, level: styleLevel - 1 };
7150
+ }
7151
+ function renderHeadingComponent(component, theme, themeName) {
7152
+ if (!isHeadingComponent(component)) return [];
7153
+ const config = component.props;
7154
+ const bookmarkId = component.id || globalBookmarkRegistry.generateId(config.text, "heading");
7155
+ const header = createHeading(
7156
+ config.text,
7157
+ config.level || 1,
7158
+ theme,
7159
+ themeName,
7160
+ {
7161
+ alignment: config.alignment,
7162
+ spacing: config.spacing,
7163
+ lineSpacing: config.lineSpacing,
7164
+ columnBreak: config.columnBreak,
7165
+ // Local font overrides
7166
+ fontFamily: config.font?.family,
7167
+ fontSize: config.font?.size,
7168
+ fontColor: config.font?.color,
7169
+ bold: config.font?.bold,
7170
+ fontWeight: config.font?.fontWeight,
7171
+ italic: config.font?.italic,
7172
+ underline: config.font?.underline,
7173
+ scale: config.font?.scale,
7174
+ characterSpacing: config.font?.characterSpacing,
7175
+ // Proofing: local language override + no-proof toggle + known-words list
7176
+ language: config.language,
7177
+ noProof: config.noProof,
7178
+ noProofWords: config.noProofWords,
7179
+ // Pagination control
7180
+ keepNext: config.keepNext,
7181
+ keepLines: config.keepLines,
7182
+ // Paragraph indentation (w:ind) in twips
7183
+ indent: config.indent,
7184
+ // Bookmark ID for internal linking
7185
+ bookmarkId,
7186
+ // Auto-numbering (1., 1.1., …) through the shared heading definition
7187
+ numbering: headingNumbering(config.numbering, config.level || 1),
7188
+ // Tracked-change segments (rendered as native Word revisions)
7189
+ revision: config.revision,
7190
+ // Review comment anchored to this heading's text
7191
+ comment: config.comment
7192
+ }
7193
+ );
7194
+ return [header];
7195
+ }
7196
+
7040
7197
  // src/components/paragraph.ts
7041
7198
  function parseMarkdownList(text) {
7042
7199
  const lines = text.split("\n");
@@ -7176,8 +7333,7 @@ function renderParagraphComponent(component, theme, themeName) {
7176
7333
  return [text];
7177
7334
  }
7178
7335
 
7179
- // src/components/list.ts
7180
- init_colorUtils();
7336
+ // src/utils/listLevels.ts
7181
7337
  function createLevelsFromSimplifiedProps(props) {
7182
7338
  const levels = [];
7183
7339
  let format2;
@@ -7224,18 +7380,8 @@ function createLevelsFromSimplifiedProps(props) {
7224
7380
  }
7225
7381
  return levels;
7226
7382
  }
7227
- function resolveMarkerFonts(levels, theme) {
7228
- return levels.map((level) => {
7229
- const font = level.font;
7230
- if (!font?.color) return level;
7231
- return {
7232
- ...level,
7233
- font: { ...font, color: resolveColor(font.color, theme) }
7234
- };
7235
- });
7236
- }
7237
7383
  function applyListStart(levels, start) {
7238
- if (start === void 0) return levels;
7384
+ if (start === void 0) return [...levels];
7239
7385
  return levels.map(
7240
7386
  (level) => level.level === 0 && level.start === void 0 ? { ...level, start } : level
7241
7387
  );
@@ -7293,28 +7439,37 @@ function fillMissingLevels(levels, maxLevel) {
7293
7439
  }
7294
7440
  return result;
7295
7441
  }
7442
+ function resolveListLevels(props) {
7443
+ const maxLevel = getMaxLevelFromItems(props.items);
7444
+ if (props.levels && props.levels.length > 0) {
7445
+ return fillMissingLevels(
7446
+ applyListStart(props.levels, props.start),
7447
+ maxLevel
7448
+ );
7449
+ }
7450
+ return fillMissingLevels(createLevelsFromSimplifiedProps(props), maxLevel);
7451
+ }
7452
+
7453
+ // src/components/list.ts
7454
+ init_colorUtils();
7455
+ function resolveMarkerFonts(levels, theme) {
7456
+ return levels.map((level) => {
7457
+ const font = level.font;
7458
+ if (!font?.color) return level;
7459
+ return {
7460
+ ...level,
7461
+ font: { ...font, color: resolveColor(font.color, theme) }
7462
+ };
7463
+ });
7464
+ }
7296
7465
  function renderListComponent(component, theme, themeName) {
7297
7466
  if (!isListComponent(component)) return [];
7298
7467
  const resolvedConfig = component.props;
7299
- const maxLevel = getMaxLevelFromItems(resolvedConfig.items);
7300
7468
  const reference = resolvedConfig.reference || globalNumberingRegistry.generateReference("list");
7301
7469
  if (!globalNumberingRegistry.has(reference)) {
7302
- let levels;
7303
- if (resolvedConfig.levels && resolvedConfig.levels.length > 0) {
7304
- levels = fillMissingLevels(
7305
- applyListStart(
7306
- resolvedConfig.levels,
7307
- resolvedConfig.start
7308
- ),
7309
- maxLevel
7310
- );
7311
- } else {
7312
- const baseLevels = createLevelsFromSimplifiedProps(resolvedConfig);
7313
- levels = fillMissingLevels(baseLevels, maxLevel);
7314
- }
7315
7470
  const config = {
7316
7471
  reference,
7317
- levels: resolveMarkerFonts(levels, theme)
7472
+ levels: resolveMarkerFonts(resolveListLevels(resolvedConfig), theme)
7318
7473
  };
7319
7474
  const numberingConfig = createNumberingConfig(config);
7320
7475
  globalNumberingRegistry.register(numberingConfig);
@@ -7354,6 +7509,7 @@ async function renderImageComponent(component, theme, themeName) {
7354
7509
 
7355
7510
  // src/components/text-box.ts
7356
7511
  import {
7512
+ Paragraph as Paragraph5,
7357
7513
  Table as Table3,
7358
7514
  TableRow as TableRow2,
7359
7515
  TableCell as TableCell2,
@@ -7362,7 +7518,8 @@ import {
7362
7518
  RelativeHorizontalPosition,
7363
7519
  RelativeVerticalPosition,
7364
7520
  OverlapType,
7365
- TableLayoutType as TableLayoutType2
7521
+ TableLayoutType as TableLayoutType2,
7522
+ WpsShapeRun
7366
7523
  } from "docx";
7367
7524
 
7368
7525
  // src/styles/utils/borderUtils.ts
@@ -7439,6 +7596,8 @@ function buildCellOptions(children, styleCfg, theme) {
7439
7596
  }
7440
7597
 
7441
7598
  // src/components/text-box.ts
7599
+ init_colorUtils();
7600
+ init_docxImagePositioning();
7442
7601
  init_widthUtils();
7443
7602
  function mapTableFloatOptions(floating, theme, themeName) {
7444
7603
  if (!floating) return void 0;
@@ -7494,26 +7653,23 @@ function mapTableFloatOptions(floating, theme, themeName) {
7494
7653
  opt.overlap = OverlapType.OVERLAP;
7495
7654
  return opt;
7496
7655
  }
7497
- async function renderTextBoxComponent(component, theme, themeName, _context) {
7498
- if (!isTextBoxComponent(component)) return [];
7499
- const tb = component;
7656
+ async function renderTextBoxChildren(tb, theme, themeName, context) {
7657
+ const childContext = {
7658
+ ...context,
7659
+ parent: tb
7660
+ };
7661
+ const rendered = [];
7662
+ for (const child of tb.children || []) {
7663
+ rendered.push(
7664
+ ...await renderComponent(child, theme, themeName, childContext)
7665
+ );
7666
+ }
7667
+ return rendered;
7668
+ }
7669
+ async function renderTextBoxAsTable(tb, theme, themeName, _context, prerendered) {
7500
7670
  const isInline = !tb.props.floating;
7501
- const childComponents = tb.children || [];
7502
7671
  if (isInline) {
7503
- const cellChildren2 = [];
7504
- const childContext2 = {
7505
- ..._context,
7506
- parent: tb
7507
- };
7508
- for (const child of childComponents) {
7509
- const rendered = await renderComponent(
7510
- child,
7511
- theme,
7512
- themeName,
7513
- childContext2
7514
- );
7515
- cellChildren2.push(...rendered);
7516
- }
7672
+ const cellChildren2 = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
7517
7673
  const styleCfg2 = tb.props.style;
7518
7674
  const cellOpts2 = buildCellOptions(cellChildren2, styleCfg2, theme);
7519
7675
  const row2 = new TableRow2({ children: [new TableCell2(cellOpts2)] });
@@ -7526,20 +7682,7 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
7526
7682
  });
7527
7683
  return [table2];
7528
7684
  }
7529
- const cellChildren = [];
7530
- const childContext = {
7531
- ..._context,
7532
- parent: tb
7533
- };
7534
- for (const child of childComponents) {
7535
- const rendered = await renderComponent(
7536
- child,
7537
- theme,
7538
- themeName,
7539
- childContext
7540
- );
7541
- cellChildren.push(...rendered);
7542
- }
7685
+ const cellChildren = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
7543
7686
  const styleCfg = tb.props.style;
7544
7687
  const cellOpts = buildCellOptions(cellChildren, styleCfg, theme);
7545
7688
  const row = new TableRow2({
@@ -7571,6 +7714,162 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
7571
7714
  });
7572
7715
  return [table];
7573
7716
  }
7717
+ var PIXELS_TO_EMU = 9525;
7718
+ var TWIPS_PER_PIXEL = 15;
7719
+ function resolveShapeSize(value, axis, theme, themeName) {
7720
+ if (typeof value === "number") {
7721
+ return { pixels: Math.round(value), resolvedPercentage: false };
7722
+ }
7723
+ if (typeof value !== "string") return { resolvedPercentage: false };
7724
+ const fraction = parseFloat(value) / 100;
7725
+ if (!Number.isFinite(fraction) || fraction <= 0) {
7726
+ return { resolvedPercentage: false };
7727
+ }
7728
+ const availableTwips = axis === "width" ? getAvailableWidthTwips(theme, themeName) : getAvailableHeightTwips(theme, themeName);
7729
+ return {
7730
+ pixels: Math.round(availableTwips * fraction / TWIPS_PER_PIXEL),
7731
+ resolvedPercentage: true
7732
+ };
7733
+ }
7734
+ function shapeColor(value, theme) {
7735
+ return resolveColor(value, theme).replace(/^#/, "");
7736
+ }
7737
+ function shapeOutline(style, theme) {
7738
+ const border = style?.border;
7739
+ if (!border) return { ignoredSides: [], unsupportedStyles: [] };
7740
+ const order = [
7741
+ "top",
7742
+ "left",
7743
+ "bottom",
7744
+ "right"
7745
+ ];
7746
+ const declared = order.map((side) => [side, border[side]]).filter(
7747
+ (entry) => Boolean(entry[1])
7748
+ );
7749
+ if (declared.length === 0) return { ignoredSides: [], unsupportedStyles: [] };
7750
+ const unsupportedStyles = [
7751
+ ...new Set(
7752
+ declared.map(([, config]) => config.style).filter(
7753
+ (value) => value === "dashed" || value === "dotted" || value === "double"
7754
+ )
7755
+ )
7756
+ ];
7757
+ if (unsupportedStyles.length > 0) {
7758
+ return { ignoredSides: [], unsupportedStyles };
7759
+ }
7760
+ const [, used] = declared[0];
7761
+ const differs = ({ style: s, width, color }) => s !== used.style || width !== used.width || color !== used.color;
7762
+ const ignoredSides = declared.slice(1).filter(([, config]) => differs(config)).map(([side]) => side);
7763
+ if (used.style === "none") return { ignoredSides, unsupportedStyles };
7764
+ return {
7765
+ outline: {
7766
+ type: "solidFill",
7767
+ solidFillType: "rgb",
7768
+ value: used.color ? shapeColor(used.color, theme) : "000000",
7769
+ ...used.width !== void 0 && {
7770
+ width: Math.round(used.width * PIXELS_TO_EMU)
7771
+ }
7772
+ },
7773
+ ignoredSides,
7774
+ unsupportedStyles
7775
+ };
7776
+ }
7777
+ function shapeBodyProperties(style) {
7778
+ const padding = style?.padding;
7779
+ if (!padding) return void 0;
7780
+ const toEmu = (value) => value === void 0 ? void 0 : Math.round(value * PIXELS_TO_EMU);
7781
+ return {
7782
+ margins: {
7783
+ ...padding.top !== void 0 && { top: toEmu(padding.top) },
7784
+ ...padding.bottom !== void 0 && { bottom: toEmu(padding.bottom) },
7785
+ ...padding.left !== void 0 && { left: toEmu(padding.left) },
7786
+ ...padding.right !== void 0 && { right: toEmu(padding.right) }
7787
+ }
7788
+ };
7789
+ }
7790
+ async function renderTextBoxAsShape(tb, theme, themeName, context) {
7791
+ const width = resolveShapeSize(tb.props.width, "width", theme, themeName);
7792
+ const height = resolveShapeSize(tb.props.height, "height", theme, themeName);
7793
+ if (width.pixels === void 0 || height.pixels === void 0) {
7794
+ console.warn(
7795
+ '[core-docx] text-box renderAs "shape" needs an explicit width and height (a shape has no autofit); falling back to table rendering.'
7796
+ );
7797
+ return { kind: "fallback" };
7798
+ }
7799
+ const style = tb.props.style;
7800
+ const { outline, ignoredSides, unsupportedStyles } = shapeOutline(
7801
+ style,
7802
+ theme
7803
+ );
7804
+ if (unsupportedStyles.length > 0) {
7805
+ console.warn(
7806
+ `[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.`
7807
+ );
7808
+ return { kind: "fallback" };
7809
+ }
7810
+ const rendered = await renderTextBoxChildren(tb, theme, themeName, context);
7811
+ if (rendered.some((element) => !(element instanceof Paragraph5))) {
7812
+ console.warn(
7813
+ '[core-docx] text-box renderAs "shape" requires paragraph-only content; falling back to table rendering.'
7814
+ );
7815
+ return { kind: "fallback", rendered };
7816
+ }
7817
+ const children = rendered;
7818
+ if (width.resolvedPercentage || height.resolvedPercentage) {
7819
+ console.warn(
7820
+ '[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.'
7821
+ );
7822
+ }
7823
+ const fill = style?.shading?.fill;
7824
+ if (ignoredSides.length > 0) {
7825
+ console.warn(
7826
+ `[core-docx] text-box renderAs "shape" has one uniform outline; using the first declared border side and ignoring ${ignoredSides.join(", ")}.`
7827
+ );
7828
+ }
7829
+ const dropOutline = Boolean(fill) && Boolean(outline);
7830
+ if (dropOutline) {
7831
+ console.warn(
7832
+ '[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.'
7833
+ );
7834
+ }
7835
+ const bodyProperties = shapeBodyProperties(style);
7836
+ const run = new WpsShapeRun({
7837
+ type: "wps",
7838
+ children,
7839
+ transformation: { width: width.pixels, height: height.pixels },
7840
+ ...fill && {
7841
+ solidFill: { type: "rgb", value: shapeColor(fill, theme) }
7842
+ },
7843
+ ...outline && !dropOutline && { outline },
7844
+ ...bodyProperties && { bodyProperties },
7845
+ // Absent `floating` makes it a `wp:inline` drawing.
7846
+ ...tb.props.floating && {
7847
+ floating: mapFloatingOptions(tb.props.floating, theme, themeName)
7848
+ }
7849
+ });
7850
+ return {
7851
+ kind: "shape",
7852
+ paragraphs: [
7853
+ new Paragraph5({ children: [run], spacing: { before: 0, after: 0 } })
7854
+ ]
7855
+ };
7856
+ }
7857
+ async function renderTextBoxComponent(component, theme, themeName, context) {
7858
+ if (!isTextBoxComponent(component)) return [];
7859
+ const tb = component;
7860
+ if (tb.props.renderAs === "shape") {
7861
+ const attempt = await renderTextBoxAsShape(tb, theme, themeName, context);
7862
+ if (attempt.kind === "shape") return attempt.paragraphs;
7863
+ return renderTextBoxAsTable(
7864
+ tb,
7865
+ theme,
7866
+ themeName,
7867
+ context,
7868
+ attempt.rendered
7869
+ );
7870
+ }
7871
+ return renderTextBoxAsTable(tb, theme, themeName, context);
7872
+ }
7574
7873
 
7575
7874
  // src/components/table.ts
7576
7875
  async function renderTableComponent(component, theme, themeName) {
@@ -7621,23 +7920,23 @@ async function renderTableComponent(component, theme, themeName) {
7621
7920
  }
7622
7921
 
7623
7922
  // src/components/section.ts
7624
- import { Paragraph as Paragraph6, BookmarkStart, BookmarkEnd } from "docx";
7923
+ import { Paragraph as Paragraph6, BookmarkStart as BookmarkStart2, BookmarkEnd as BookmarkEnd2 } from "docx";
7625
7924
 
7626
7925
  // src/core/sectionBookmarks.ts
7627
- import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
7926
+ import { AsyncLocalStorage as AsyncLocalStorage8 } from "async_hooks";
7628
7927
  var NESTED_LINK_ID_BASE = 1e6;
7629
- function createState3() {
7928
+ function createState4() {
7630
7929
  return { nextNested: 1, resolved: /* @__PURE__ */ new WeakMap() };
7631
7930
  }
7632
7931
  var SectionBookmarkRegistry = class {
7633
- fallback = createState3();
7634
- scopes = new AsyncLocalStorage7();
7932
+ fallback = createState4();
7933
+ scopes = new AsyncLocalStorage8();
7635
7934
  get state() {
7636
7935
  return this.scopes.getStore() ?? this.fallback;
7637
7936
  }
7638
7937
  /** Run work with an isolated registry that follows its async call chain. */
7639
7938
  runScoped(callback) {
7640
- return this.scopes.run(createState3(), callback);
7939
+ return this.scopes.run(createState4(), callback);
7641
7940
  }
7642
7941
  /**
7643
7942
  * The bookmark for a layout section, derived from its ordinal.
@@ -7668,7 +7967,7 @@ var SectionBookmarkRegistry = class {
7668
7967
  /** Test-only: reset the unscoped fallback counters. */
7669
7968
  clear() {
7670
7969
  if (!this.scopes.getStore()) {
7671
- this.fallback = createState3();
7970
+ this.fallback = createState4();
7672
7971
  }
7673
7972
  }
7674
7973
  };
@@ -7681,7 +7980,7 @@ async function renderSectionComponent(component, theme, themeName, context) {
7681
7980
  const { id: sectionBookmarkId, linkId: bookmarkLinkId } = globalSectionBookmarkRegistry.forSectionComponent(component);
7682
7981
  elements.push(
7683
7982
  new Paragraph6({
7684
- children: [new BookmarkStart(sectionBookmarkId, bookmarkLinkId)],
7983
+ children: [new BookmarkStart2(sectionBookmarkId, bookmarkLinkId)],
7685
7984
  spacing: {
7686
7985
  before: 0,
7687
7986
  after: 0,
@@ -7709,7 +8008,7 @@ async function renderSectionComponent(component, theme, themeName, context) {
7709
8008
  }
7710
8009
  elements.push(
7711
8010
  new Paragraph6({
7712
- children: [new BookmarkEnd(bookmarkLinkId)],
8011
+ children: [new BookmarkEnd2(bookmarkLinkId)],
7713
8012
  spacing: {
7714
8013
  before: 0,
7715
8014
  after: 0,
@@ -7889,7 +8188,10 @@ function selectCachedEntries(collected, options) {
7889
8188
  continue;
7890
8189
  }
7891
8190
  if (entry.level < depthStart || entry.level > depthEnd) continue;
7892
- entries.push({ title: entry.title, level: entry.level });
8191
+ entries.push({
8192
+ title: entry.number ? `${headingNumberLabel(entry.number)} ${entry.title}` : entry.title,
8193
+ level: entry.level
8194
+ });
7893
8195
  }
7894
8196
  return entries;
7895
8197
  }
@@ -8197,6 +8499,61 @@ function computeSectionOrdinals(sections) {
8197
8499
 
8198
8500
  // src/core/collectTocHeadings.ts
8199
8501
  import { getStandardComponent } from "@json-to-office/shared-docx";
8502
+
8503
+ // src/utils/numberFormatting.ts
8504
+ var ROMAN_NUMERALS = [
8505
+ [1e3, "m"],
8506
+ [900, "cm"],
8507
+ [500, "d"],
8508
+ [400, "cd"],
8509
+ [100, "c"],
8510
+ [90, "xc"],
8511
+ [50, "l"],
8512
+ [40, "xl"],
8513
+ [10, "x"],
8514
+ [9, "ix"],
8515
+ [5, "v"],
8516
+ [4, "iv"],
8517
+ [1, "i"]
8518
+ ];
8519
+ function toRoman(value) {
8520
+ let remaining = value;
8521
+ let out = "";
8522
+ for (const [amount, glyph] of ROMAN_NUMERALS) {
8523
+ while (remaining >= amount) {
8524
+ out += glyph;
8525
+ remaining -= amount;
8526
+ }
8527
+ }
8528
+ return out;
8529
+ }
8530
+ function toLetters(value) {
8531
+ const index = (value - 1) % 26;
8532
+ const repeats = Math.floor((value - 1) / 26) + 1;
8533
+ return String.fromCharCode(97 + index).repeat(repeats);
8534
+ }
8535
+ function formatNumberForLevel(value, format2) {
8536
+ if (!Number.isFinite(value) || value < 1) return void 0;
8537
+ const n = Math.floor(value);
8538
+ switch (format2) {
8539
+ case "decimal":
8540
+ return String(n);
8541
+ case "lowerLetter":
8542
+ return toLetters(n);
8543
+ case "upperLetter":
8544
+ return toLetters(n).toUpperCase();
8545
+ case "lowerRoman":
8546
+ return toRoman(n);
8547
+ case "upperRoman":
8548
+ return toRoman(n).toUpperCase();
8549
+ default:
8550
+ return void 0;
8551
+ }
8552
+ }
8553
+
8554
+ // src/core/collectTocHeadings.ts
8555
+ var MAX_HEADING_LEVEL2 = 6;
8556
+ var MAX_LIST_LEVEL = 9;
8200
8557
  function normalizeEntryTitle(text) {
8201
8558
  return normalizeUnicodeText(text).replace(/(\*\*\*|___)([\s\S]*?)\1/g, "$2").replace(/(\*\*|__)([\s\S]*?)\1/g, "$2").replace(/(\*|_)([\s\S]*?)\1/g, "$2").trim();
8202
8559
  }
@@ -8215,9 +8572,97 @@ function styleEntryKey(props) {
8215
8572
  }
8216
8573
  return themeStyle;
8217
8574
  }
8218
- function collectTocHeadings(sections) {
8575
+ function levelStart(levels, level) {
8576
+ return levels[level]?.start ?? 1;
8577
+ }
8578
+ function collectDocumentOutline(sections) {
8219
8579
  const entries = [];
8580
+ const numberedItems = /* @__PURE__ */ new Map();
8220
8581
  const ordinals = computeSectionOrdinals(sections);
8582
+ const takenIds = /* @__PURE__ */ new Set();
8583
+ const headingCounters = new Array(MAX_HEADING_LEVEL2).fill(0);
8584
+ const listCounters = /* @__PURE__ */ new Map();
8585
+ const visitHeading = (component, props, sectionBookmarkId) => {
8586
+ const text = typeof props.text === "string" ? props.text : "";
8587
+ const title = normalizeEntryTitle(text);
8588
+ const level = typeof props.level === "number" ? props.level : 1;
8589
+ const styleLevel = level >= 1 && level <= MAX_HEADING_LEVEL2 ? level : 1;
8590
+ let full;
8591
+ let own;
8592
+ if (props.numbering === true) {
8593
+ headingCounters[styleLevel - 1] += 1;
8594
+ for (let deeper = styleLevel; deeper < MAX_HEADING_LEVEL2; deeper++) {
8595
+ headingCounters[deeper] = 0;
8596
+ }
8597
+ full = headingCounters.slice(0, styleLevel).join(".");
8598
+ own = String(headingCounters[styleLevel - 1]);
8599
+ }
8600
+ const explicitId = component.id;
8601
+ const bookmarkId = typeof explicitId === "string" && explicitId ? explicitId : dedupeBookmarkId(slugifyBookmarkText(text), (id) => takenIds.has(id));
8602
+ takenIds.add(bookmarkId);
8603
+ numberedItems.set(bookmarkId, {
8604
+ kind: "heading",
8605
+ text: title,
8606
+ ...full !== void 0 && { full, own }
8607
+ });
8608
+ if (title) {
8609
+ entries.push({
8610
+ title,
8611
+ level,
8612
+ sectionBookmarkId,
8613
+ ...full !== void 0 && { number: full }
8614
+ });
8615
+ }
8616
+ };
8617
+ const visitList = (props) => {
8618
+ const items = Array.isArray(props.items) ? props.items : [];
8619
+ if (items.length === 0) return;
8620
+ const reference = typeof props.reference === "string" && props.reference ? props.reference : void 0;
8621
+ const freshState = () => {
8622
+ const levels = resolveListLevels(
8623
+ props
8624
+ );
8625
+ return {
8626
+ levels,
8627
+ counters: Array.from(
8628
+ { length: MAX_LIST_LEVEL },
8629
+ (_, level) => levelStart(levels, level) - 1
8630
+ )
8631
+ };
8632
+ };
8633
+ let state;
8634
+ if (reference === void 0) {
8635
+ state = freshState();
8636
+ } else {
8637
+ state = listCounters.get(reference) ?? freshState();
8638
+ listCounters.set(reference, state);
8639
+ }
8640
+ for (const item of items) {
8641
+ const isObject = typeof item === "object" && item !== null;
8642
+ const raw = isObject ? item.text : item;
8643
+ const text = typeof raw === "string" ? raw : "";
8644
+ if (!text.trim() && !(isObject && item.revision)) continue;
8645
+ const rawLevel = isObject ? item.level : void 0;
8646
+ const level = typeof rawLevel === "number" && rawLevel >= 0 ? rawLevel : 0;
8647
+ if (level >= MAX_LIST_LEVEL) continue;
8648
+ state.counters[level] += 1;
8649
+ for (let deeper = level + 1; deeper < MAX_LIST_LEVEL; deeper++) {
8650
+ state.counters[deeper] = levelStart(state.levels, deeper) - 1;
8651
+ }
8652
+ const id = isObject ? item.id : void 0;
8653
+ if (typeof id !== "string" || !id) continue;
8654
+ takenIds.add(id);
8655
+ const number = formatNumberForLevel(
8656
+ state.counters[level],
8657
+ state.levels[level]?.format
8658
+ );
8659
+ numberedItems.set(id, {
8660
+ kind: "list-item",
8661
+ text: normalizeUnicodeText(text).trim(),
8662
+ ...number !== void 0 && { full: number, own: number }
8663
+ });
8664
+ }
8665
+ };
8221
8666
  sections.forEach((section, index) => {
8222
8667
  const ordinal = ordinals[index]?.ordinal;
8223
8668
  const sectionBookmarkId = ordinal ? globalSectionBookmarkRegistry.forLayoutSection(ordinal).id : void 0;
@@ -8225,15 +8670,11 @@ function collectTocHeadings(sections) {
8225
8670
  if (!isEnabled(component)) return;
8226
8671
  const props = component.props ?? {};
8227
8672
  if (component.name === "heading") {
8228
- const text = typeof props.text === "string" ? props.text : "";
8229
- const title = normalizeEntryTitle(text);
8230
- if (title) {
8231
- const level = typeof props.level === "number" ? props.level : 1;
8232
- entries.push({ title, level, sectionBookmarkId });
8233
- }
8673
+ visitHeading(component, props, sectionBookmarkId);
8234
8674
  return;
8235
8675
  }
8236
8676
  if (component.name === "paragraph") {
8677
+ if (typeof props.id === "string" && props.id) takenIds.add(props.id);
8237
8678
  const styleId = styleEntryKey(props);
8238
8679
  const text = typeof props.text === "string" ? props.text : "";
8239
8680
  const title = normalizeEntryTitle(text);
@@ -8242,13 +8683,17 @@ function collectTocHeadings(sections) {
8242
8683
  }
8243
8684
  return;
8244
8685
  }
8686
+ if (component.name === "list") {
8687
+ visitList(props);
8688
+ return;
8689
+ }
8245
8690
  if (!isContainer(component.name)) return;
8246
8691
  const children = component.children;
8247
8692
  if (Array.isArray(children)) children.forEach(visit);
8248
8693
  };
8249
8694
  section.components.forEach(visit);
8250
8695
  });
8251
- return entries;
8696
+ return { entries, numberedItems };
8252
8697
  }
8253
8698
 
8254
8699
  // src/core/render.ts
@@ -8301,7 +8746,13 @@ async function renderDocument(structure, layout, options) {
8301
8746
  // Footnote ids are document-scoped too: a reference resolved
8302
8747
  // against another render's counter points at the wrong body.
8303
8748
  globalNoteRegistry.runScoped(
8304
- () => renderDocumentScoped(structure, layout, options)
8749
+ () => (
8750
+ // Cross-reference targets are keyed by bookmark id, which
8751
+ // is only unique within one document.
8752
+ globalNumberedItemsRegistry.runScoped(
8753
+ () => renderDocumentScoped(structure, layout, options)
8754
+ )
8755
+ )
8305
8756
  )
8306
8757
  )
8307
8758
  )
@@ -8344,13 +8795,14 @@ async function renderDocumentScoped(structure, layout, options) {
8344
8795
  );
8345
8796
  }
8346
8797
  try {
8347
- const tocHeadings = collectTocHeadings(layout.sections);
8348
- if (tocHeadings.length > 0) {
8349
- context.tocHeadings = tocHeadings;
8798
+ const outline = collectDocumentOutline(layout.sections);
8799
+ if (outline.entries.length > 0) {
8800
+ context.tocHeadings = outline.entries;
8350
8801
  }
8802
+ globalNumberedItemsRegistry.seed(outline.numberedItems);
8351
8803
  } catch (error) {
8352
8804
  console.warn(
8353
- "[core-docx] TOC entry collection failed; the TOC field will rely on the reader refreshing it:",
8805
+ "[core-docx] Document outline collection failed; the TOC field will rely on the reader refreshing it:",
8354
8806
  error instanceof Error ? error.message : error
8355
8807
  );
8356
8808
  }
@@ -8593,7 +9045,7 @@ async function renderSection(section, theme, themeName, context, sectionOrdinal,
8593
9045
  if (sectionBookmarkId && isFirstLayoutOfUserSection && sharedLinkId !== void 0) {
8594
9046
  elements.push(
8595
9047
  new Paragraph9({
8596
- children: [new BookmarkStart2(sectionBookmarkId, sharedLinkId)],
9048
+ children: [new BookmarkStart3(sectionBookmarkId, sharedLinkId)],
8597
9049
  spacing: {
8598
9050
  before: 0,
8599
9051
  after: 0,
@@ -8618,7 +9070,7 @@ async function renderSection(section, theme, themeName, context, sectionOrdinal,
8618
9070
  if (closeBookmark && sharedLinkId !== void 0) {
8619
9071
  elements.push(
8620
9072
  new Paragraph9({
8621
- children: [new BookmarkEnd2(sharedLinkId)]
9073
+ children: [new BookmarkEnd3(sharedLinkId)]
8622
9074
  })
8623
9075
  );
8624
9076
  }