@json-to-office/core-docx 0.36.1 → 0.37.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 +605 -160
  15. package/dist/index.js.map +1 -1
  16. package/dist/plugin/example/index.js +603 -160
  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 +14 -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 +3 -3
@@ -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
  };
@@ -2458,6 +2446,8 @@ import {
2458
2446
  InternalHyperlink as InternalHyperlink2,
2459
2447
  FootnoteReferenceRun as FootnoteReferenceRun2,
2460
2448
  EndnoteReferenceRun as EndnoteReferenceRun2,
2449
+ NumberedItemReference,
2450
+ NumberedItemReferenceFormat,
2461
2451
  Tab
2462
2452
  } from "docx";
2463
2453
 
@@ -2658,6 +2648,45 @@ function initializeBuiltinPlaceholders() {
2658
2648
  }
2659
2649
  initializeBuiltinPlaceholders();
2660
2650
 
2651
+ // src/utils/numberedItemsRegistry.ts
2652
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
2653
+ var NumberedItemsRegistry = class {
2654
+ fallback = {
2655
+ items: /* @__PURE__ */ new Map(),
2656
+ seeded: false
2657
+ };
2658
+ scopes = new AsyncLocalStorage2();
2659
+ get state() {
2660
+ return this.scopes.getStore() ?? this.fallback;
2661
+ }
2662
+ /** Run work with an isolated registry that follows its async call chain. */
2663
+ runScoped(callback) {
2664
+ return this.scopes.run({ items: /* @__PURE__ */ new Map(), seeded: false }, callback);
2665
+ }
2666
+ /** Replace the contents with the pre-pass result. */
2667
+ seed(items) {
2668
+ const state = this.state;
2669
+ state.items = new Map(items);
2670
+ state.seeded = true;
2671
+ }
2672
+ /**
2673
+ * False outside a render (a unit test calling `createText` directly). An
2674
+ * unresolved reference is then expected rather than an authoring mistake, so
2675
+ * the caller stays quiet about it.
2676
+ */
2677
+ isSeeded() {
2678
+ return this.state.seeded;
2679
+ }
2680
+ get(id) {
2681
+ return this.state.items.get(id);
2682
+ }
2683
+ clear() {
2684
+ this.state.items.clear();
2685
+ this.state.seeded = false;
2686
+ }
2687
+ };
2688
+ var globalNumberedItemsRegistry = new NumberedItemsRegistry();
2689
+
2661
2690
  // src/utils/textParser.ts
2662
2691
  var FOOTNOTE_MARKER_REGEX = /\[\^([^\]\s]+)\]/;
2663
2692
  function escapeRegExp(s) {
@@ -2874,13 +2903,59 @@ function splitNoteMarkers(text, noteRef, runs) {
2874
2903
  if (trailing) out.push(...runs(trailing));
2875
2904
  return out;
2876
2905
  }
2906
+ var CROSS_REFERENCE_PATTERN = "\\[@([^\\]\\s:]+)(?::(relative|no_context|full_context|none))?\\]";
2907
+ var INLINE_TOKEN_REGEX = `\\[([^\\]]+)\\]\\(([^)]+)\\)|${CROSS_REFERENCE_PATTERN}`;
2908
+ function hasCrossReference(text) {
2909
+ return new RegExp(CROSS_REFERENCE_PATTERN).test(text);
2910
+ }
2911
+ var REFERENCE_FORMATS = {
2912
+ relative: NumberedItemReferenceFormat.RELATIVE,
2913
+ no_context: NumberedItemReferenceFormat.NO_CONTEXT,
2914
+ full_context: NumberedItemReferenceFormat.FULL_CONTEXT,
2915
+ none: NumberedItemReferenceFormat.NONE
2916
+ };
2917
+ function createCrossReference(id, format2, token, baseStyle, options) {
2918
+ const info = globalNumberedItemsRegistry.get(id);
2919
+ if (!info) {
2920
+ if (globalNumberedItemsRegistry.isSeeded()) {
2921
+ console.warn(
2922
+ `[core-docx] Cross-reference ${token} has no target: no heading or list item declares the id "${id}". Rendering the token as literal text.`
2923
+ );
2924
+ }
2925
+ return buildTextRuns(
2926
+ token,
2927
+ buildRunCommonProps(baseStyle, { boldColor: options.boldColor }),
2928
+ { noProof: baseStyle.noProof, noProofWords: options.noProofWords }
2929
+ );
2930
+ }
2931
+ if (format2 === "none") {
2932
+ return [
2933
+ new NumberedItemReference(id, info.text, {
2934
+ hyperlink: true,
2935
+ referenceFormat: REFERENCE_FORMATS.none
2936
+ })
2937
+ ];
2938
+ }
2939
+ const cachedValue = format2 === "no_context" ? info.own : info.full;
2940
+ if (cachedValue === void 0) {
2941
+ console.warn(
2942
+ `[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.`
2943
+ );
2944
+ }
2945
+ return [
2946
+ new NumberedItemReference(id, cachedValue, {
2947
+ hyperlink: true,
2948
+ referenceFormat: REFERENCE_FORMATS[format2]
2949
+ })
2950
+ ];
2951
+ }
2877
2952
  function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
2878
2953
  const normalizedText = normalizeUnicodeText(text);
2879
2954
  const runs = [];
2880
- const hyperlinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
2955
+ const tokenRegex = new RegExp(INLINE_TOKEN_REGEX, "g");
2881
2956
  let lastIndex = 0;
2882
2957
  let match;
2883
- while ((match = hyperlinkRegex.exec(normalizedText)) !== null) {
2958
+ while ((match = tokenRegex.exec(normalizedText)) !== null) {
2884
2959
  if (match.index > lastIndex) {
2885
2960
  const plainText = normalizedText.substring(lastIndex, match.index);
2886
2961
  if (plainText) {
@@ -2892,6 +2967,19 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
2892
2967
  runs.push(...plainRuns);
2893
2968
  }
2894
2969
  }
2970
+ lastIndex = match.index + match[0].length;
2971
+ if (match[1] === void 0) {
2972
+ runs.push(
2973
+ ...createCrossReference(
2974
+ match[3],
2975
+ match[4] ?? "relative",
2976
+ match[0],
2977
+ baseStyle,
2978
+ options
2979
+ )
2980
+ );
2981
+ continue;
2982
+ }
2895
2983
  const linkText = match[1];
2896
2984
  const linkUrl = match[2];
2897
2985
  const isInternal = linkUrl.startsWith("#");
@@ -2918,7 +3006,6 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
2918
3006
  })
2919
3007
  );
