@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
package/dist/index.js CHANGED
@@ -383,9 +383,6 @@ var init_minimal_docx_theme = __esm({
383
383
  },
384
384
  section: {
385
385
  pageBreak: false
386
- },
387
- heading: {
388
- numbering: false
389
386
  }
390
387
  }
391
388
  };
@@ -508,9 +505,6 @@ var init_corporate_docx_theme = __esm({
508
505
  },
509
506
  section: {
510
507
  pageBreak: false
511
- },
512
- heading: {
513
- numbering: false
514
508
  }
515
509
  }
516
510
  };
@@ -753,9 +747,6 @@ var init_apex_docx_theme = __esm({
753
747
  },
754
748
  section: {
755
749
  pageBreak: false
756
- },
757
- heading: {
758
- numbering: false
759
750
  }
760
751
  }
761
752
  };
@@ -889,9 +880,6 @@ var init_devportal_docx_theme = __esm({
889
880
  },
890
881
  section: {
891
882
  pageBreak: false
892
- },
893
- heading: {
894
- numbering: false
895
883
  }
896
884
  }
897
885
  };
@@ -3752,8 +3740,51 @@ import {
3752
3740
  InternalHyperlink,
3753
3741
  FootnoteReferenceRun,
3754
3742
  EndnoteReferenceRun,
3743
+ NumberedItemReference,
3744
+ NumberedItemReferenceFormat,
3755
3745
  Tab
3756
3746
  } from "docx";
3747
+
3748
+ // src/utils/numberedItemsRegistry.ts
3749
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
3750
+ var NumberedItemsRegistry = class {
3751
+ fallback = {
3752
+ items: /* @__PURE__ */ new Map(),
3753
+ seeded: false
3754
+ };
3755
+ scopes = new AsyncLocalStorage2();
3756
+ get state() {
3757
+ return this.scopes.getStore() ?? this.fallback;
3758
+ }
3759
+ /** Run work with an isolated registry that follows its async call chain. */
3760
+ runScoped(callback) {
3761
+ return this.scopes.run({ items: /* @__PURE__ */ new Map(), seeded: false }, callback);
3762
+ }
3763
+ /** Replace the contents with the pre-pass result. */
3764
+ seed(items) {
3765
+ const state = this.state;
3766
+ state.items = new Map(items);
3767
+ state.seeded = true;
3768
+ }
3769
+ /**
3770
+ * False outside a render (a unit test calling `createText` directly). An
3771
+ * unresolved reference is then expected rather than an authoring mistake, so
3772
+ * the caller stays quiet about it.
3773
+ */
3774
+ isSeeded() {
3775
+ return this.state.seeded;
3776
+ }
3777
+ get(id) {
3778
+ return this.state.items.get(id);
3779
+ }
3780
+ clear() {
3781
+ this.state.items.clear();
3782
+ this.state.seeded = false;
3783
+ }
3784
+ };
3785
+ var globalNumberedItemsRegistry = new NumberedItemsRegistry();
3786
+
3787
+ // src/utils/textParser.ts
3757
3788
  var FOOTNOTE_MARKER_REGEX = /\[\^([^\]\s]+)\]/;
3758
3789
  function escapeRegExp(s) {
3759
3790
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3969,13 +4000,59 @@ function splitNoteMarkers(text, noteRef, runs) {
3969
4000
  if (trailing) out.push(...runs(trailing));
3970
4001
  return out;
3971
4002
  }
4003
+ var CROSS_REFERENCE_PATTERN = "\\[@([^\\]\\s:]+)(?::(relative|no_context|full_context|none))?\\]";
4004
+ var INLINE_TOKEN_REGEX = `\\[([^\\]]+)\\]\\(([^)]+)\\)|${CROSS_REFERENCE_PATTERN}`;
4005
+ function hasCrossReference(text) {
4006
+ return new RegExp(CROSS_REFERENCE_PATTERN).test(text);
4007
+ }
4008
+ var REFERENCE_FORMATS = {
4009
+ relative: NumberedItemReferenceFormat.RELATIVE,
4010
+ no_context: NumberedItemReferenceFormat.NO_CONTEXT,
4011
+ full_context: NumberedItemReferenceFormat.FULL_CONTEXT,
4012
+ none: NumberedItemReferenceFormat.NONE
4013
+ };
4014
+ function createCrossReference(id, format2, token, baseStyle, options) {
4015
+ const info = globalNumberedItemsRegistry.get(id);
4016
+ if (!info) {
4017
+ if (globalNumberedItemsRegistry.isSeeded()) {
4018
+ console.warn(
4019
+ `[core-docx] Cross-reference ${token} has no target: no heading or list item declares the id "${id}". Rendering the token as literal text.`
4020
+ );
4021
+ }
4022
+ return buildTextRuns(
4023
+ token,
4024
+ buildRunCommonProps(baseStyle, { boldColor: options.boldColor }),
4025
+ { noProof: baseStyle.noProof, noProofWords: options.noProofWords }
4026
+ );
4027
+ }
4028
+ if (format2 === "none") {
4029
+ return [
4030
+ new NumberedItemReference(id, info.text, {
4031
+ hyperlink: true,
4032
+ referenceFormat: REFERENCE_FORMATS.none
4033
+ })
4034
+ ];
4035
+ }
4036
+ const cachedValue = format2 === "no_context" ? info.own : info.full;
4037
+ if (cachedValue === void 0) {
4038
+ console.warn(
4039
+ `[core-docx] Cross-reference ${token} targets an unnumbered ${info.kind} ("${id}"), so the field carries no cached number and reads blank until the reader updates fields. Use [@${id}:none] to reference its text instead.`
4040
+ );
4041
+ }
4042
+ return [
4043
+ new NumberedItemReference(id, cachedValue, {
4044
+ hyperlink: true,
4045
+ referenceFormat: REFERENCE_FORMATS[format2]
4046
+ })
4047
+ ];
4048
+ }
3972
4049
  function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
3973
4050
  const normalizedText = normalizeUnicodeText(text);
3974
4051
  const runs = [];
3975
- const hyperlinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
4052
+ const tokenRegex = new RegExp(INLINE_TOKEN_REGEX, "g");
3976
4053
  let lastIndex = 0;
3977
4054
  let match;
3978
- while ((match = hyperlinkRegex.exec(normalizedText)) !== null) {
4055
+ while ((match = tokenRegex.exec(normalizedText)) !== null) {
3979
4056
  if (match.index > lastIndex) {
3980
4057
  const plainText = normalizedText.substring(lastIndex, match.index);
3981
4058
  if (plainText) {
@@ -3987,6 +4064,19 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
3987
4064
  runs.push(...plainRuns);
3988
4065
  }
3989
4066
  }
4067
+ lastIndex = match.index + match[0].length;
4068
+ if (match[1] === void 0) {
4069
+ runs.push(
4070
+ ...createCrossReference(
4071
+ match[3],
4072
+ match[4] ?? "relative",
4073
+ match[0],
4074
+ baseStyle,
4075
+ options
4076
+ )
4077
+ );
4078
+ continue;
4079
+ }
3990
4080
  const linkText = match[1];
3991
4081
  const linkUrl = match[2];
3992
4082
  const isInternal = linkUrl.startsWith("#");
@@ -4013,7 +4103,6 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
4013
4103
  })
4014
4104
  );
