@json-to-office/core-docx 0.35.0 → 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 (45) 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/generator.d.ts.map +1 -1
  13. package/dist/core/render.d.ts +8 -2
  14. package/dist/core/render.d.ts.map +1 -1
  15. package/dist/index.js +690 -181
  16. package/dist/index.js.map +1 -1
  17. package/dist/plugin/createDocumentGenerator.d.ts.map +1 -1
  18. package/dist/plugin/example/index.js +687 -181
  19. package/dist/plugin/example/index.js.map +1 -1
  20. package/dist/templates/themes/apex.docx.theme.json +0 -3
  21. package/dist/templates/themes/corporate.docx.theme.json +0 -3
  22. package/dist/templates/themes/devportal.docx.theme.json +0 -3
  23. package/dist/templates/themes/index.d.ts +4 -0
  24. package/dist/templates/themes/index.d.ts.map +1 -1
  25. package/dist/templates/themes/minimal.docx.theme.json +0 -3
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/dist/utils/bookmarkRegistry.d.ts +14 -0
  28. package/dist/utils/bookmarkRegistry.d.ts.map +1 -1
  29. package/dist/utils/generationContext.d.ts +9 -0
  30. package/dist/utils/generationContext.d.ts.map +1 -1
  31. package/dist/utils/imageUtils.d.ts +7 -3
  32. package/dist/utils/imageUtils.d.ts.map +1 -1
  33. package/dist/utils/listLevels.d.ts +38 -0
  34. package/dist/utils/listLevels.d.ts.map +1 -0
  35. package/dist/utils/numberFormatting.d.ts +11 -0
  36. package/dist/utils/numberFormatting.d.ts.map +1 -0
  37. package/dist/utils/numberedItemsRegistry.d.ts +38 -0
  38. package/dist/utils/numberedItemsRegistry.d.ts.map +1 -0
  39. package/dist/utils/numberingConfig.d.ts +31 -0
  40. package/dist/utils/numberingConfig.d.ts.map +1 -1
  41. package/dist/utils/placeholderProcessor.d.ts +2 -2
  42. package/dist/utils/placeholderProcessor.d.ts.map +1 -1
  43. package/dist/utils/textParser.d.ts +6 -0
  44. package/dist/utils/textParser.d.ts.map +1 -1
  45. package/package.json +4 -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
  };
@@ -2665,10 +2653,68 @@ function resolveFromBaseDir(filePath) {
2665
2653
  if (!base || isAbsolute(filePath)) return filePath;
2666
2654
  return resolve(base, filePath);
2667
2655
  }
2656
+ var warningsStorage = new AsyncLocalStorage();
2657
+ function runWithWarnings(warnings, callback) {
2658
+ return warnings === void 0 ? callback() : warningsStorage.run(warnings, callback);
2659
+ }
2660
+ function reportWarning(component, code, message, context) {
2661
+ const warnings = warningsStorage.getStore();
2662
+ if (warnings) {
2663
+ warnings.push({
2664
+ component,
2665
+ message,
2666
+ severity: "warning",
2667
+ context: { code, ...context }
2668
+ });
2669
+ return;
2670
+ }
2671
+ console.warn(`[json-to-docx] [${code}] ${message}`);
2672
+ }
2668
2673
 
2669
2674
  // src/utils/imageUtils.ts
2670
2675
  init_widthUtils();
2671
2676
  import { ImageRun } from "docx";