2920
3008
  }
2921
- lastIndex = match.index + match[0].length;
2922
3009
  }
2923
3010
  if (lastIndex < normalizedText.length) {
2924
3011
  const remainingText = normalizedText.substring(lastIndex);
@@ -3612,10 +3699,21 @@ function getStyleIdForLevel(level) {
3612
3699
  }
3613
3700
 
3614
3701
  // src/utils/bookmarkRegistry.ts
3615
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
3702
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3703
+ function slugifyBookmarkText(text) {
3704
+ return text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").substring(0, 40);
3705
+ }
3706
+ function dedupeBookmarkId(base, taken) {
3707
+ let id = base;
3708
+ let attempt = 0;
3709
+ while (taken(id) && attempt < 100) {
3710
+ id = `${base}-${++attempt}`;
3711
+ }
3712
+ return id;
3713
+ }
3616
3714
  var BookmarkRegistry = class {
3617
3715
  fallback = { bookmarks: /* @__PURE__ */ new Map() };
3618
- scopes = new AsyncLocalStorage2();
3716
+ scopes = new AsyncLocalStorage3();
3619
3717
  get state() {
3620
3718
  return this.scopes.getStore() ?? this.fallback;
3621
3719
  }
@@ -3639,13 +3737,10 @@ var BookmarkRegistry = class {
3639
3737
  * Converts text to a URL-friendly format
3640
3738
  */
3641
3739
  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;
3740
+ return dedupeBookmarkId(
3741
+ slugifyBookmarkText(text),
3742
+ (id) => this.state.bookmarks.has(id)
3743
+ );
3649
3744
  }
3650
3745
  /**
3651
3746
  * Check if a bookmark exists
@@ -3689,7 +3784,7 @@ var globalBookmarkRegistry = new BookmarkRegistry();
3689
3784
 
3690
3785
  // src/utils/revisionUtils.ts
3691
3786
  import { TextRun as TextRun3, InsertedTextRun, DeletedTextRun } from "docx";
3692
- import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3787
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
3693
3788
 
3694
3789
  // src/utils/componentAnnotations.ts
3695
3790
  function isRecord(value) {
@@ -3740,7 +3835,7 @@ var DEFAULT_REVISION_AUTHOR = "json-to-office";
3740
3835
  var DEFAULT_REVISION_DATE = "1970-01-01T00:00:00Z";
3741
3836
  var RevisionIdRegistry = class {
3742
3837
  fallbackCounter = 0;
3743
- scopes = new AsyncLocalStorage3();
3838
+ scopes = new AsyncLocalStorage4();
3744
3839
  runScoped(callback) {
3745
3840
  return this.scopes.run({ counter: 0 }, callback);
3746
3841
  }
@@ -3851,7 +3946,7 @@ import {
3851
3946
 
3852
3947
  // src/utils/commentRegistry.ts
3853
3948
  import { Paragraph, TextRun as TextRun4 } from "docx";
3854
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
3949
+ import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
3855
3950
  var DEFAULT_COMMENT_AUTHOR = "json-to-office";
3856
3951
  var DEFAULT_COMMENT_DATE = "1970-01-01T00:00:00Z";
3857
3952
  function createState() {
@@ -3866,7 +3961,7 @@ function bodyParagraphs(text) {
3866
3961
  }
3867
3962
  var CommentRegistry = class {
3868
3963
  fallback = createState();
3869
- scopes = new AsyncLocalStorage4();
3964
+ scopes = new AsyncLocalStorage5();
3870
3965
  get state() {
3871
3966
  return this.scopes.getStore() ?? this.fallback;
3872
3967
  }
@@ -3961,7 +4056,7 @@ function closeCommentRange(ids) {
3961
4056
 
3962
4057
  // src/utils/noteRegistry.ts
3963
4058
  import { Paragraph as Paragraph2, TextRun as TextRun6 } from "docx";
3964
- import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4059
+ import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
3965
4060
  function createState2() {
3966
4061
  return {
3967
4062
  footnoteCounter: 0,
@@ -3980,7 +4075,7 @@ function bodyParagraphs2(text, style) {
3980
4075
  }
3981
4076
  var NoteRegistry = class {
3982
4077
  fallback = createState2();
3983
- scopes = new AsyncLocalStorage5();
4078
+ scopes = new AsyncLocalStorage6();
3984
4079
  get state() {
3985
4080
  return this.scopes.getStore() ?? this.fallback;
3986
4081
  }
@@ -4336,7 +4431,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4336
4431
  if (commentAnchor) {
4337
4432
  children.push(...commentAnchor.start);
4338
4433
  }
4339
- const hasDecorators = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText);
4434
+ const hasInlineSyntax = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText) || hasCrossReference(normalizedText);
4340
4435
  const headingHasWeightRequest = options.fontWeight != null || options.bold === true;
4341
4436
  const headingEffectiveFamily = options.fontFamily ?? (headingHasWeightRequest ? resolveFontFamily(theme, "heading") : void 0);
4342
4437
  const headingWeighted = applyFontWeightAlias({
@@ -4403,7 +4498,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4403
4498
  "heading"
4404
4499
  );
4405
4500
  const headingTextChildren = [];
4406
- if (hasDecorators) {
4501
+ if (hasInlineSyntax) {
4407
4502
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
4408
4503
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
4409
4504
  enableHyperlinks: true,
@@ -4420,7 +4515,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4420
4515
  })
4421
4516
  );
4422
4517
  } else {
4423
- if (hasDecorators) {
4518
+ if (hasInlineSyntax) {
4424
4519
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
4425
4520
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
4426
4521
  enableHyperlinks: true,
@@ -4442,7 +4537,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4442
4537
  spacing: hasExplicitSpacing ? spacing : void 0,
4443
4538
  ...options.keepNext !== void 0 && { keepNext: options.keepNext },
4444
4539
  ...options.keepLines !== void 0 && { keepLines: options.keepLines },
4445
- ...options.indent && { indent: options.indent }
4540
+ ...options.indent && { indent: options.indent },
4541
+ // docx creates the concrete numbering instance for the reference itself,
4542
+ // in ParagraphProperties.prepForXml.
4543
+ ...options.numbering !== void 0 && { numbering: options.numbering }
4446
4544
  });
4447
4545
  }
4448
4546
  async function createImage(path3, theme, themeName, options = {}) {
@@ -4602,9 +4700,17 @@ function createList(items, _theme, _themeName, options = {}) {
4602
4700
  } else if (options.spacing?.item) {
4603
4701
  spacing.after = pointsToTwips(options.spacing.item);
4604
4702
  }
4703
+ const itemId = typeof item === "object" ? item.id : void 0;
4704
+ let itemContent = textRuns;
4705
+ if (itemId) {
4706
+ globalBookmarkRegistry.register(itemId, itemText, "list-item");
4707
+ itemContent = [
4708
+ new Bookmark({ id: itemId, children: textRuns })
4709
+ ];
4710
+ }
4605
4711
  const paragraphChildren = [
4606
4712
  ...commentAnchor && index === firstRendered ? commentAnchor.start : [],
4607
- ...textRuns,
4713
+ ...itemContent,
4608
4714
  ...commentAnchor && index === lastRendered ? closeCommentRange(commentAnchor.ids) : []
4609
4715
  ];
4610
4716
  const paragraph = new Paragraph3({
@@ -6807,54 +6913,14 @@ async function renderComponentWithCache(component, theme, themeName, context, by
6807
6913
  return rendered;
6808
6914
  }
6809
6915
 
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
6916
  // src/utils/numberingConfig.ts
6856
- import { AlignmentType as AlignmentType3, convertInchesToTwip, LevelFormat } from "docx";
6857
- import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
6917
+ import {
6918
+ AlignmentType as AlignmentType3,
6919
+ convertInchesToTwip,
6920
+ LevelFormat,
6921
+ LevelSuffix
6922
+ } from "docx";
6923
+ import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
6858
6924
  var LEVEL_FORMAT_MAP = {
6859
6925
  decimal: LevelFormat.DECIMAL,
6860
6926
  upperRoman: LevelFormat.UPPER_ROMAN,
@@ -6982,12 +7048,37 @@ function createNumberingConfig(config) {
6982
7048
  levels
6983
7049
  };
6984
7050
  }
7051
+ var HEADING_NUMBERING_REFERENCE = "jto-heading-numbering";
7052
+ function createHeadingNumberingConfig() {
7053
+ const levels = [];
7054
+ for (let level = 0; level < 6; level++) {
7055
+ const text = Array.from({ length: level + 1 }, (_, i) => `%${i + 1}`).join(".") + ".";
7056
+ levels.push({
7057
+ level,
7058
+ format: LevelFormat.DECIMAL,
7059
+ text,
7060
+ alignment: AlignmentType3.LEFT,
7061
+ start: 1,
7062
+ // A space, not the default tab: a tab would push the heading text to the
7063
+ // next tab stop and misalign it against unnumbered headings.
7064
+ suffix: LevelSuffix.SPACE,
7065
+ style: {
7066
+ paragraph: { indent: { left: 0, hanging: 0 } },
7067
+ style: `Heading${level + 1}`
7068
+ }
7069
+ });
7070
+ }
7071
+ return { reference: HEADING_NUMBERING_REFERENCE, levels };
7072
+ }
7073
+ function headingNumberLabel(number) {
7074
+ return `${number}.`;
7075
+ }
6985
7076
  var NumberingRegistry = class {
6986
7077
  fallback = {
6987
7078
  configs: /* @__PURE__ */ new Map(),
6988
7079
  counter: 0
6989
7080
  };
6990
- scopes = new AsyncLocalStorage6();
7081
+ scopes = new AsyncLocalStorage7();
6991
7082
  get state() {
6992
7083
  return this.scopes.getStore() ?? this.fallback;
6993
7084
  }
@@ -7037,6 +7128,63 @@ var NumberingRegistry = class {
7037
7128
  };
7038
7129
  var globalNumberingRegistry = new NumberingRegistry();
7039
7130
 
7131
+ // src/components/heading.ts
7132
+ var MAX_HEADING_LEVEL = 6;
7133
+ function headingNumbering(numbering, level) {
7134
+ if (numbering === false) return false;
7135
+ if (numbering !== true) return void 0;
7136
+ if (!globalNumberingRegistry.has(HEADING_NUMBERING_REFERENCE)) {
7137
+ globalNumberingRegistry.register(createHeadingNumberingConfig());
7138
+ }
7139
+ const styleLevel = level >= 1 && level <= MAX_HEADING_LEVEL ? level : 1;
7140
+ return { reference: HEADING_NUMBERING_REFERENCE, level: styleLevel - 1 };
7141
+ }
7142
+ function renderHeadingComponent(component, theme, themeName) {
7143
+ if (!isHeadingComponent(component)) return [];
7144
+ const config = component.props;
7145
+ const bookmarkId = component.id || globalBookmarkRegistry.generateId(config.text, "heading");
7146
+ const header = createHeading(
7147
+ config.text,
7148
+ config.level || 1,
7149
+ theme,
7150
+ themeName,
7151
+ {
7152
+ alignment: config.alignment,
7153
+ spacing: config.spacing,
7154
+ lineSpacing: config.lineSpacing,
7155
+ columnBreak: config.columnBreak,
7156
+ // Local font overrides
7157
+ fontFamily: config.font?.family,
7158
+ fontSize: config.font?.size,
7159
+ fontColor: config.font?.color,
7160
+ bold: config.font?.bold,
7161
+ fontWeight: config.font?.fontWeight,
7162
+ italic: config.font?.italic,
7163
+ underline: config.font?.underline,
7164
+ scale: config.font?.scale,
7165
+ characterSpacing: config.font?.characterSpacing,
7166
+ // Proofing: local language override + no-proof toggle + known-words list
7167
+ language: config.language,
7168
+ noProof: config.noProof,
7169
+ noProofWords: config.noProofWords,
7170
+ // Pagination control
7171
+ keepNext: config.keepNext,
7172
+ keepLines: config.keepLines,
7173
+ // Paragraph indentation (w:ind) in twips
7174
+ indent: config.indent,
7175
+ // Bookmark ID for internal linking
7176
+ bookmarkId,
7177
+ // Auto-numbering (1., 1.1., …) through the shared heading definition
7178
+ numbering: headingNumbering(config.numbering, config.level || 1),
7179
+ // Tracked-change segments (rendered as native Word revisions)
7180
+ revision: config.revision,
7181
+ // Review comment anchored to this heading's text
7182
+ comment: config.comment
7183
+ }
7184
+ );
7185
+ return [header];
7186
+ }
7187
+
7040
7188
  // src/components/paragraph.ts
7041
7189
  function parseMarkdownList(text) {
7042
7190
  const lines = text.split("\n");
@@ -7176,8 +7324,7 @@ function renderParagraphComponent(component, theme, themeName) {
7176
7324
  return [text];
7177
7325
  }
7178
7326
 
7179
- // src/components/list.ts
7180
- init_colorUtils();
7327
+ // src/utils/listLevels.ts
7181
7328
  function createLevelsFromSimplifiedProps(props) {
7182
7329
  const levels = [];
7183
7330
  let format2;
@@ -7224,18 +7371,8 @@ function createLevelsFromSimplifiedProps(props) {
7224
7371
  }
7225
7372
  return levels;
7226
7373
  }
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
7374
  function applyListStart(levels, start) {
7238
- if (start === void 0) return levels;
7375
+ if (start === void 0) return [...levels];
7239
7376
  return levels.map(
7240
7377
  (level) => level.level === 0 && level.start === void 0 ? { ...level, start } : level
7241
7378
  );
@@ -7293,28 +7430,37 @@ function fillMissingLevels(levels, maxLevel) {
7293
7430
  }
7294
7431
  return result;
7295
7432
  }
7433
+ function resolveListLevels(props) {
7434
+ const maxLevel = getMaxLevelFromItems(props.items);
7435
+ if (props.levels && props.levels.length > 0) {
7436
+ return fillMissingLevels(
7437
+ applyListStart(props.levels, props.start),
7438
+ maxLevel
7439
+ );
7440
+ }
7441
+ return fillMissingLevels(createLevelsFromSimplifiedProps(props), maxLevel);
7442
+ }
7443
+
7444
+ // src/components/list.ts
7445
+ init_colorUtils();
7446
+ function resolveMarkerFonts(levels, theme) {
7447
+ return levels.map((level) => {
7448
+ const font = level.font;
7449
+ if (!font?.color) return level;
7450
+ return {
7451
+ ...level,
7452
+ font: { ...font, color: resolveColor(font.color, theme) }
7453
+ };
7454
+ });
7455
+ }
7296
7456
  function renderListComponent(component, theme, themeName) {
7297
7457
  if (!isListComponent(component)) return [];
7298
7458
  const resolvedConfig = component.props;
7299
- const maxLevel = getMaxLevelFromItems(resolvedConfig.items);
7300
7459
  const reference = resolvedConfig.reference || globalNumberingRegistry.generateReference("list");
7301
7460
  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
7461
  const config = {
7316
7462
  reference,
7317
- levels: resolveMarkerFonts(levels, theme)
7463
+ levels: resolveMarkerFonts(resolveListLevels(resolvedConfig), theme)
7318
7464
  };
7319
7465
  const numberingConfig = createNumberingConfig(config);
7320
7466
  globalNumberingRegistry.register(numberingConfig);
@@ -7354,6 +7500,7 @@ async function renderImageComponent(component, theme, themeName) {
7354
7500
 
7355
7501
  // src/components/text-box.ts
7356
7502
  import {
7503
+ Paragraph as Paragraph5,
7357
7504
  Table as Table3,
7358
7505
  TableRow as TableRow2,
7359
7506
  TableCell as TableCell2,
@@ -7362,7 +7509,8 @@ import {
7362
7509
  RelativeHorizontalPosition,
7363
7510
  RelativeVerticalPosition,
7364
7511
  OverlapType,
7365
- TableLayoutType as TableLayoutType2
7512
+ TableLayoutType as TableLayoutType2,
7513
+ WpsShapeRun
7366
7514
  } from "docx";
7367
7515
 
7368
7516
  // src/styles/utils/borderUtils.ts
@@ -7439,6 +7587,8 @@ function buildCellOptions(children, styleCfg, theme) {
7439
7587
  }
7440
7588
 
7441
7589
  // src/components/text-box.ts
7590
+ init_colorUtils();
7591
+ init_docxImagePositioning();
7442
7592
  init_widthUtils();
7443
7593
  function mapTableFloatOptions(floating, theme, themeName) {
7444
7594
  if (!floating) return void 0;
@@ -7494,26 +7644,23 @@ function mapTableFloatOptions(floating, theme, themeName) {
7494
7644
  opt.overlap = OverlapType.OVERLAP;
7495
7645
  return opt;
7496
7646
  }
7497
- async function renderTextBoxComponent(component, theme, themeName, _context) {
7498
- if (!isTextBoxComponent(component)) return [];
7499
- const tb = component;
7647
+ async function renderTextBoxChildren(tb, theme, themeName, context) {
7648
+ const childContext = {
7649
+ ...context,
7650
+ parent: tb
7651
+ };
7652
+ const rendered = [];
7653
+ for (const child of tb.children || []) {
7654
+ rendered.push(
7655
+ ...await renderComponent(child, theme, themeName, childContext)
7656
+ );
7657
+ }
7658
+ return rendered;
7659
+ }
7660
+ async function renderTextBoxAsTable(tb, theme, themeName, _context, prerendered) {
7500
7661
  const isInline = !tb.props.floating;
7501
- const childComponents = tb.children || [];
7502
7662
  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
- }
7663
+ const cellChildren2 = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
7517
7664
  const styleCfg2 = tb.props.style;
7518
7665
  const cellOpts2 = buildCellOptions(cellChildren2, styleCfg2, theme);
7519
7666
  const row2 = new TableRow2({ children: [new TableCell2(cellOpts2)] });
@@ -7526,20 +7673,7 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
7526
7673
  });
7527
7674
  return [table2];
7528
7675
  }
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
- }
7676
+ const cellChildren = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
7543
7677
  const styleCfg = tb.props.style;
7544
7678
  const cellOpts = buildCellOptions(cellChildren, styleCfg, theme);
7545
7679
  const row = new TableRow2({
@@ -7571,6 +7705,162 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
7571
7705
  });
7572
7706
  return [table];
7573
7707
  }
7708
+ var PIXELS_TO_EMU = 9525;
7709
+ var TWIPS_PER_PIXEL = 15;
7710
+ function resolveShapeSize(value, axis, theme, themeName) {
7711
+ if (typeof value === "number") {
7712
+ return { pixels: Math.round(value), resolvedPercentage: false };
7713
+ }
7714
+ if (typeof value !== "string") return { resolvedPercentage: false };
7715
+ const fraction = parseFloat(value) / 100;
7716
+ if (!Number.isFinite(fraction) || fraction <= 0) {
7717
+ return { resolvedPercentage: false };
7718
+ }
7719
+ const availableTwips = axis === "width" ? getAvailableWidthTwips(theme, themeName) : getAvailableHeightTwips(theme, themeName);
7720
+ return {
7721
+ pixels: Math.round(availableTwips * fraction / TWIPS_PER_PIXEL),
7722
+ resolvedPercentage: true
7723
+ };
7724
+ }
7725
+ function shapeColor(value, theme) {
7726
+ return resolveColor(value, theme).replace(/^#/, "");
7727
+ }
7728
+ function shapeOutline(style, theme) {
7729
+ const border = style?.border;
7730
+ if (!border) return { ignoredSides: [], unsupportedStyles: [] };
7731
+ const order = [
7732
+ "top",
7733
+ "left",
7734
+ "bottom",
7735
+ "right"
7736
+ ];
7737
+ const declared = order.map((side) => [side, border[side]]).filter(
7738
+ (entry) => Boolean(entry[1])
7739
+ );
7740
+ if (declared.length === 0) return { ignoredSides: [], unsupportedStyles: [] };
7741
+ const unsupportedStyles = [
7742
+ ...new Set(
7743
+ declared.map(([, config]) => config.style).filter(
7744
+ (value) => value === "dashed" || value === "dotted" || value === "double"
7745
+ )
7746
+ )
7747
+ ];
7748
+ if (unsupportedStyles.length > 0) {
7749
+ return { ignoredSides: [], unsupportedStyles };
7750
+ }
7751
+ const [, used] = declared[0];
7752
+ const differs = ({ style: s, width, color }) => s !== used.style || width !== used.width || color !== used.color;
7753
+ const ignoredSides = declared.slice(1).filter(([, config]) => differs(config)).map(([side]) => side);
7754
+ if (used.style === "none") return { ignoredSides, unsupportedStyles };
7755
+ return {
7756
+ outline: {
7757
+ type: "solidFill",
7758
+ solidFillType: "rgb",
7759
+ value: used.color ? shapeColor(used.color, theme) : "000000",
7760
+ ...used.width !== void 0 && {
7761
+ width: Math.round(used.width * PIXELS_TO_EMU)
7762
+ }
7763
+ },
7764
+ ignoredSides,
7765
+ unsupportedStyles
7766
+ };
7767
+ }
7768
+ function shapeBodyProperties(style) {
7769
+ const padding = style?.padding;
7770
+ if (!padding) return void 0;
7771
+ const toEmu = (value) => value === void 0 ? void 0 : Math.round(value * PIXELS_TO_EMU);
7772
+ return {
7773
+ margins: {
7774
+ ...padding.top !== void 0 && { top: toEmu(padding.top) },
7775
+ ...padding.bottom !== void 0 && { bottom: toEmu(padding.bottom) },
7776
+ ...padding.left !== void 0 && { left: toEmu(padding.left) },
7777
+ ...padding.right !== void 0 && { right: toEmu(padding.right) }
7778
+ }
7779
+ };
7780
+ }
7781
+ async function renderTextBoxAsShape(tb, theme, themeName, context) {
7782
+ const width = resolveShapeSize(tb.props.width, "width", theme, themeName);
7783
+ const height = resolveShapeSize(tb.props.height, "height", theme, themeName);
7784
+ if (width.pixels === void 0 || height.pixels === void 0) {
7785
+ console.warn(
7786
+ '[core-docx] text-box renderAs "shape" needs an explicit width and height (a shape has no autofit); falling back to table rendering.'
7787
+ );
7788
+ return { kind: "fallback" };
7789
+ }
7790
+ const style = tb.props.style;
7791
+ const { outline, ignoredSides, unsupportedStyles } = shapeOutline(
7792
+ style,
7793
+ theme
7794
+ );
7795
+ if (unsupportedStyles.length > 0) {
7796
+ console.warn(
7797
+ `[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.`
7798
+ );
7799
+ return { kind: "fallback" };
7800
+ }
7801
+ const rendered = await renderTextBoxChildren(tb, theme, themeName, context);
7802
+ if (rendered.some((element) => !(element instanceof Paragraph5))) {
7803
+ console.warn(
7804
+ '[core-docx] text-box renderAs "shape" requires paragraph-only content; falling back to table rendering.'
7805
+ );
7806
+ return { kind: "fallback", rendered };
7807
+ }
7808
+ const children = rendered;
7809
+ if (width.resolvedPercentage || height.resolvedPercentage) {
7810
+ console.warn(
7811
+ '[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.'
7812
+ );
7813
+ }
7814
+ const fill = style?.shading?.fill;
7815
+ if (ignoredSides.length > 0) {
7816
+ console.warn(
7817
+ `[core-docx] text-box renderAs "shape" has one uniform outline; using the first declared border side and ignoring ${ignoredSides.join(", ")}.`
7818
+ );
7819
+ }
7820
+ const dropOutline = Boolean(fill) && Boolean(outline);
7821
+ if (dropOutline) {
7822
+ console.warn(
7823
+ '[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.'
7824
+ );
7825
+ }
7826
+ const bodyProperties = shapeBodyProperties(style);
7827
+ const run = new WpsShapeRun({
7828
+ type: "wps",
7829
+ children,
7830
+ transformation: { width: width.pixels, height: height.pixels },
7831
+ ...fill && {
7832
+ solidFill: { type: "rgb", value: shapeColor(fill, theme) }
7833
+ },
7834
+ ...outline && !dropOutline && { outline },
7835
+ ...bodyProperties && { bodyProperties },
7836
+ // Absent `floating` makes it a `wp:inline` drawing.
7837
+ ...tb.props.floating && {
7838
+ floating: mapFloatingOptions(tb.props.floating, theme, themeName)
7839
+ }
7840
+ });
7841
+ return {
7842
+ kind: "shape",
7843
+ paragraphs: [
7844
+ new Paragraph5({ children: [run], spacing: { before: 0, after: 0 } })
7845
+ ]
7846
+ };
7847
+ }
7848
+ async function renderTextBoxComponent(component, theme, themeName, context) {
7849
+ if (!isTextBoxComponent(component)) return [];
7850
+ const tb = component;
7851
+ if (tb.props.renderAs === "shape") {
7852
+ const attempt = await renderTextBoxAsShape(tb, theme, themeName, context);
7853
+ if (attempt.kind === "shape") return attempt.paragraphs;
7854
+ return renderTextBoxAsTable(
7855
+ tb,
7856
+ theme,
7857
+ themeName,
7858
+ context,
7859
+ attempt.rendered
7860
+ );
7861
+ }
7862
+ return renderTextBoxAsTable(tb, theme, themeName, context);
7863
+ }
7574
7864
 
7575
7865
  // src/components/table.ts
7576
7866
  async function renderTableComponent(component, theme, themeName) {
@@ -7624,14 +7914,14 @@ async function renderTableComponent(component, theme, themeName) {
7624
7914
  import { Paragraph as Paragraph6, BookmarkStart, BookmarkEnd } from "docx";
7625
7915
 
7626
7916
  // src/core/sectionBookmarks.ts
7627
- import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
7917
+ import { AsyncLocalStorage as AsyncLocalStorage8 } from "async_hooks";
7628
7918
  var NESTED_LINK_ID_BASE = 1e6;
7629
7919
  function createState3() {
7630
7920
  return { nextNested: 1, resolved: /* @__PURE__ */ new WeakMap() };
7631
7921
  }
7632
7922
  var SectionBookmarkRegistry = class {
7633
7923
  fallback = createState3();
7634
- scopes = new AsyncLocalStorage7();
7924
+ scopes = new AsyncLocalStorage8();
7635
7925
  get state() {
7636
7926
  return this.scopes.getStore() ?? this.fallback;
7637
7927
  }
@@ -7889,7 +8179,10 @@ function selectCachedEntries(collected, options) {
7889
8179
  continue;
7890
8180
  }
7891
8181
  if (entry.level < depthStart || entry.level > depthEnd) continue;
7892
- entries.push({ title: entry.title, level: entry.level });
8182
+ entries.push({
8183
+ title: entry.number ? `${headingNumberLabel(entry.number)} ${entry.title}` : entry.title,
8184
+ level: entry.level
8185
+ });
7893
8186
  }
7894
8187
  return entries;
7895
8188
  }
@@ -8197,6 +8490,61 @@ function computeSectionOrdinals(sections) {
8197
8490
 
8198
8491
  // src/core/collectTocHeadings.ts
8199
8492
  import { getStandardComponent } from "@json-to-office/shared-docx";
8493
+
8494
+ // src/utils/numberFormatting.ts
8495
+ var ROMAN_NUMERALS = [
8496
+ [1e3, "m"],
8497
+ [900, "cm"],
8498
+ [500, "d"],
8499
+ [400, "cd"],
8500
+ [100, "c"],
8501
+ [90, "xc"],
8502
+ [50, "l"],
8503
+ [40, "xl"],
8504
+ [10, "x"],
8505
+ [9, "ix"],
8506
+ [5, "v"],
8507
+ [4, "iv"],
8508
+ [1, "i"]
8509
+ ];
8510
+ function toRoman(value) {
8511
+ let remaining = value;
8512
+ let out = "";
8513
+ for (const [amount, glyph] of ROMAN_NUMERALS) {
8514
+ while (remaining >= amount) {
8515
+ out += glyph;
8516
+ remaining -= amount;
8517
+ }
8518
+ }
8519
+ return out;
8520
+ }
8521
+ function toLetters(value) {
8522
+ const index = (value - 1) % 26;
8523
+ const repeats = Math.floor((value - 1) / 26) + 1;
8524
+ return String.fromCharCode(97 + index).repeat(repeats);
8525
+ }
8526
+ function formatNumberForLevel(value, format2) {
8527
+ if (!Number.isFinite(value) || value < 1) return void 0;
8528
+ const n = Math.floor(value);
8529
+ switch (format2) {
8530
+ case "decimal":
8531
+ return String(n);
8532
+ case "lowerLetter":
8533
+ return toLetters(n);
8534
+ case "upperLetter":
8535
+ return toLetters(n).toUpperCase();
8536
+ case "lowerRoman":
8537
+ return toRoman(n);
8538
+ case "upperRoman":
8539
+ return toRoman(n).toUpperCase();
8540
+ default:
8541
+ return void 0;
8542
+ }
8543
+ }
8544
+
8545
+ // src/core/collectTocHeadings.ts
8546
+ var MAX_HEADING_LEVEL2 = 6;
8547
+ var MAX_LIST_LEVEL = 9;
8200
8548
  function normalizeEntryTitle(text) {
8201
8549
  return normalizeUnicodeText(text).replace(/(\*\*\*|___)([\s\S]*?)\1/g, "$2").replace(/(\*\*|__)([\s\S]*?)\1/g, "$2").replace(/(\*|_)([\s\S]*?)\1/g, "$2").trim();
8202
8550
  }
@@ -8215,9 +8563,97 @@ function styleEntryKey(props) {
8215
8563
  }
8216
8564
  return themeStyle;
8217
8565
  }
8218
- function collectTocHeadings(sections) {
8566
+ function levelStart(levels, level) {
8567
+ return levels[level]?.start ?? 1;
8568
+ }
8569
+ function collectDocumentOutline(sections) {
8219
8570
  const entries = [];
8571
+ const numberedItems = /* @__PURE__ */ new Map();
8220
8572
  const ordinals = computeSectionOrdinals(sections);
8573
+ const takenIds = /* @__PURE__ */ new Set();
8574
+ const headingCounters = new Array(MAX_HEADING_LEVEL2).fill(0);
8575
+ const listCounters = /* @__PURE__ */ new Map();
8576
+ const visitHeading = (component, props, sectionBookmarkId) => {
8577
+ const text = typeof props.text === "string" ? props.text : "";
8578
+ const title = normalizeEntryTitle(text);
8579
+ const level = typeof props.level === "number" ? props.level : 1;
8580
+ const styleLevel = level >= 1 && level <= MAX_HEADING_LEVEL2 ? level : 1;
8581
+ let full;
8582
+ let own;
8583
+ if (props.numbering === true) {
8584
+ headingCounters[styleLevel - 1] += 1;
8585
+ for (let deeper = styleLevel; deeper < MAX_HEADING_LEVEL2; deeper++) {
8586
+ headingCounters[deeper] = 0;
8587
+ }
8588
+ full = headingCounters.slice(0, styleLevel).join(".");
8589
+ own = String(headingCounters[styleLevel - 1]);
8590
+ }
8591
+ const explicitId = component.id;
8592
+ const bookmarkId = typeof explicitId === "string" && explicitId ? explicitId : dedupeBookmarkId(slugifyBookmarkText(text), (id) => takenIds.has(id));
8593
+ takenIds.add(bookmarkId);
8594
+ numberedItems.set(bookmarkId, {
8595
+ kind: "heading",
8596
+ text: title,
8597
+ ...full !== void 0 && { full, own }
8598
+ });
8599
+ if (title) {
8600
+ entries.push({
8601
+ title,
8602
+ level,
8603
+ sectionBookmarkId,
8604
+ ...full !== void 0 && { number: full }
8605
+ });
8606
+ }
8607
+ };
8608
+ const visitList = (props) => {
8609
+ const items = Array.isArray(props.items) ? props.items : [];
8610
+ if (items.length === 0) return;
8611
+ const reference = typeof props.reference === "string" && props.reference ? props.reference : void 0;
8612
+ const freshState = () => {
8613
+ const levels = resolveListLevels(
8614
+ props
8615
+ );
8616
+ return {
8617
+ levels,
8618
+ counters: Array.from(
8619
+ { length: MAX_LIST_LEVEL },
8620
+ (_, level) => levelStart(levels, level) - 1
8621
+ )
8622
+ };
8623
+ };
8624
+ let state;
8625
+ if (reference === void 0) {
8626
+ state = freshState();
8627
+ } else {
8628
+ state = listCounters.get(reference) ?? freshState();
8629
+ listCounters.set(reference, state);
8630
+ }
8631
+ for (const item of items) {
8632
+ const isObject = typeof item === "object" && item !== null;
8633
+ const raw = isObject ? item.text : item;
8634
+ const text = typeof raw === "string" ? raw : "";
8635
+ if (!text.trim() && !(isObject && item.revision)) continue;
8636
+ const rawLevel = isObject ? item.level : void 0;
8637
+ const level = typeof rawLevel === "number" && rawLevel >= 0 ? rawLevel : 0;
8638
+ if (level >= MAX_LIST_LEVEL) continue;
8639
+ state.counters[level] += 1;
8640
+ for (let deeper = level + 1; deeper < MAX_LIST_LEVEL; deeper++) {
8641
+ state.counters[deeper] = levelStart(state.levels, deeper) - 1;
8642
+ }
8643
+ const id = isObject ? item.id : void 0;
8644
+ if (typeof id !== "string" || !id) continue;
8645
+ takenIds.add(id);
8646
+ const number = formatNumberForLevel(
8647
+ state.counters[level],
8648
+ state.levels[level]?.format
8649
+ );
8650
+ numberedItems.set(id, {
8651
+ kind: "list-item",
8652
+ text: normalizeUnicodeText(text).trim(),
8653
+ ...number !== void 0 && { full: number, own: number }
8654
+ });
8655
+ }
8656
+ };
8221
8657
  sections.forEach((section, index) => {
8222
8658
  const ordinal = ordinals[index]?.ordinal;
8223
8659
  const sectionBookmarkId = ordinal ? globalSectionBookmarkRegistry.forLayoutSection(ordinal).id : void 0;
@@ -8225,15 +8661,11 @@ function collectTocHeadings(sections) {
8225
8661
  if (!isEnabled(component)) return;
8226
8662
  const props = component.props ?? {};
8227
8663
  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
- }
8664
+ visitHeading(component, props, sectionBookmarkId);
8234
8665
  return;
8235
8666
  }
8236
8667
  if (component.name === "paragraph") {
8668
+ if (typeof props.id === "string" && props.id) takenIds.add(props.id);
8237
8669
  const styleId = styleEntryKey(props);
8238
8670
  const text = typeof props.text === "string" ? props.text : "";
8239
8671
  const title = normalizeEntryTitle(text);
@@ -8242,13 +8674,17 @@ function collectTocHeadings(sections) {
8242
8674
  }
8243
8675
  return;
8244
8676
  }
8677
+ if (component.name === "list") {
8678
+ visitList(props);
8679
+ return;
8680
+ }
8245
8681
  if (!isContainer(component.name)) return;
8246
8682
  const children = component.children;
8247
8683
  if (Array.isArray(children)) children.forEach(visit);
8248
8684
  };
8249
8685
  section.components.forEach(visit);
8250
8686
  });
8251
- return entries;
8687
+ return { entries, numberedItems };
8252
8688
  }
8253
8689
 
8254
8690
  // src/core/render.ts
@@ -8301,7 +8737,13 @@ async function renderDocument(structure, layout, options) {
8301
8737
  // Footnote ids are document-scoped too: a reference resolved
8302
8738
  // against another render's counter points at the wrong body.
8303
8739
  globalNoteRegistry.runScoped(
8304
- () => renderDocumentScoped(structure, layout, options)
8740
+ () => (
8741
+ // Cross-reference targets are keyed by bookmark id, which
8742
+ // is only unique within one document.
8743
+ globalNumberedItemsRegistry.runScoped(
8744
+ () => renderDocumentScoped(structure, layout, options)
8745
+ )
8746
+ )
8305
8747
  )
8306
8748
  )
8307
8749
  )
@@ -8344,13 +8786,14 @@ async function renderDocumentScoped(structure, layout, options) {
8344
8786
  );
8345
8787
  }
8346
8788
  try {
8347
- const tocHeadings = collectTocHeadings(layout.sections);
8348
- if (tocHeadings.length > 0) {
8349
- context.tocHeadings = tocHeadings;
8789
+ const outline = collectDocumentOutline(layout.sections);
8790
+ if (outline.entries.length > 0) {
8791
+ context.tocHeadings = outline.entries;
8350
8792
  }
8793
+ globalNumberedItemsRegistry.seed(outline.numberedItems);
8351
8794
  } catch (error) {
8352
8795
  console.warn(
8353
- "[core-docx] TOC entry collection failed; the TOC field will rely on the reader refreshing it:",
8796
+ "[core-docx] Document outline collection failed; the TOC field will rely on the reader refreshing it:",
8354
8797
  error instanceof Error ? error.message : error
8355
8798
  );
8356
8799
  }