4015
4105
  }
4016
- lastIndex = match.index + match[0].length;
4017
4106
  }
4018
4107
  if (lastIndex < normalizedText.length) {
4019
4108
  const remainingText = normalizedText.substring(lastIndex);
@@ -4222,7 +4311,7 @@ function initializeBuiltinPlaceholders() {
4222
4311
  initializeBuiltinPlaceholders();
4223
4312
 
4224
4313
  // src/utils/revisionUtils.ts
4225
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
4314
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
4226
4315
 
4227
4316
  // src/utils/componentAnnotations.ts
4228
4317
  function isRecord(value) {
@@ -4273,7 +4362,7 @@ var DEFAULT_REVISION_AUTHOR = "json-to-office";
4273
4362
  var DEFAULT_REVISION_DATE = "1970-01-01T00:00:00Z";
4274
4363
  var RevisionIdRegistry = class {
4275
4364
  fallbackCounter = 0;
4276
- scopes = new AsyncLocalStorage2();
4365
+ scopes = new AsyncLocalStorage3();
4277
4366
  runScoped(callback) {
4278
4367
  return this.scopes.run({ counter: 0 }, callback);
4279
4368
  }
@@ -4555,10 +4644,21 @@ init_styles();
4555
4644
  init_defaults();
4556
4645
 
4557
4646
  // src/utils/bookmarkRegistry.ts
4558
- import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
4647
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
4648
+ function slugifyBookmarkText(text) {
4649
+ return text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").substring(0, 40);
4650
+ }
4651
+ function dedupeBookmarkId(base, taken) {
4652
+ let id = base;
4653
+ let attempt = 0;
4654
+ while (taken(id) && attempt < 100) {
4655
+ id = `${base}-${++attempt}`;
4656
+ }
4657
+ return id;
4658
+ }
4559
4659
  var BookmarkRegistry = class {
4560
4660
  fallback = { bookmarks: /* @__PURE__ */ new Map() };
4561
- scopes = new AsyncLocalStorage3();
4661
+ scopes = new AsyncLocalStorage4();
4562
4662
  get state() {
4563
4663
  return this.scopes.getStore() ?? this.fallback;
4564
4664
  }
@@ -4582,13 +4682,10 @@ var BookmarkRegistry = class {
4582
4682
  * Converts text to a URL-friendly format
4583
4683
  */
4584
4684
  generateId(text, _type = "bookmark") {
4585
- const baseId = text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").substring(0, 40);
4586
- let id = baseId;
4587
- let attempt = 0;
4588
- while (this.state.bookmarks.has(id) && attempt < 100) {
4589
- id = `${baseId}-${++attempt}`;
4590
- }
4591
- return id;
4685
+ return dedupeBookmarkId(
4686
+ slugifyBookmarkText(text),
4687
+ (id) => this.state.bookmarks.has(id)
4688
+ );
4592
4689
  }
4593
4690
  /**
4594
4691
  * Check if a bookmark exists
@@ -4640,7 +4737,7 @@ import {
4640
4737
 
4641
4738
  // src/utils/commentRegistry.ts
4642
4739
  import { Paragraph, TextRun as TextRun4 } from "docx";
4643
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
4740
+ import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4644
4741
  var DEFAULT_COMMENT_AUTHOR = "json-to-office";
4645
4742
  var DEFAULT_COMMENT_DATE = "1970-01-01T00:00:00Z";
4646
4743
  function createState() {
@@ -4655,7 +4752,7 @@ function bodyParagraphs(text) {
4655
4752
  }
4656
4753
  var CommentRegistry = class {
4657
4754
  fallback = createState();
4658
- scopes = new AsyncLocalStorage4();
4755
+ scopes = new AsyncLocalStorage5();
4659
4756
  get state() {
4660
4757
  return this.scopes.getStore() ?? this.fallback;
4661
4758
  }
@@ -4750,7 +4847,7 @@ function closeCommentRange(ids) {
4750
4847
 
4751
4848
  // src/utils/noteRegistry.ts
4752
4849
  import { Paragraph as Paragraph2, TextRun as TextRun6 } from "docx";
4753
- import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4850
+ import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
4754
4851
  function createState2() {
4755
4852
  return {
4756
4853
  footnoteCounter: 0,
@@ -4769,7 +4866,7 @@ function bodyParagraphs2(text, style) {
4769
4866
  }
4770
4867
  var NoteRegistry = class {
4771
4868
  fallback = createState2();
4772
- scopes = new AsyncLocalStorage5();
4869
+ scopes = new AsyncLocalStorage6();
4773
4870
  get state() {
4774
4871
  return this.scopes.getStore() ?? this.fallback;
4775
4872
  }
@@ -5125,7 +5222,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5125
5222
  if (commentAnchor) {
5126
5223
  children.push(...commentAnchor.start);
5127
5224
  }
5128
- const hasDecorators = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText);
5225
+ const hasInlineSyntax = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText) || hasCrossReference(normalizedText);
5129
5226
  const headingHasWeightRequest = options.fontWeight != null || options.bold === true;
5130
5227
  const headingEffectiveFamily = options.fontFamily ?? (headingHasWeightRequest ? resolveFontFamily(theme, "heading") : void 0);
5131
5228
  const headingWeighted = applyFontWeightAlias({
@@ -5192,7 +5289,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5192
5289
  "heading"
5193
5290
  );
5194
5291
  const headingTextChildren = [];
5195
- if (hasDecorators) {
5292
+ if (hasInlineSyntax) {
5196
5293
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
5197
5294
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
5198
5295
  enableHyperlinks: true,
@@ -5209,7 +5306,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5209
5306
  })
5210
5307
  );
5211
5308
  } else {
5212
- if (hasDecorators) {
5309
+ if (hasInlineSyntax) {
5213
5310
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
5214
5311
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
5215
5312
  enableHyperlinks: true,
@@ -5231,7 +5328,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5231
5328
  spacing: hasExplicitSpacing ? spacing : void 0,
5232
5329
  ...options.keepNext !== void 0 && { keepNext: options.keepNext },
5233
5330
  ...options.keepLines !== void 0 && { keepLines: options.keepLines },
5234
- ...options.indent && { indent: options.indent }
5331
+ ...options.indent && { indent: options.indent },
5332
+ // docx creates the concrete numbering instance for the reference itself,
5333
+ // in ParagraphProperties.prepForXml.
5334
+ ...options.numbering !== void 0 && { numbering: options.numbering }
5235
5335
  });
5236
5336
  }
5237
5337
  async function createImage(path4, theme, themeName, options = {}) {
@@ -5391,9 +5491,17 @@ function createList(items, _theme, _themeName, options = {}) {
5391
5491
  } else if (options.spacing?.item) {
5392
5492
  spacing.after = pointsToTwips(options.spacing.item);
5393
5493
  }
5494
+ const itemId = typeof item === "object" ? item.id : void 0;
5495
+ let itemContent = textRuns;
5496
+ if (itemId) {
5497
+ globalBookmarkRegistry.register(itemId, itemText, "list-item");
5498
+ itemContent = [
5499
+ new Bookmark({ id: itemId, children: textRuns })
5500
+ ];
5501
+ }
5394
5502
  const paragraphChildren = [
5395
5503
  ...commentAnchor && index === firstRendered ? commentAnchor.start : [],
5396
- ...textRuns,
5504
+ ...itemContent,
5397
5505
  ...commentAnchor && index === lastRendered ? closeCommentRange(commentAnchor.ids) : []
5398
5506
  ];
5399
5507
  const paragraph = new Paragraph3({
@@ -6236,54 +6344,14 @@ function createFooterElement(children, _options) {
6236
6344
  });
6237
6345
  }
6238
6346
 
6239
- // src/components/heading.ts
6240
- function renderHeadingComponent(component, theme, themeName) {
6241
- if (!isHeadingComponent(component)) return [];
6242
- const config = component.props;
6243
- const bookmarkId = component.id || globalBookmarkRegistry.generateId(config.text, "heading");
6244
- const header = createHeading(
6245
- config.text,
6246
- config.level || 1,
6247
- theme,
6248
- themeName,
6249
- {
6250
- alignment: config.alignment,
6251
- spacing: config.spacing,
6252
- lineSpacing: config.lineSpacing,
6253
- columnBreak: config.columnBreak,
6254
- // Local font overrides
6255
- fontFamily: config.font?.family,
6256
- fontSize: config.font?.size,
6257
- fontColor: config.font?.color,
6258
- bold: config.font?.bold,
6259
- fontWeight: config.font?.fontWeight,
6260
- italic: config.font?.italic,
6261
- underline: config.font?.underline,
6262
- scale: config.font?.scale,
6263
- characterSpacing: config.font?.characterSpacing,
6264
- // Proofing: local language override + no-proof toggle + known-words list
6265
- language: config.language,
6266
- noProof: config.noProof,
6267
- noProofWords: config.noProofWords,
6268
- // Pagination control
6269
- keepNext: config.keepNext,
6270
- keepLines: config.keepLines,
6271
- // Paragraph indentation (w:ind) in twips
6272
- indent: config.indent,
6273
- // Bookmark ID for internal linking
6274
- bookmarkId,
6275
- // Tracked-change segments (rendered as native Word revisions)
6276
- revision: config.revision,
6277
- // Review comment anchored to this heading's text
6278
- comment: config.comment
6279
- }
6280
- );
6281
- return [header];
6282
- }
6283
-
6284
6347
  // src/utils/numberingConfig.ts
6285
- import { AlignmentType as AlignmentType3, convertInchesToTwip, LevelFormat } from "docx";
6286
- import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
6348
+ import {
6349
+ AlignmentType as AlignmentType3,
6350
+ convertInchesToTwip,
6351
+ LevelFormat,
6352
+ LevelSuffix
6353
+ } from "docx";
6354
+ import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
6287
6355
  var LEVEL_FORMAT_MAP = {
6288
6356
  decimal: LevelFormat.DECIMAL,
6289
6357
  upperRoman: LevelFormat.UPPER_ROMAN,
@@ -6411,12 +6479,37 @@ function createNumberingConfig(config) {
6411
6479
  levels
6412
6480
  };
6413
6481
  }
6482
+ var HEADING_NUMBERING_REFERENCE = "jto-heading-numbering";
6483
+ function createHeadingNumberingConfig() {
6484
+ const levels = [];
6485
+ for (let level = 0; level < 6; level++) {
6486
+ const text = Array.from({ length: level + 1 }, (_, i) => `%${i + 1}`).join(".") + ".";
6487
+ levels.push({
6488
+ level,
6489
+ format: LevelFormat.DECIMAL,
6490
+ text,
6491
+ alignment: AlignmentType3.LEFT,
6492
+ start: 1,
6493
+ // A space, not the default tab: a tab would push the heading text to the
6494
+ // next tab stop and misalign it against unnumbered headings.
6495
+ suffix: LevelSuffix.SPACE,
6496
+ style: {
6497
+ paragraph: { indent: { left: 0, hanging: 0 } },
6498
+ style: `Heading${level + 1}`
6499
+ }
6500
+ });
6501
+ }
6502
+ return { reference: HEADING_NUMBERING_REFERENCE, levels };
6503
+ }
6504
+ function headingNumberLabel(number) {
6505
+ return `${number}.`;
6506
+ }
6414
6507
  var NumberingRegistry = class {
6415
6508
  fallback = {
6416
6509
  configs: /* @__PURE__ */ new Map(),
6417
6510
  counter: 0
6418
6511
  };
6419
- scopes = new AsyncLocalStorage6();
6512
+ scopes = new AsyncLocalStorage7();
6420
6513
  get state() {
6421
6514
  return this.scopes.getStore() ?? this.fallback;
6422
6515
  }
@@ -6466,6 +6559,63 @@ var NumberingRegistry = class {
6466
6559
  };
6467
6560
  var globalNumberingRegistry = new NumberingRegistry();
6468
6561
 
6562
+ // src/components/heading.ts
6563
+ var MAX_HEADING_LEVEL = 6;
6564
+ function headingNumbering(numbering, level) {
6565
+ if (numbering === false) return false;
6566
+ if (numbering !== true) return void 0;
6567
+ if (!globalNumberingRegistry.has(HEADING_NUMBERING_REFERENCE)) {
6568
+ globalNumberingRegistry.register(createHeadingNumberingConfig());
6569
+ }
6570
+ const styleLevel = level >= 1 && level <= MAX_HEADING_LEVEL ? level : 1;
6571
+ return { reference: HEADING_NUMBERING_REFERENCE, level: styleLevel - 1 };
6572
+ }
6573
+ function renderHeadingComponent(component, theme, themeName) {
6574
+ if (!isHeadingComponent(component)) return [];
6575
+ const config = component.props;
6576
+ const bookmarkId = component.id || globalBookmarkRegistry.generateId(config.text, "heading");
6577
+ const header = createHeading(
6578
+ config.text,
6579
+ config.level || 1,
6580
+ theme,
6581
+ themeName,
6582
+ {
6583
+ alignment: config.alignment,
6584
+ spacing: config.spacing,
6585
+ lineSpacing: config.lineSpacing,
6586
+ columnBreak: config.columnBreak,
6587
+ // Local font overrides
6588
+ fontFamily: config.font?.family,
6589
+ fontSize: config.font?.size,
6590
+ fontColor: config.font?.color,
6591
+ bold: config.font?.bold,
6592
+ fontWeight: config.font?.fontWeight,
6593
+ italic: config.font?.italic,
6594
+ underline: config.font?.underline,
6595
+ scale: config.font?.scale,
6596
+ characterSpacing: config.font?.characterSpacing,
6597
+ // Proofing: local language override + no-proof toggle + known-words list
6598
+ language: config.language,
6599
+ noProof: config.noProof,
6600
+ noProofWords: config.noProofWords,
6601
+ // Pagination control
6602
+ keepNext: config.keepNext,
6603
+ keepLines: config.keepLines,
6604
+ // Paragraph indentation (w:ind) in twips
6605
+ indent: config.indent,
6606
+ // Bookmark ID for internal linking
6607
+ bookmarkId,
6608
+ // Auto-numbering (1., 1.1., …) through the shared heading definition
6609
+ numbering: headingNumbering(config.numbering, config.level || 1),
6610
+ // Tracked-change segments (rendered as native Word revisions)
6611
+ revision: config.revision,
6612
+ // Review comment anchored to this heading's text
6613
+ comment: config.comment
6614
+ }
6615
+ );
6616
+ return [header];
6617
+ }
6618
+
6469
6619
  // src/components/paragraph.ts
6470
6620
  function parseMarkdownList(text) {
6471
6621
  const lines = text.split("\n");
@@ -6605,8 +6755,7 @@ function renderParagraphComponent(component, theme, themeName) {
6605
6755
  return [text];
6606
6756
  }
6607
6757
 
6608
- // src/components/list.ts
6609
- init_colorUtils();
6758
+ // src/utils/listLevels.ts
6610
6759
  function createLevelsFromSimplifiedProps(props) {
6611
6760
  const levels = [];
6612
6761
  let format2;
@@ -6653,18 +6802,8 @@ function createLevelsFromSimplifiedProps(props) {
6653
6802
  }
6654
6803
  return levels;
6655
6804
  }
6656
- function resolveMarkerFonts(levels, theme) {
6657
- return levels.map((level) => {
6658
- const font = level.font;
6659
- if (!font?.color) return level;
6660
- return {
6661
- ...level,
6662
- font: { ...font, color: resolveColor(font.color, theme) }
6663
- };
6664
- });
6665
- }
6666
6805
  function applyListStart(levels, start) {
6667
- if (start === void 0) return levels;
6806
+ if (start === void 0) return [...levels];
6668
6807
  return levels.map(
6669
6808
  (level) => level.level === 0 && level.start === void 0 ? { ...level, start } : level
6670
6809
  );
@@ -6722,28 +6861,37 @@ function fillMissingLevels(levels, maxLevel) {
6722
6861
  }
6723
6862
  return result;
6724
6863
  }
6864
+ function resolveListLevels(props) {
6865
+ const maxLevel = getMaxLevelFromItems(props.items);
6866
+ if (props.levels && props.levels.length > 0) {
6867
+ return fillMissingLevels(
6868
+ applyListStart(props.levels, props.start),
6869
+ maxLevel
6870
+ );
6871
+ }
6872
+ return fillMissingLevels(createLevelsFromSimplifiedProps(props), maxLevel);
6873
+ }
6874
+
6875
+ // src/components/list.ts
6876
+ init_colorUtils();
6877
+ function resolveMarkerFonts(levels, theme) {
6878
+ return levels.map((level) => {
6879
+ const font = level.font;
6880
+ if (!font?.color) return level;
6881
+ return {
6882
+ ...level,
6883
+ font: { ...font, color: resolveColor(font.color, theme) }
6884
+ };
6885
+ });
6886
+ }
6725
6887
  function renderListComponent(component, theme, themeName) {
6726
6888
  if (!isListComponent(component)) return [];
6727
6889
  const resolvedConfig = component.props;
6728
- const maxLevel = getMaxLevelFromItems(resolvedConfig.items);
6729
6890
  const reference = resolvedConfig.reference || globalNumberingRegistry.generateReference("list");
6730
6891
  if (!globalNumberingRegistry.has(reference)) {
6731
- let levels;
6732
- if (resolvedConfig.levels && resolvedConfig.levels.length > 0) {
6733
- levels = fillMissingLevels(
6734
- applyListStart(
6735
- resolvedConfig.levels,
6736
- resolvedConfig.start
6737
- ),
6738
- maxLevel
6739
- );
6740
- } else {
6741
- const baseLevels = createLevelsFromSimplifiedProps(resolvedConfig);
6742
- levels = fillMissingLevels(baseLevels, maxLevel);
6743
- }
6744
6892
  const config = {
6745
6893
  reference,
6746
- levels: resolveMarkerFonts(levels, theme)
6894
+ levels: resolveMarkerFonts(resolveListLevels(resolvedConfig), theme)
6747
6895
  };
6748
6896
  const numberingConfig = createNumberingConfig(config);
6749
6897
  globalNumberingRegistry.register(numberingConfig);
@@ -6783,6 +6931,7 @@ async function renderImageComponent(component, theme, themeName) {
6783
6931
 
6784
6932
  // src/components/text-box.ts
6785
6933
  import {
6934
+ Paragraph as Paragraph5,
6786
6935
  Table as Table3,
6787
6936
  TableRow as TableRow2,
6788
6937
  TableCell as TableCell2,
@@ -6791,7 +6940,8 @@ import {
6791
6940
  RelativeHorizontalPosition,
6792
6941
  RelativeVerticalPosition,
6793
6942
  OverlapType,
6794
- TableLayoutType as TableLayoutType2
6943
+ TableLayoutType as TableLayoutType2,
6944
+ WpsShapeRun
6795
6945
  } from "docx";
6796
6946
 
6797
6947
  // src/styles/utils/borderUtils.ts
@@ -6868,6 +7018,8 @@ function buildCellOptions(children, styleCfg, theme) {
6868
7018
  }
6869
7019
 
6870
7020
  // src/components/text-box.ts
7021
+ init_colorUtils();
7022
+ init_docxImagePositioning();
6871
7023
  init_widthUtils();
6872
7024
  function mapTableFloatOptions(floating, theme, themeName) {
6873
7025
  if (!floating) return void 0;
@@ -6923,26 +7075,23 @@ function mapTableFloatOptions(floating, theme, themeName) {
6923
7075
  opt.overlap = OverlapType.OVERLAP;
6924
7076
  return opt;
6925
7077
  }
6926
- async function renderTextBoxComponent(component, theme, themeName, _context) {
6927
- if (!isTextBoxComponent(component)) return [];
6928
- const tb = component;
7078
+ async function renderTextBoxChildren(tb, theme, themeName, context) {
7079
+ const childContext = {
7080
+ ...context,
7081
+ parent: tb
7082
+ };
7083
+ const rendered = [];
7084
+ for (const child of tb.children || []) {
7085
+ rendered.push(
7086
+ ...await renderComponent(child, theme, themeName, childContext)
7087
+ );
7088
+ }
7089
+ return rendered;
7090
+ }
7091
+ async function renderTextBoxAsTable(tb, theme, themeName, _context, prerendered) {
6929
7092
  const isInline = !tb.props.floating;
6930
- const childComponents = tb.children || [];
6931
7093
  if (isInline) {
6932
- const cellChildren2 = [];
6933
- const childContext2 = {
6934
- ..._context,
6935
- parent: tb
6936
- };
6937
- for (const child of childComponents) {
6938
- const rendered = await renderComponent(
6939
- child,
6940
- theme,
6941
- themeName,
6942
- childContext2
6943
- );
6944
- cellChildren2.push(...rendered);
6945
- }
7094
+ const cellChildren2 = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
6946
7095
  const styleCfg2 = tb.props.style;
6947
7096
  const cellOpts2 = buildCellOptions(cellChildren2, styleCfg2, theme);
6948
7097
  const row2 = new TableRow2({ children: [new TableCell2(cellOpts2)] });
@@ -6955,20 +7104,7 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
6955
7104
  });
6956
7105
  return [table2];
6957
7106
  }
6958
- const cellChildren = [];
6959
- const childContext = {
6960
- ..._context,
6961
- parent: tb
6962
- };
6963
- for (const child of childComponents) {
6964
- const rendered = await renderComponent(
6965
- child,
6966
- theme,
6967
- themeName,
6968
- childContext
6969
- );
6970
- cellChildren.push(...rendered);
6971
- }
7107
+ const cellChildren = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
6972
7108
  const styleCfg = tb.props.style;
6973
7109
  const cellOpts = buildCellOptions(cellChildren, styleCfg, theme);
6974
7110
  const row = new TableRow2({
@@ -7000,6 +7136,162 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
7000
7136
  });
7001
7137
  return [table];
7002
7138
  }
7139
+ var PIXELS_TO_EMU = 9525;
7140
+ var TWIPS_PER_PIXEL = 15;
7141
+ function resolveShapeSize(value, axis, theme, themeName) {
7142
+ if (typeof value === "number") {
7143
+ return { pixels: Math.round(value), resolvedPercentage: false };
7144
+ }
7145
+ if (typeof value !== "string") return { resolvedPercentage: false };
7146
+ const fraction = parseFloat(value) / 100;
7147
+ if (!Number.isFinite(fraction) || fraction <= 0) {
7148
+ return { resolvedPercentage: false };
7149
+ }
7150
+ const availableTwips = axis === "width" ? getAvailableWidthTwips(theme, themeName) : getAvailableHeightTwips(theme, themeName);
7151
+ return {
7152
+ pixels: Math.round(availableTwips * fraction / TWIPS_PER_PIXEL),
7153
+ resolvedPercentage: true
7154
+ };
7155
+ }
7156
+ function shapeColor(value, theme) {
7157
+ return resolveColor(value, theme).replace(/^#/, "");
7158
+ }
7159
+ function shapeOutline(style, theme) {
7160
+ const border = style?.border;
7161
+ if (!border) return { ignoredSides: [], unsupportedStyles: [] };
7162
+ const order = [
7163
+ "top",
7164
+ "left",
7165
+ "bottom",
7166
+ "right"
7167
+ ];
7168
+ const declared = order.map((side) => [side, border[side]]).filter(
7169
+ (entry) => Boolean(entry[1])
7170
+ );
7171
+ if (declared.length === 0) return { ignoredSides: [], unsupportedStyles: [] };
7172
+ const unsupportedStyles = [
7173
+ ...new Set(
7174
+ declared.map(([, config]) => config.style).filter(
7175
+ (value) => value === "dashed" || value === "dotted" || value === "double"
7176
+ )
7177
+ )
7178
+ ];
7179
+ if (unsupportedStyles.length > 0) {
7180
+ return { ignoredSides: [], unsupportedStyles };
7181
+ }
7182
+ const [, used] = declared[0];
7183
+ const differs = ({ style: s, width, color }) => s !== used.style || width !== used.width || color !== used.color;
7184
+ const ignoredSides = declared.slice(1).filter(([, config]) => differs(config)).map(([side]) => side);
7185
+ if (used.style === "none") return { ignoredSides, unsupportedStyles };
7186
+ return {
7187
+ outline: {
7188
+ type: "solidFill",
7189
+ solidFillType: "rgb",
7190
+ value: used.color ? shapeColor(used.color, theme) : "000000",
7191
+ ...used.width !== void 0 && {
7192
+ width: Math.round(used.width * PIXELS_TO_EMU)
7193
+ }
7194
+ },
7195
+ ignoredSides,
7196
+ unsupportedStyles
7197
+ };
7198
+ }
7199
+ function shapeBodyProperties(style) {
7200
+ const padding = style?.padding;
7201
+ if (!padding) return void 0;
7202
+ const toEmu = (value) => value === void 0 ? void 0 : Math.round(value * PIXELS_TO_EMU);
7203
+ return {
7204
+ margins: {
7205
+ ...padding.top !== void 0 && { top: toEmu(padding.top) },
7206
+ ...padding.bottom !== void 0 && { bottom: toEmu(padding.bottom) },
7207
+ ...padding.left !== void 0 && { left: toEmu(padding.left) },
7208
+ ...padding.right !== void 0 && { right: toEmu(padding.right) }
7209
+ }
7210
+ };
7211
+ }
7212
+ async function renderTextBoxAsShape(tb, theme, themeName, context) {
7213
+ const width = resolveShapeSize(tb.props.width, "width", theme, themeName);
7214
+ const height = resolveShapeSize(tb.props.height, "height", theme, themeName);
7215
+ if (width.pixels === void 0 || height.pixels === void 0) {
7216
+ console.warn(
7217
+ '[core-docx] text-box renderAs "shape" needs an explicit width and height (a shape has no autofit); falling back to table rendering.'
7218
+ );
7219
+ return { kind: "fallback" };
7220
+ }
7221
+ const style = tb.props.style;
7222
+ const { outline, ignoredSides, unsupportedStyles } = shapeOutline(
7223
+ style,
7224
+ theme
7225
+ );
7226
+ if (unsupportedStyles.length > 0) {
7227
+ console.warn(
7228
+ `[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.`
7229
+ );
7230
+ return { kind: "fallback" };
7231
+ }
7232
+ const rendered = await renderTextBoxChildren(tb, theme, themeName, context);
7233
+ if (rendered.some((element) => !(element instanceof Paragraph5))) {
7234
+ console.warn(
7235
+ '[core-docx] text-box renderAs "shape" requires paragraph-only content; falling back to table rendering.'
7236
+ );
7237
+ return { kind: "fallback", rendered };
7238
+ }
7239
+ const children = rendered;
7240
+ if (width.resolvedPercentage || height.resolvedPercentage) {
7241
+ console.warn(
7242
+ '[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.'
7243
+ );
7244
+ }
7245
+ const fill = style?.shading?.fill;
7246
+ if (ignoredSides.length > 0) {
7247
+ console.warn(
7248
+ `[core-docx] text-box renderAs "shape" has one uniform outline; using the first declared border side and ignoring ${ignoredSides.join(", ")}.`
7249
+ );
7250
+ }
7251
+ const dropOutline = Boolean(fill) && Boolean(outline);
7252
+ if (dropOutline) {
7253
+ console.warn(
7254
+ '[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.'
7255
+ );
7256
+ }
7257
+ const bodyProperties = shapeBodyProperties(style);
7258
+ const run = new WpsShapeRun({
7259
+ type: "wps",
7260
+ children,
7261
+ transformation: { width: width.pixels, height: height.pixels },
7262
+ ...fill && {
7263
+ solidFill: { type: "rgb", value: shapeColor(fill, theme) }
7264
+ },
7265
+ ...outline && !dropOutline && { outline },
7266
+ ...bodyProperties && { bodyProperties },
7267
+ // Absent `floating` makes it a `wp:inline` drawing.
7268
+ ...tb.props.floating && {
7269
+ floating: mapFloatingOptions(tb.props.floating, theme, themeName)
7270
+ }
7271
+ });
7272
+ return {
7273
+ kind: "shape",
7274
+ paragraphs: [
7275
+ new Paragraph5({ children: [run], spacing: { before: 0, after: 0 } })
7276
+ ]
7277
+ };
7278
+ }
7279
+ async function renderTextBoxComponent(component, theme, themeName, context) {
7280
+ if (!isTextBoxComponent(component)) return [];
7281
+ const tb = component;
7282
+ if (tb.props.renderAs === "shape") {
7283
+ const attempt = await renderTextBoxAsShape(tb, theme, themeName, context);
7284
+ if (attempt.kind === "shape") return attempt.paragraphs;
7285
+ return renderTextBoxAsTable(
7286
+ tb,
7287
+ theme,
7288
+ themeName,
7289
+ context,
7290
+ attempt.rendered
7291
+ );
7292
+ }
7293
+ return renderTextBoxAsTable(tb, theme, themeName, context);
7294
+ }
7003
7295
 
7004
7296
  // src/components/table.ts
7005
7297
  async function renderTableComponent(component, theme, themeName) {
@@ -7053,14 +7345,14 @@ async function renderTableComponent(component, theme, themeName) {
7053
7345
  import { Paragraph as Paragraph6, BookmarkStart, BookmarkEnd } from "docx";
7054
7346
 
7055
7347
  // src/core/sectionBookmarks.ts
7056
- import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
7348
+ import { AsyncLocalStorage as AsyncLocalStorage8 } from "async_hooks";
7057
7349
  var NESTED_LINK_ID_BASE = 1e6;
7058
7350
  function createState3() {
7059
7351
  return { nextNested: 1, resolved: /* @__PURE__ */ new WeakMap() };
7060
7352
  }
7061
7353
  var SectionBookmarkRegistry = class {
7062
7354
  fallback = createState3();
7063
- scopes = new AsyncLocalStorage7();
7355
+ scopes = new AsyncLocalStorage8();
7064
7356
  get state() {
7065
7357
  return this.scopes.getStore() ?? this.fallback;
7066
7358
  }
@@ -7318,7 +7610,10 @@ function selectCachedEntries(collected, options) {
7318
7610
  continue;
7319
7611
  }
7320
7612
  if (entry.level < depthStart || entry.level > depthEnd) continue;
7321
- entries.push({ title: entry.title, level: entry.level });
7613
+ entries.push({
7614
+ title: entry.number ? `${headingNumberLabel(entry.number)} ${entry.title}` : entry.title,
7615
+ level: entry.level
7616
+ });
7322
7617
  }
7323
7618
  return entries;
7324
7619
  }
@@ -8077,6 +8372,61 @@ function computeSectionOrdinals(sections) {
8077
8372
 
8078
8373
  // src/core/collectTocHeadings.ts
8079
8374
  import { getStandardComponent } from "@json-to-office/shared-docx";
8375
+
8376
+ // src/utils/numberFormatting.ts
8377
+ var ROMAN_NUMERALS = [
8378
+ [1e3, "m"],
8379
+ [900, "cm"],
8380
+ [500, "d"],
8381
+ [400, "cd"],
8382
+ [100, "c"],
8383
+ [90, "xc"],
8384
+ [50, "l"],
8385
+ [40, "xl"],
8386
+ [10, "x"],
8387
+ [9, "ix"],
8388
+ [5, "v"],
8389
+ [4, "iv"],
8390
+ [1, "i"]
8391
+ ];
8392
+ function toRoman(value) {
8393
+ let remaining = value;
8394
+ let out = "";
8395
+ for (const [amount, glyph] of ROMAN_NUMERALS) {
8396
+ while (remaining >= amount) {
8397
+ out += glyph;
8398
+ remaining -= amount;
8399
+ }
8400
+ }
8401
+ return out;
8402
+ }
8403
+ function toLetters(value) {
8404
+ const index = (value - 1) % 26;
8405
+ const repeats = Math.floor((value - 1) / 26) + 1;
8406
+ return String.fromCharCode(97 + index).repeat(repeats);
8407
+ }
8408
+ function formatNumberForLevel(value, format2) {
8409
+ if (!Number.isFinite(value) || value < 1) return void 0;
8410
+ const n = Math.floor(value);
8411
+ switch (format2) {
8412
+ case "decimal":
8413
+ return String(n);
8414
+ case "lowerLetter":
8415
+ return toLetters(n);
8416
+ case "upperLetter":
8417
+ return toLetters(n).toUpperCase();
8418
+ case "lowerRoman":
8419
+ return toRoman(n);
8420
+ case "upperRoman":
8421
+ return toRoman(n).toUpperCase();
8422
+ default:
8423
+ return void 0;
8424
+ }
8425
+ }
8426
+
8427
+ // src/core/collectTocHeadings.ts
8428
+ var MAX_HEADING_LEVEL2 = 6;
8429
+ var MAX_LIST_LEVEL = 9;
8080
8430
  function normalizeEntryTitle(text) {
8081
8431
  return normalizeUnicodeText(text).replace(/(\*\*\*|___)([\s\S]*?)\1/g, "$2").replace(/(\*\*|__)([\s\S]*?)\1/g, "$2").replace(/(\*|_)([\s\S]*?)\1/g, "$2").trim();
8082
8432
  }
@@ -8095,9 +8445,97 @@ function styleEntryKey(props) {
8095
8445
  }
8096
8446
  return themeStyle;
8097
8447
  }
8098
- function collectTocHeadings(sections) {
8448
+ function levelStart(levels, level) {
8449
+ return levels[level]?.start ?? 1;
8450
+ }
8451
+ function collectDocumentOutline(sections) {
8099
8452
  const entries = [];
8453
+ const numberedItems = /* @__PURE__ */ new Map();
8100
8454
  const ordinals = computeSectionOrdinals(sections);
8455
+ const takenIds = /* @__PURE__ */ new Set();
8456
+ const headingCounters = new Array(MAX_HEADING_LEVEL2).fill(0);
8457
+ const listCounters = /* @__PURE__ */ new Map();
8458
+ const visitHeading = (component, props, sectionBookmarkId) => {
8459
+ const text = typeof props.text === "string" ? props.text : "";
8460
+ const title = normalizeEntryTitle(text);
8461
+ const level = typeof props.level === "number" ? props.level : 1;
8462
+ const styleLevel = level >= 1 && level <= MAX_HEADING_LEVEL2 ? level : 1;
8463
+ let full;
8464
+ let own;
8465
+ if (props.numbering === true) {
8466
+ headingCounters[styleLevel - 1] += 1;
8467
+ for (let deeper = styleLevel; deeper < MAX_HEADING_LEVEL2; deeper++) {
8468
+ headingCounters[deeper] = 0;
8469
+ }
8470
+ full = headingCounters.slice(0, styleLevel).join(".");
8471
+ own = String(headingCounters[styleLevel - 1]);
8472
+ }
8473
+ const explicitId = component.id;
8474
+ const bookmarkId = typeof explicitId === "string" && explicitId ? explicitId : dedupeBookmarkId(slugifyBookmarkText(text), (id) => takenIds.has(id));
8475
+ takenIds.add(bookmarkId);
8476
+ numberedItems.set(bookmarkId, {
8477
+ kind: "heading",
8478
+ text: title,
8479
+ ...full !== void 0 && { full, own }
8480
+ });
8481
+ if (title) {
8482
+ entries.push({
8483
+ title,
8484
+ level,
8485
+ sectionBookmarkId,
8486
+ ...full !== void 0 && { number: full }
8487
+ });
8488
+ }
8489
+ };
8490
+ const visitList = (props) => {
8491
+ const items = Array.isArray(props.items) ? props.items : [];
8492
+ if (items.length === 0) return;
8493
+ const reference = typeof props.reference === "string" && props.reference ? props.reference : void 0;
8494
+ const freshState = () => {
8495
+ const levels = resolveListLevels(
8496
+ props
8497
+ );
8498
+ return {
8499
+ levels,
8500
+ counters: Array.from(
8501
+ { length: MAX_LIST_LEVEL },
8502
+ (_, level) => levelStart(levels, level) - 1
8503
+ )
8504
+ };
8505
+ };
8506
+ let state;
8507
+ if (reference === void 0) {
8508
+ state = freshState();
8509
+ } else {
8510
+ state = listCounters.get(reference) ?? freshState();
8511
+ listCounters.set(reference, state);
8512
+ }
8513
+ for (const item of items) {
8514
+ const isObject = typeof item === "object" && item !== null;
8515
+ const raw = isObject ? item.text : item;
8516
+ const text = typeof raw === "string" ? raw : "";
8517
+ if (!text.trim() && !(isObject && item.revision)) continue;
8518
+ const rawLevel = isObject ? item.level : void 0;
8519
+ const level = typeof rawLevel === "number" && rawLevel >= 0 ? rawLevel : 0;
8520
+ if (level >= MAX_LIST_LEVEL) continue;
8521
+ state.counters[level] += 1;
8522
+ for (let deeper = level + 1; deeper < MAX_LIST_LEVEL; deeper++) {
8523
+ state.counters[deeper] = levelStart(state.levels, deeper) - 1;
8524
+ }
8525
+ const id = isObject ? item.id : void 0;
8526
+ if (typeof id !== "string" || !id) continue;
8527
+ takenIds.add(id);
8528
+ const number = formatNumberForLevel(
8529
+ state.counters[level],
8530
+ state.levels[level]?.format
8531
+ );
8532
+ numberedItems.set(id, {
8533
+ kind: "list-item",
8534
+ text: normalizeUnicodeText(text).trim(),
8535
+ ...number !== void 0 && { full: number, own: number }
8536
+ });
8537
+ }
8538
+ };
8101
8539
  sections.forEach((section, index) => {
8102
8540
  const ordinal = ordinals[index]?.ordinal;
8103
8541
  const sectionBookmarkId = ordinal ? globalSectionBookmarkRegistry.forLayoutSection(ordinal).id : void 0;
@@ -8105,15 +8543,11 @@ function collectTocHeadings(sections) {
8105
8543
  if (!isEnabled(component)) return;
8106
8544
  const props = component.props ?? {};
8107
8545
  if (component.name === "heading") {
8108
- const text = typeof props.text === "string" ? props.text : "";
8109
- const title = normalizeEntryTitle(text);
8110
- if (title) {
8111
- const level = typeof props.level === "number" ? props.level : 1;
8112
- entries.push({ title, level, sectionBookmarkId });
8113
- }
8546
+ visitHeading(component, props, sectionBookmarkId);
8114
8547
  return;
8115
8548
  }
8116
8549
  if (component.name === "paragraph") {
8550
+ if (typeof props.id === "string" && props.id) takenIds.add(props.id);
8117
8551
  const styleId = styleEntryKey(props);
8118
8552
  const text = typeof props.text === "string" ? props.text : "";
8119
8553
  const title = normalizeEntryTitle(text);
@@ -8122,13 +8556,17 @@ function collectTocHeadings(sections) {
8122
8556
  }
8123
8557
  return;
8124
8558
  }
8559
+ if (component.name === "list") {
8560
+ visitList(props);
8561
+ return;
8562
+ }
8125
8563
  if (!isContainer(component.name)) return;
8126
8564
  const children = component.children;
8127
8565
  if (Array.isArray(children)) children.forEach(visit);
8128
8566
  };
8129
8567
  section.components.forEach(visit);
8130
8568
  });
8131
- return entries;
8569
+ return { entries, numberedItems };
8132
8570
  }
8133
8571
 
8134
8572
  // src/core/render.ts
@@ -8181,7 +8619,13 @@ async function renderDocument(structure, layout, options) {
8181
8619
  // Footnote ids are document-scoped too: a reference resolved
8182
8620
  // against another render's counter points at the wrong body.
8183
8621
  globalNoteRegistry.runScoped(
8184
- () => renderDocumentScoped(structure, layout, options)
8622
+ () => (
8623
+ // Cross-reference targets are keyed by bookmark id, which
8624
+ // is only unique within one document.
8625
+ globalNumberedItemsRegistry.runScoped(
8626
+ () => renderDocumentScoped(structure, layout, options)
8627
+ )
8628
+ )
8185
8629
  )
8186
8630
  )
8187
8631
  )
@@ -8224,13 +8668,14 @@ async function renderDocumentScoped(structure, layout, options) {
8224
8668
  );
8225
8669
  }
8226
8670
  try {
8227
- const tocHeadings = collectTocHeadings(layout.sections);
8228
- if (tocHeadings.length > 0) {
8229
- context.tocHeadings = tocHeadings;
8671
+ const outline = collectDocumentOutline(layout.sections);
8672
+ if (outline.entries.length > 0) {
8673
+ context.tocHeadings = outline.entries;
8230
8674
  }
8675
+ globalNumberedItemsRegistry.seed(outline.numberedItems);
8231
8676
  } catch (error) {
8232
8677
  console.warn(
8233
- "[core-docx] TOC entry collection failed; the TOC field will rely on the reader refreshing it:",
8678
+ "[core-docx] Document outline collection failed; the TOC field will rely on the reader refreshing it:",
8234
8679
  error instanceof Error ? error.message : error
8235
8680
  );
8236
8681
  }