2677
+ var SVG_RASTER_SCALE = 3;
2678
+ var SVG_MAX_EDGE_PX = 4096;
2679
+ var SVG_MIN_EDGE_PX = 16;
2680
+ var resvgModule;
2681
+ function loadResvg() {
2682
+ resvgModule ??= import("@resvg/resvg-js");
2683
+ return resvgModule;
2684
+ }
2685
+ function toRasterPx(px) {
2686
+ return Math.min(
2687
+ SVG_MAX_EDGE_PX,
2688
+ Math.max(SVG_MIN_EDGE_PX, Math.round(px * SVG_RASTER_SCALE))
2689
+ );
2690
+ }
2691
+ async function rasterizeSvgFallback(svg, transformation) {
2692
+ let Resvg;
2693
+ try {
2694
+ ({ Resvg } = await loadResvg());
2695
+ } catch (error) {
2696
+ reportWarning(
2697
+ "image",
2698
+ "IMAGE_SVG_RASTER_FAILED",
2699
+ `Could not load the SVG rasterizer, so inline SVG keeps a fallback that only Word 2016+ can draw: ${String(error)}`
2700
+ );
2701
+ return void 0;
2702
+ }
2703
+ try {
2704
+ const markup = svg.toString("utf-8");
2705
+ const probeImage = new Resvg(markup);
2706
+ const wide = probeImage.width / probeImage.height > transformation.width / transformation.height;
2707
+ const fitTo = wide ? { mode: "height", value: toRasterPx(transformation.height) } : { mode: "width", value: toRasterPx(transformation.width) };
2708
+ return Buffer.from(new Resvg(markup, { fitTo }).render().asPng());
2709
+ } catch (error) {
2710
+ reportWarning(
2711
+ "image",
2712
+ "IMAGE_SVG_RASTER_FAILED",
2713
+ `Could not rasterize inline SVG, so its fallback only renders in Word 2016+: ${String(error)}`
2714
+ );
2715
+ return void 0;
2716
+ }
2717
+ }
2672
2718
  function parseWidthValue(width, availableWidthPx) {
2673
2719
  if (typeof width === "number") {
2674
2720
  return width;
@@ -2778,17 +2824,18 @@ function detectImageType(imagePath, responseContentType) {
2778
2824
  if (typeFromExtension) return typeFromExtension;
2779
2825
  return "png";
2780
2826
  }
2781
- function createTypedImageRun(opts) {
2827
+ async function createTypedImageRun(opts) {
2782
2828
  const base = {
2783
2829
  data: opts.data,
2784
2830
  transformation: opts.transformation,
2785
2831
  ...opts.floating && { floating: opts.floating }
2786
2832
  };
2787
2833
  if (opts.type === "svg") {
2834
+ const raster = await rasterizeSvgFallback(opts.data, opts.transformation);
2788
2835
  return new ImageRun({
2789
2836
  type: "svg",
2790
2837
  ...base,
2791
- fallback: { type: "png", data: opts.data }
2838
+ fallback: { type: "png", data: raster ?? opts.data }
2792
2839
  });
2793
2840
  }
2794
2841
  return new ImageRun({ type: opts.type, ...base });
@@ -3693,8 +3740,51 @@ import {
3693
3740
  InternalHyperlink,
3694
3741
  FootnoteReferenceRun,
3695
3742
  EndnoteReferenceRun,
3743
+ NumberedItemReference,
3744
+ NumberedItemReferenceFormat,
3696
3745
  Tab
3697
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
3698
3788
  var FOOTNOTE_MARKER_REGEX = /\[\^([^\]\s]+)\]/;
3699
3789
  function escapeRegExp(s) {
3700
3790
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3910,13 +4000,59 @@ function splitNoteMarkers(text, noteRef, runs) {
3910
4000
  if (trailing) out.push(...runs(trailing));
3911
4001
  return out;
3912
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
+ }
3913
4049
  function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
3914
4050
  const normalizedText = normalizeUnicodeText(text);
3915
4051
  const runs = [];
3916
- const hyperlinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
4052
+ const tokenRegex = new RegExp(INLINE_TOKEN_REGEX, "g");
3917
4053
  let lastIndex = 0;
3918
4054
  let match;
3919
- while ((match = hyperlinkRegex.exec(normalizedText)) !== null) {
4055
+ while ((match = tokenRegex.exec(normalizedText)) !== null) {
3920
4056
  if (match.index > lastIndex) {
3921
4057
  const plainText = normalizedText.substring(lastIndex, match.index);
3922
4058
  if (plainText) {
@@ -3928,6 +4064,19 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
3928
4064
  runs.push(...plainRuns);
3929
4065
  }
3930
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
+ }
3931
4080
  const linkText = match[1];
3932
4081
  const linkUrl = match[2];
3933
4082
  const isInternal = linkUrl.startsWith("#");
@@ -3954,7 +4103,6 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
3954
4103
  })
3955
4104
  );
3956
4105
  }
3957
- lastIndex = match.index + match[0].length;
3958
4106
  }
3959
4107
  if (lastIndex < normalizedText.length) {
3960
4108
  const remainingText = normalizedText.substring(lastIndex);
@@ -4163,7 +4311,7 @@ function initializeBuiltinPlaceholders() {
4163
4311
  initializeBuiltinPlaceholders();
4164
4312
 
4165
4313
  // src/utils/revisionUtils.ts
4166
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
4314
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
4167
4315
 
4168
4316
  // src/utils/componentAnnotations.ts
4169
4317
  function isRecord(value) {
@@ -4214,7 +4362,7 @@ var DEFAULT_REVISION_AUTHOR = "json-to-office";
4214
4362
  var DEFAULT_REVISION_DATE = "1970-01-01T00:00:00Z";
4215
4363
  var RevisionIdRegistry = class {
4216
4364
  fallbackCounter = 0;
4217
- scopes = new AsyncLocalStorage2();
4365
+ scopes = new AsyncLocalStorage3();
4218
4366
  runScoped(callback) {
4219
4367
  return this.scopes.run({ counter: 0 }, callback);
4220
4368
  }
@@ -4496,10 +4644,21 @@ init_styles();
4496
4644
  init_defaults();
4497
4645
 
4498
4646
  // src/utils/bookmarkRegistry.ts
4499
- 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
+ }
4500
4659
  var BookmarkRegistry = class {
4501
4660
  fallback = { bookmarks: /* @__PURE__ */ new Map() };
4502
- scopes = new AsyncLocalStorage3();
4661
+ scopes = new AsyncLocalStorage4();
4503
4662
  get state() {
4504
4663
  return this.scopes.getStore() ?? this.fallback;
4505
4664
  }
@@ -4523,13 +4682,10 @@ var BookmarkRegistry = class {
4523
4682
  * Converts text to a URL-friendly format
4524
4683
  */
4525
4684
  generateId(text, _type = "bookmark") {
4526
- const baseId = text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").substring(0, 40);
4527
- let id = baseId;
4528
- let attempt = 0;
4529
- while (this.state.bookmarks.has(id) && attempt < 100) {
4530
- id = `${baseId}-${++attempt}`;
4531
- }
4532
- return id;
4685
+ return dedupeBookmarkId(
4686
+ slugifyBookmarkText(text),
4687
+ (id) => this.state.bookmarks.has(id)
4688
+ );
4533
4689
  }
4534
4690
  /**
4535
4691
  * Check if a bookmark exists
@@ -4581,7 +4737,7 @@ import {
4581
4737
 
4582
4738
  // src/utils/commentRegistry.ts
4583
4739
  import { Paragraph, TextRun as TextRun4 } from "docx";
4584
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
4740
+ import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4585
4741
  var DEFAULT_COMMENT_AUTHOR = "json-to-office";
4586
4742
  var DEFAULT_COMMENT_DATE = "1970-01-01T00:00:00Z";
4587
4743
  function createState() {
@@ -4596,7 +4752,7 @@ function bodyParagraphs(text) {
4596
4752
  }
4597
4753
  var CommentRegistry = class {
4598
4754
  fallback = createState();
4599
- scopes = new AsyncLocalStorage4();
4755
+ scopes = new AsyncLocalStorage5();
4600
4756
  get state() {
4601
4757
  return this.scopes.getStore() ?? this.fallback;
4602
4758
  }
@@ -4691,7 +4847,7 @@ function closeCommentRange(ids) {
4691
4847
 
4692
4848
  // src/utils/noteRegistry.ts
4693
4849
  import { Paragraph as Paragraph2, TextRun as TextRun6 } from "docx";
4694
- import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4850
+ import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
4695
4851
  function createState2() {
4696
4852
  return {
4697
4853
  footnoteCounter: 0,
@@ -4710,7 +4866,7 @@ function bodyParagraphs2(text, style) {
4710
4866
  }
4711
4867
  var NoteRegistry = class {
4712
4868
  fallback = createState2();
4713
- scopes = new AsyncLocalStorage5();
4869
+ scopes = new AsyncLocalStorage6();
4714
4870
  get state() {
4715
4871
  return this.scopes.getStore() ?? this.fallback;
4716
4872
  }
@@ -5066,7 +5222,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5066
5222
  if (commentAnchor) {
5067
5223
  children.push(...commentAnchor.start);
5068
5224
  }
5069
- const hasDecorators = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText);
5225
+ const hasInlineSyntax = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText) || hasCrossReference(normalizedText);
5070
5226
  const headingHasWeightRequest = options.fontWeight != null || options.bold === true;
5071
5227
  const headingEffectiveFamily = options.fontFamily ?? (headingHasWeightRequest ? resolveFontFamily(theme, "heading") : void 0);
5072
5228
  const headingWeighted = applyFontWeightAlias({
@@ -5133,7 +5289,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5133
5289
  "heading"
5134
5290
  );
5135
5291
  const headingTextChildren = [];
5136
- if (hasDecorators) {
5292
+ if (hasInlineSyntax) {
5137
5293
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
5138
5294
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
5139
5295
  enableHyperlinks: true,
@@ -5150,7 +5306,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5150
5306
  })
5151
5307
  );
5152
5308
  } else {
5153
- if (hasDecorators) {
5309
+ if (hasInlineSyntax) {
5154
5310
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
5155
5311
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
5156
5312
  enableHyperlinks: true,
@@ -5172,7 +5328,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
5172
5328
  spacing: hasExplicitSpacing ? spacing : void 0,
5173
5329
  ...options.keepNext !== void 0 && { keepNext: options.keepNext },
5174
5330
  ...options.keepLines !== void 0 && { keepLines: options.keepLines },
5175
- ...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 }
5176
5335
  });
5177
5336
  }
5178
5337
  async function createImage(path4, theme, themeName, options = {}) {
@@ -5217,7 +5376,7 @@ async function createImage(path4, theme, themeName, options = {}) {
5217
5376
  const { mapFloatingOptions: mapFloatingOptions2 } = await Promise.resolve().then(() => (init_docxImagePositioning(), docxImagePositioning_exports));
5218
5377
  const floating = isFloating ? mapFloatingOptions2(options.floating, theme, themeName) : void 0;
5219
5378
  const imageType = detectImageType(imagePath, responseContentType);
5220
- const imageRun = createTypedImageRun({
5379
+ const imageRun = await createTypedImageRun({
5221
5380
  type: imageType,
5222
5381
  data: imageBuffer,
5223
5382
  transformation: { width: dimensions.width, height: dimensions.height },
@@ -5332,9 +5491,17 @@ function createList(items, _theme, _themeName, options = {}) {
5332
5491
  } else if (options.spacing?.item) {
5333
5492
  spacing.after = pointsToTwips(options.spacing.item);
5334
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
+ }
5335
5502
  const paragraphChildren = [
5336
5503
  ...commentAnchor && index === firstRendered ? commentAnchor.start : [],
5337
- ...textRuns,
5504
+ ...itemContent,
5338
5505
  ...commentAnchor && index === lastRendered ? closeCommentRange(commentAnchor.ids) : []
5339
5506
  ];
5340
5507
  const paragraph = new Paragraph3({
@@ -5762,7 +5929,7 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5762
5929
  // fallback height
5763
5930
  );
5764
5931
  const imgType = detectImageType(imageSource, imageResult.contentType);
5765
- const imageRun = createTypedImageRun({
5932
+ const imageRun = await createTypedImageRun({
5766
5933
  type: imgType,
5767
5934
  data: imageResult.buffer,
5768
5935
  transformation: {
@@ -6177,54 +6344,14 @@ function createFooterElement(children, _options) {
6177
6344
  });
6178
6345
  }
6179
6346
 
6180
- // src/components/heading.ts
6181
- function renderHeadingComponent(component, theme, themeName) {
6182
- if (!isHeadingComponent(component)) return [];
6183
- const config = component.props;
6184
- const bookmarkId = component.id || globalBookmarkRegistry.generateId(config.text, "heading");
6185
- const header = createHeading(
6186
- config.text,
6187
- config.level || 1,
6188
- theme,
6189
- themeName,
6190
- {
6191
- alignment: config.alignment,
6192
- spacing: config.spacing,
6193
- lineSpacing: config.lineSpacing,
6194
- columnBreak: config.columnBreak,
6195
- // Local font overrides
6196
- fontFamily: config.font?.family,
6197
- fontSize: config.font?.size,
6198
- fontColor: config.font?.color,
6199
- bold: config.font?.bold,
6200
- fontWeight: config.font?.fontWeight,
6201
- italic: config.font?.italic,
6202
- underline: config.font?.underline,
6203
- scale: config.font?.scale,
6204
- characterSpacing: config.font?.characterSpacing,
6205
- // Proofing: local language override + no-proof toggle + known-words list
6206
- language: config.language,
6207
- noProof: config.noProof,
6208
- noProofWords: config.noProofWords,
6209
- // Pagination control
6210
- keepNext: config.keepNext,
6211
- keepLines: config.keepLines,
6212
- // Paragraph indentation (w:ind) in twips
6213
- indent: config.indent,
6214
- // Bookmark ID for internal linking
6215
- bookmarkId,
6216
- // Tracked-change segments (rendered as native Word revisions)
6217
- revision: config.revision,
6218
- // Review comment anchored to this heading's text
6219
- comment: config.comment
6220
- }
6221
- );
6222
- return [header];
6223
- }
6224
-
6225
6347
  // src/utils/numberingConfig.ts
6226
- import { AlignmentType as AlignmentType3, convertInchesToTwip, LevelFormat } from "docx";
6227
- 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";
6228
6355
  var LEVEL_FORMAT_MAP = {
6229
6356
  decimal: LevelFormat.DECIMAL,
6230
6357
  upperRoman: LevelFormat.UPPER_ROMAN,
@@ -6352,12 +6479,37 @@ function createNumberingConfig(config) {
6352
6479
  levels
6353
6480
  };
6354
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
+ }
6355
6507
  var NumberingRegistry = class {
6356
6508
  fallback = {
6357
6509
  configs: /* @__PURE__ */ new Map(),
6358
6510
  counter: 0
6359
6511
  };
6360
- scopes = new AsyncLocalStorage6();
6512
+ scopes = new AsyncLocalStorage7();
6361
6513
  get state() {
6362
6514
  return this.scopes.getStore() ?? this.fallback;
6363
6515
  }
@@ -6407,6 +6559,63 @@ var NumberingRegistry = class {
6407
6559
  };
6408
6560
  var globalNumberingRegistry = new NumberingRegistry();
6409
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
+
6410
6619
  // src/components/paragraph.ts
6411
6620
  function parseMarkdownList(text) {
6412
6621
  const lines = text.split("\n");
@@ -6546,8 +6755,7 @@ function renderParagraphComponent(component, theme, themeName) {
6546
6755
  return [text];
6547
6756
  }
6548
6757
 
6549
- // src/components/list.ts
6550
- init_colorUtils();
6758
+ // src/utils/listLevels.ts
6551
6759
  function createLevelsFromSimplifiedProps(props) {
6552
6760
  const levels = [];
6553
6761
  let format2;
@@ -6594,18 +6802,8 @@ function createLevelsFromSimplifiedProps(props) {
6594
6802
  }
6595
6803
  return levels;
6596
6804
  }
6597
- function resolveMarkerFonts(levels, theme) {
6598
- return levels.map((level) => {
6599
- const font = level.font;
6600
- if (!font?.color) return level;
6601
- return {
6602
- ...level,
6603
- font: { ...font, color: resolveColor(font.color, theme) }
6604
- };
6605
- });
6606
- }
6607
6805
  function applyListStart(levels, start) {
6608
- if (start === void 0) return levels;
6806
+ if (start === void 0) return [...levels];
6609
6807
  return levels.map(
6610
6808
  (level) => level.level === 0 && level.start === void 0 ? { ...level, start } : level
6611
6809
  );
@@ -6663,28 +6861,37 @@ function fillMissingLevels(levels, maxLevel) {
6663
6861
  }
6664
6862
  return result;
6665
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
+ }
6666
6887
  function renderListComponent(component, theme, themeName) {
6667
6888
  if (!isListComponent(component)) return [];
6668
6889
  const resolvedConfig = component.props;
6669
- const maxLevel = getMaxLevelFromItems(resolvedConfig.items);
6670
6890
  const reference = resolvedConfig.reference || globalNumberingRegistry.generateReference("list");
6671
6891
  if (!globalNumberingRegistry.has(reference)) {
6672
- let levels;
6673
- if (resolvedConfig.levels && resolvedConfig.levels.length > 0) {
6674
- levels = fillMissingLevels(
6675
- applyListStart(
6676
- resolvedConfig.levels,
6677
- resolvedConfig.start
6678
- ),
6679
- maxLevel
6680
- );
6681
- } else {
6682
- const baseLevels = createLevelsFromSimplifiedProps(resolvedConfig);
6683
- levels = fillMissingLevels(baseLevels, maxLevel);
6684
- }
6685
6892
  const config = {
6686
6893
  reference,
6687
- levels: resolveMarkerFonts(levels, theme)
6894
+ levels: resolveMarkerFonts(resolveListLevels(resolvedConfig), theme)
6688
6895
  };
6689
6896
  const numberingConfig = createNumberingConfig(config);
6690
6897
  globalNumberingRegistry.register(numberingConfig);
@@ -6724,6 +6931,7 @@ async function renderImageComponent(component, theme, themeName) {
6724
6931
 
6725
6932
  // src/components/text-box.ts
6726
6933
  import {
6934
+ Paragraph as Paragraph5,
6727
6935
  Table as Table3,
6728
6936
  TableRow as TableRow2,
6729
6937
  TableCell as TableCell2,
@@ -6732,7 +6940,8 @@ import {
6732
6940
  RelativeHorizontalPosition,
6733
6941
  RelativeVerticalPosition,
6734
6942
  OverlapType,
6735
- TableLayoutType as TableLayoutType2
6943
+ TableLayoutType as TableLayoutType2,
6944
+ WpsShapeRun
6736
6945
  } from "docx";
6737
6946
 
6738
6947
  // src/styles/utils/borderUtils.ts
@@ -6809,6 +7018,8 @@ function buildCellOptions(children, styleCfg, theme) {
6809
7018
  }
6810
7019
 
6811
7020
  // src/components/text-box.ts
7021
+ init_colorUtils();
7022
+ init_docxImagePositioning();
6812
7023
  init_widthUtils();
6813
7024
  function mapTableFloatOptions(floating, theme, themeName) {
6814
7025
  if (!floating) return void 0;
@@ -6864,26 +7075,23 @@ function mapTableFloatOptions(floating, theme, themeName) {
6864
7075
  opt.overlap = OverlapType.OVERLAP;
6865
7076
  return opt;
6866
7077
  }
6867
- async function renderTextBoxComponent(component, theme, themeName, _context) {
6868
- if (!isTextBoxComponent(component)) return [];
6869
- 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) {
6870
7092
  const isInline = !tb.props.floating;
6871
- const childComponents = tb.children || [];
6872
7093
  if (isInline) {
6873
- const cellChildren2 = [];
6874
- const childContext2 = {
6875
- ..._context,
6876
- parent: tb
6877
- };
6878
- for (const child of childComponents) {
6879
- const rendered = await renderComponent(
6880
- child,
6881
- theme,
6882
- themeName,
6883
- childContext2
6884
- );
6885
- cellChildren2.push(...rendered);
6886
- }
7094
+ const cellChildren2 = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
6887
7095
  const styleCfg2 = tb.props.style;
6888
7096
  const cellOpts2 = buildCellOptions(cellChildren2, styleCfg2, theme);
6889
7097
  const row2 = new TableRow2({ children: [new TableCell2(cellOpts2)] });
@@ -6896,20 +7104,7 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
6896
7104
  });
6897
7105
  return [table2];
6898
7106
  }
6899
- const cellChildren = [];
6900
- const childContext = {
6901
- ..._context,
6902
- parent: tb
6903
- };
6904
- for (const child of childComponents) {
6905
- const rendered = await renderComponent(
6906
- child,
6907
- theme,
6908
- themeName,
6909
- childContext
6910
- );
6911
- cellChildren.push(...rendered);
6912
- }
7107
+ const cellChildren = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
6913
7108
  const styleCfg = tb.props.style;
6914
7109
  const cellOpts = buildCellOptions(cellChildren, styleCfg, theme);
6915
7110
  const row = new TableRow2({
@@ -6941,6 +7136,162 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
6941
7136
  });
6942
7137
  return [table];
6943
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
+ }
6944
7295
 
6945
7296
  // src/components/table.ts
6946
7297
  async function renderTableComponent(component, theme, themeName) {
@@ -6994,14 +7345,14 @@ async function renderTableComponent(component, theme, themeName) {
6994
7345
  import { Paragraph as Paragraph6, BookmarkStart, BookmarkEnd } from "docx";
6995
7346
 
6996
7347
  // src/core/sectionBookmarks.ts
6997
- import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
7348
+ import { AsyncLocalStorage as AsyncLocalStorage8 } from "async_hooks";
6998
7349
  var NESTED_LINK_ID_BASE = 1e6;
6999
7350
  function createState3() {
7000
7351
  return { nextNested: 1, resolved: /* @__PURE__ */ new WeakMap() };
7001
7352
  }
7002
7353
  var SectionBookmarkRegistry = class {
7003
7354
  fallback = createState3();
7004
- scopes = new AsyncLocalStorage7();
7355
+ scopes = new AsyncLocalStorage8();
7005
7356
  get state() {
7006
7357
  return this.scopes.getStore() ?? this.fallback;
7007
7358
  }
@@ -7259,7 +7610,10 @@ function selectCachedEntries(collected, options) {
7259
7610
  continue;
7260
7611
  }
7261
7612
  if (entry.level < depthStart || entry.level > depthEnd) continue;
7262
- 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
+ });
7263
7617
  }
7264
7618
  return entries;
7265
7619
  }
@@ -8018,6 +8372,61 @@ function computeSectionOrdinals(sections) {
8018
8372
 
8019
8373
  // src/core/collectTocHeadings.ts
8020
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;
8021
8430
  function normalizeEntryTitle(text) {
8022
8431
  return normalizeUnicodeText(text).replace(/(\*\*\*|___)([\s\S]*?)\1/g, "$2").replace(/(\*\*|__)([\s\S]*?)\1/g, "$2").replace(/(\*|_)([\s\S]*?)\1/g, "$2").trim();
8023
8432
  }
@@ -8036,9 +8445,97 @@ function styleEntryKey(props) {
8036
8445
  }
8037
8446
  return themeStyle;
8038
8447
  }
8039
- function collectTocHeadings(sections) {
8448
+ function levelStart(levels, level) {
8449
+ return levels[level]?.start ?? 1;
8450
+ }
8451
+ function collectDocumentOutline(sections) {
8040
8452
  const entries = [];
8453
+ const numberedItems = /* @__PURE__ */ new Map();
8041
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
+ };
8042
8539
  sections.forEach((section, index) => {
8043
8540
  const ordinal = ordinals[index]?.ordinal;
8044
8541
  const sectionBookmarkId = ordinal ? globalSectionBookmarkRegistry.forLayoutSection(ordinal).id : void 0;
@@ -8046,15 +8543,11 @@ function collectTocHeadings(sections) {
8046
8543
  if (!isEnabled(component)) return;
8047
8544
  const props = component.props ?? {};
8048
8545
  if (component.name === "heading") {
8049
- const text = typeof props.text === "string" ? props.text : "";
8050
- const title = normalizeEntryTitle(text);
8051
- if (title) {
8052
- const level = typeof props.level === "number" ? props.level : 1;
8053
- entries.push({ title, level, sectionBookmarkId });
8054
- }
8546
+ visitHeading(component, props, sectionBookmarkId);
8055
8547
  return;
8056
8548
  }
8057
8549
  if (component.name === "paragraph") {
8550
+ if (typeof props.id === "string" && props.id) takenIds.add(props.id);
8058
8551
  const styleId = styleEntryKey(props);
8059
8552
  const text = typeof props.text === "string" ? props.text : "";
8060
8553
  const title = normalizeEntryTitle(text);
@@ -8063,13 +8556,17 @@ function collectTocHeadings(sections) {
8063
8556
  }
8064
8557
  return;
8065
8558
  }
8559
+ if (component.name === "list") {
8560
+ visitList(props);
8561
+ return;
8562
+ }
8066
8563
  if (!isContainer(component.name)) return;
8067
8564
  const children = component.children;
8068
8565
  if (Array.isArray(children)) children.forEach(visit);
8069
8566
  };
8070
8567
  section.components.forEach(visit);
8071
8568
  });
8072
- return entries;
8569
+ return { entries, numberedItems };
8073
8570
  }
8074
8571
 
8075
8572
  // src/core/render.ts
@@ -8104,23 +8601,32 @@ function coreProperties(metadata) {
8104
8601
  async function renderDocument(structure, layout, options) {
8105
8602
  return runWithGenerationDate(
8106
8603
  structure.metadata.date,
8107
- () => runWithBaseDir(
8108
- options?.baseDir,
8109
- () => globalBookmarkRegistry.runScoped(
8110
- () => globalRevisionIdRegistry.runScoped(
8111
- () => globalNumberingRegistry.runScoped(
8112
- () => globalSectionBookmarkRegistry.runScoped(
8113
- () => (
8114
- // Comment ids are a separate OOXML namespace from w:ins/w:del,
8115
- // but they need the same per-render isolation: outside this nest
8116
- // concurrent generations would interleave counters and an anchor
8117
- // would point at another document's comment body.
8118
- globalCommentRegistry.runScoped(
8119
- () => (
8120
- // Footnote ids are document-scoped too: a reference resolved
8121
- // against another render's counter points at the wrong body.
8122
- globalNoteRegistry.runScoped(
8123
- () => renderDocumentScoped(structure, layout, options)
8604
+ () => runWithWarnings(
8605
+ options?.warnings,
8606
+ () => runWithBaseDir(
8607
+ options?.baseDir,
8608
+ () => globalBookmarkRegistry.runScoped(
8609
+ () => globalRevisionIdRegistry.runScoped(
8610
+ () => globalNumberingRegistry.runScoped(
8611
+ () => globalSectionBookmarkRegistry.runScoped(
8612
+ () => (
8613
+ // Comment ids are a separate OOXML namespace from w:ins/w:del,
8614
+ // but they need the same per-render isolation: outside this nest
8615
+ // concurrent generations would interleave counters and an anchor
8616
+ // would point at another document's comment body.
8617
+ globalCommentRegistry.runScoped(
8618
+ () => (
8619
+ // Footnote ids are document-scoped too: a reference resolved
8620
+ // against another render's counter points at the wrong body.
8621
+ globalNoteRegistry.runScoped(
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
+ )
8629
+ )
8124
8630
  )
8125
8631
  )
8126
8632
  )
@@ -8162,13 +8668,14 @@ async function renderDocumentScoped(structure, layout, options) {
8162
8668
  );
8163
8669
  }
8164
8670
  try {
8165
- const tocHeadings = collectTocHeadings(layout.sections);
8166
- if (tocHeadings.length > 0) {
8167
- context.tocHeadings = tocHeadings;
8671
+ const outline = collectDocumentOutline(layout.sections);
8672
+ if (outline.entries.length > 0) {
8673
+ context.tocHeadings = outline.entries;
8168
8674
  }
8675
+ globalNumberedItemsRegistry.seed(outline.numberedItems);
8169
8676
  } catch (error) {
8170
8677
  console.warn(
8171
- "[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:",
8172
8679
  error instanceof Error ? error.message : error
8173
8680
  );
8174
8681
  }
@@ -8340,7 +8847,7 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
8340
8847
  themeName
8341
8848
  );
8342
8849
  const imageType = detectImageType(imageSource, responseContentType);
8343
- const imageRun = createTypedImageRun({
8850
+ const imageRun = await createTypedImageRun({
8344
8851
  type: imageType,
8345
8852
  data: imageBuffer,
8346
8853
  transformation: {
@@ -8868,6 +9375,7 @@ async function generateDocumentWithCustomThemes(documentIn, customThemes, servic
8868
9375
  bypassCache: false,
8869
9376
  services,
8870
9377
  baseDir,
9378
+ warnings,
8871
9379
  ...visualFonts.length > 0 && { visualFonts }
8872
9380
  });
8873
9381
  return renderedDocument;
@@ -9850,6 +10358,7 @@ function createBuilderImpl(state) {
9850
10358
  services: state.services,
9851
10359
  bypassCache: !state.enableCache,
9852
10360
  baseDir: options?.baseDir ?? state.baseDir,
10361
+ warnings,
9853
10362
  ...visualFonts.length > 0 && { visualFonts }
9854
10363
  });
9855
10364
  const preservedDefinition = preserveSet ? {