@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
@@ -377,9 +377,6 @@ var init_minimal_docx_theme = __esm({
377
377
  },
378
378
  section: {
379
379
  pageBreak: false
380
- },
381
- heading: {
382
- numbering: false
383
380
  }
384
381
  }
385
382
  };
@@ -502,9 +499,6 @@ var init_corporate_docx_theme = __esm({
502
499
  },
503
500
  section: {
504
501
  pageBreak: false
505
- },
506
- heading: {
507
- numbering: false
508
502
  }
509
503
  }
510
504
  };
@@ -747,9 +741,6 @@ var init_apex_docx_theme = __esm({
747
741
  },
748
742
  section: {
749
743
  pageBreak: false
750
- },
751
- heading: {
752
- numbering: false
753
744
  }
754
745
  }
755
746
  };
@@ -883,9 +874,6 @@ var init_devportal_docx_theme = __esm({
883
874
  },
884
875
  section: {
885
876
  pageBreak: false
886
- },
887
- heading: {
888
- numbering: false
889
877
  }
890
878
  }
891
879
  };
@@ -2146,10 +2134,68 @@ function resolveFromBaseDir(filePath) {
2146
2134
  if (!base || isAbsolute(filePath)) return filePath;
2147
2135
  return resolve(base, filePath);
2148
2136
  }
2137
+ var warningsStorage = new AsyncLocalStorage();
2138
+ function runWithWarnings(warnings, callback) {
2139
+ return warnings === void 0 ? callback() : warningsStorage.run(warnings, callback);
2140
+ }
2141
+ function reportWarning(component, code, message, context) {
2142
+ const warnings = warningsStorage.getStore();
2143
+ if (warnings) {
2144
+ warnings.push({
2145
+ component,
2146
+ message,
2147
+ severity: "warning",
2148
+ context: { code, ...context }
2149
+ });
2150
+ return;
2151
+ }
2152
+ console.warn(`[json-to-docx] [${code}] ${message}`);
2153
+ }
2149
2154
 
2150
2155
  // src/utils/imageUtils.ts
2151
2156
  init_widthUtils();
2152
2157
  import { ImageRun } from "docx";
2158
+ var SVG_RASTER_SCALE = 3;
2159
+ var SVG_MAX_EDGE_PX = 4096;
2160
+ var SVG_MIN_EDGE_PX = 16;
2161
+ var resvgModule;
2162
+ function loadResvg() {
2163
+ resvgModule ??= import("@resvg/resvg-js");
2164
+ return resvgModule;
2165
+ }
2166
+ function toRasterPx(px) {
2167
+ return Math.min(
2168
+ SVG_MAX_EDGE_PX,
2169
+ Math.max(SVG_MIN_EDGE_PX, Math.round(px * SVG_RASTER_SCALE))
2170
+ );
2171
+ }
2172
+ async function rasterizeSvgFallback(svg, transformation) {
2173
+ let Resvg;
2174
+ try {
2175
+ ({ Resvg } = await loadResvg());
2176
+ } catch (error) {
2177
+ reportWarning(
2178
+ "image",
2179
+ "IMAGE_SVG_RASTER_FAILED",
2180
+ `Could not load the SVG rasterizer, so inline SVG keeps a fallback that only Word 2016+ can draw: ${String(error)}`
2181
+ );
2182
+ return void 0;
2183
+ }
2184
+ try {
2185
+ const markup = svg.toString("utf-8");
2186
+ const probeImage = new Resvg(markup);
2187
+ const wide = probeImage.width / probeImage.height > transformation.width / transformation.height;
2188
+ const fitTo = wide ? { mode: "height", value: toRasterPx(transformation.height) } : { mode: "width", value: toRasterPx(transformation.width) };
2189
+ return Buffer.from(new Resvg(markup, { fitTo }).render().asPng());
2190
+ } catch (error) {
2191
+ reportWarning(
2192
+ "image",
2193
+ "IMAGE_SVG_RASTER_FAILED",
2194
+ `Could not rasterize inline SVG, so its fallback only renders in Word 2016+: ${String(error)}`
2195
+ );
2196
+ return void 0;
2197
+ }
2198
+ }
2153
2199
  function parseWidthValue(width, availableWidthPx) {
2154
2200
  if (typeof width === "number") {
2155
2201
  return width;
@@ -2259,17 +2305,18 @@ function detectImageType(imagePath, responseContentType) {
2259
2305
  if (typeFromExtension) return typeFromExtension;
2260
2306
  return "png";
2261
2307
  }
2262
- function createTypedImageRun(opts) {
2308
+ async function createTypedImageRun(opts) {
2263
2309
  const base = {
2264
2310
  data: opts.data,
2265
2311
  transformation: opts.transformation,
2266
2312
  ...opts.floating && { floating: opts.floating }
2267
2313
  };
2268
2314
  if (opts.type === "svg") {
2315
+ const raster = await rasterizeSvgFallback(opts.data, opts.transformation);
2269
2316
  return new ImageRun({
2270
2317
  type: "svg",
2271
2318
  ...base,
2272
- fallback: { type: "png", data: opts.data }
2319
+ fallback: { type: "png", data: raster ?? opts.data }
2273
2320
  });
2274
2321
  }
2275
2322
  return new ImageRun({ type: opts.type, ...base });
@@ -2399,6 +2446,8 @@ import {
2399
2446
  InternalHyperlink as InternalHyperlink2,
2400
2447
  FootnoteReferenceRun as FootnoteReferenceRun2,
2401
2448
  EndnoteReferenceRun as EndnoteReferenceRun2,
2449
+ NumberedItemReference,
2450
+ NumberedItemReferenceFormat,
2402
2451
  Tab
2403
2452
  } from "docx";
2404
2453
 
@@ -2599,6 +2648,45 @@ function initializeBuiltinPlaceholders() {
2599
2648
  }
2600
2649
  initializeBuiltinPlaceholders();
2601
2650
 
2651
+ // src/utils/numberedItemsRegistry.ts
2652
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
2653
+ var NumberedItemsRegistry = class {
2654
+ fallback = {
2655
+ items: /* @__PURE__ */ new Map(),
2656
+ seeded: false
2657
+ };
2658
+ scopes = new AsyncLocalStorage2();
2659
+ get state() {
2660
+ return this.scopes.getStore() ?? this.fallback;
2661
+ }
2662
+ /** Run work with an isolated registry that follows its async call chain. */
2663
+ runScoped(callback) {
2664
+ return this.scopes.run({ items: /* @__PURE__ */ new Map(), seeded: false }, callback);
2665
+ }
2666
+ /** Replace the contents with the pre-pass result. */
2667
+ seed(items) {
2668
+ const state = this.state;
2669
+ state.items = new Map(items);
2670
+ state.seeded = true;
2671
+ }
2672
+ /**
2673
+ * False outside a render (a unit test calling `createText` directly). An
2674
+ * unresolved reference is then expected rather than an authoring mistake, so
2675
+ * the caller stays quiet about it.
2676
+ */
2677
+ isSeeded() {
2678
+ return this.state.seeded;
2679
+ }
2680
+ get(id) {
2681
+ return this.state.items.get(id);
2682
+ }
2683
+ clear() {
2684
+ this.state.items.clear();
2685
+ this.state.seeded = false;
2686
+ }
2687
+ };
2688
+ var globalNumberedItemsRegistry = new NumberedItemsRegistry();
2689
+
2602
2690
  // src/utils/textParser.ts
2603
2691
  var FOOTNOTE_MARKER_REGEX = /\[\^([^\]\s]+)\]/;
2604
2692
  function escapeRegExp(s) {
@@ -2815,13 +2903,59 @@ function splitNoteMarkers(text, noteRef, runs) {
2815
2903
  if (trailing) out.push(...runs(trailing));
2816
2904
  return out;
2817
2905
  }
2906
+ var CROSS_REFERENCE_PATTERN = "\\[@([^\\]\\s:]+)(?::(relative|no_context|full_context|none))?\\]";
2907
+ var INLINE_TOKEN_REGEX = `\\[([^\\]]+)\\]\\(([^)]+)\\)|${CROSS_REFERENCE_PATTERN}`;
2908
+ function hasCrossReference(text) {
2909
+ return new RegExp(CROSS_REFERENCE_PATTERN).test(text);
2910
+ }
2911
+ var REFERENCE_FORMATS = {
2912
+ relative: NumberedItemReferenceFormat.RELATIVE,
2913
+ no_context: NumberedItemReferenceFormat.NO_CONTEXT,
2914
+ full_context: NumberedItemReferenceFormat.FULL_CONTEXT,
2915
+ none: NumberedItemReferenceFormat.NONE
2916
+ };
2917
+ function createCrossReference(id, format2, token, baseStyle, options) {
2918
+ const info = globalNumberedItemsRegistry.get(id);
2919
+ if (!info) {
2920
+ if (globalNumberedItemsRegistry.isSeeded()) {
2921
+ console.warn(
2922
+ `[core-docx] Cross-reference ${token} has no target: no heading or list item declares the id "${id}". Rendering the token as literal text.`
2923
+ );
2924
+ }
2925
+ return buildTextRuns(
2926
+ token,
2927
+ buildRunCommonProps(baseStyle, { boldColor: options.boldColor }),
2928
+ { noProof: baseStyle.noProof, noProofWords: options.noProofWords }
2929
+ );
2930
+ }
2931
+ if (format2 === "none") {
2932
+ return [
2933
+ new NumberedItemReference(id, info.text, {
2934
+ hyperlink: true,
2935
+ referenceFormat: REFERENCE_FORMATS.none
2936
+ })
2937
+ ];
2938
+ }
2939
+ const cachedValue = format2 === "no_context" ? info.own : info.full;
2940
+ if (cachedValue === void 0) {
2941
+ console.warn(
2942
+ `[core-docx] Cross-reference ${token} targets an unnumbered ${info.kind} ("${id}"), so the field carries no cached number and reads blank until the reader updates fields. Use [@${id}:none] to reference its text instead.`
2943
+ );
2944
+ }
2945
+ return [
2946
+ new NumberedItemReference(id, cachedValue, {
2947
+ hyperlink: true,
2948
+ referenceFormat: REFERENCE_FORMATS[format2]
2949
+ })
2950
+ ];
2951
+ }
2818
2952
  function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
2819
2953
  const normalizedText = normalizeUnicodeText(text);
2820
2954
  const runs = [];
2821
- const hyperlinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
2955
+ const tokenRegex = new RegExp(INLINE_TOKEN_REGEX, "g");
2822
2956
  let lastIndex = 0;
2823
2957
  let match;
2824
- while ((match = hyperlinkRegex.exec(normalizedText)) !== null) {
2958
+ while ((match = tokenRegex.exec(normalizedText)) !== null) {
2825
2959
  if (match.index > lastIndex) {
2826
2960
  const plainText = normalizedText.substring(lastIndex, match.index);
2827
2961
  if (plainText) {
@@ -2833,6 +2967,19 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
2833
2967
  runs.push(...plainRuns);
2834
2968
  }
2835
2969
  }
2970
+ lastIndex = match.index + match[0].length;
2971
+ if (match[1] === void 0) {
2972
+ runs.push(
2973
+ ...createCrossReference(
2974
+ match[3],
2975
+ match[4] ?? "relative",
2976
+ match[0],
2977
+ baseStyle,
2978
+ options
2979
+ )
2980
+ );
2981
+ continue;
2982
+ }
2836
2983
  const linkText = match[1];
2837
2984
  const linkUrl = match[2];
2838
2985
  const isInternal = linkUrl.startsWith("#");
@@ -2859,7 +3006,6 @@ function parseTextWithHyperlinks(text, baseStyle = {}, options = {}) {
2859
3006
  })
2860
3007
  );
2861
3008
  }
2862
- lastIndex = match.index + match[0].length;
2863
3009
  }
2864
3010
  if (lastIndex < normalizedText.length) {
2865
3011
  const remainingText = normalizedText.substring(lastIndex);
@@ -3553,10 +3699,21 @@ function getStyleIdForLevel(level) {
3553
3699
  }
3554
3700
 
3555
3701
  // src/utils/bookmarkRegistry.ts
3556
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
3702
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3703
+ function slugifyBookmarkText(text) {
3704
+ return text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").substring(0, 40);
3705
+ }
3706
+ function dedupeBookmarkId(base, taken) {
3707
+ let id = base;
3708
+ let attempt = 0;
3709
+ while (taken(id) && attempt < 100) {
3710
+ id = `${base}-${++attempt}`;
3711
+ }
3712
+ return id;
3713
+ }
3557
3714
  var BookmarkRegistry = class {
3558
3715
  fallback = { bookmarks: /* @__PURE__ */ new Map() };
3559
- scopes = new AsyncLocalStorage2();
3716
+ scopes = new AsyncLocalStorage3();
3560
3717
  get state() {
3561
3718
  return this.scopes.getStore() ?? this.fallback;
3562
3719
  }
@@ -3580,13 +3737,10 @@ var BookmarkRegistry = class {
3580
3737
  * Converts text to a URL-friendly format
3581
3738
  */
3582
3739
  generateId(text, _type = "bookmark") {
3583
- const baseId = text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").substring(0, 40);
3584
- let id = baseId;
3585
- let attempt = 0;
3586
- while (this.state.bookmarks.has(id) && attempt < 100) {
3587
- id = `${baseId}-${++attempt}`;
3588
- }
3589
- return id;
3740
+ return dedupeBookmarkId(
3741
+ slugifyBookmarkText(text),
3742
+ (id) => this.state.bookmarks.has(id)
3743
+ );
3590
3744
  }
3591
3745
  /**
3592
3746
  * Check if a bookmark exists
@@ -3630,7 +3784,7 @@ var globalBookmarkRegistry = new BookmarkRegistry();
3630
3784
 
3631
3785
  // src/utils/revisionUtils.ts
3632
3786
  import { TextRun as TextRun3, InsertedTextRun, DeletedTextRun } from "docx";
3633
- import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
3787
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
3634
3788
 
3635
3789
  // src/utils/componentAnnotations.ts
3636
3790
  function isRecord(value) {
@@ -3681,7 +3835,7 @@ var DEFAULT_REVISION_AUTHOR = "json-to-office";
3681
3835
  var DEFAULT_REVISION_DATE = "1970-01-01T00:00:00Z";
3682
3836
  var RevisionIdRegistry = class {
3683
3837
  fallbackCounter = 0;
3684
- scopes = new AsyncLocalStorage3();
3838
+ scopes = new AsyncLocalStorage4();
3685
3839
  runScoped(callback) {
3686
3840
  return this.scopes.run({ counter: 0 }, callback);
3687
3841
  }
@@ -3792,7 +3946,7 @@ import {
3792
3946
 
3793
3947
  // src/utils/commentRegistry.ts
3794
3948
  import { Paragraph, TextRun as TextRun4 } from "docx";
3795
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
3949
+ import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
3796
3950
  var DEFAULT_COMMENT_AUTHOR = "json-to-office";
3797
3951
  var DEFAULT_COMMENT_DATE = "1970-01-01T00:00:00Z";
3798
3952
  function createState() {
@@ -3807,7 +3961,7 @@ function bodyParagraphs(text) {
3807
3961
  }
3808
3962
  var CommentRegistry = class {
3809
3963
  fallback = createState();
3810
- scopes = new AsyncLocalStorage4();
3964
+ scopes = new AsyncLocalStorage5();
3811
3965
  get state() {
3812
3966
  return this.scopes.getStore() ?? this.fallback;
3813
3967
  }
@@ -3902,7 +4056,7 @@ function closeCommentRange(ids) {
3902
4056
 
3903
4057
  // src/utils/noteRegistry.ts
3904
4058
  import { Paragraph as Paragraph2, TextRun as TextRun6 } from "docx";
3905
- import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
4059
+ import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
3906
4060
  function createState2() {
3907
4061
  return {
3908
4062
  footnoteCounter: 0,
@@ -3921,7 +4075,7 @@ function bodyParagraphs2(text, style) {
3921
4075
  }
3922
4076
  var NoteRegistry = class {
3923
4077
  fallback = createState2();
3924
- scopes = new AsyncLocalStorage5();
4078
+ scopes = new AsyncLocalStorage6();
3925
4079
  get state() {
3926
4080
  return this.scopes.getStore() ?? this.fallback;
3927
4081
  }
@@ -4277,7 +4431,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4277
4431
  if (commentAnchor) {
4278
4432
  children.push(...commentAnchor.start);
4279
4433
  }
4280
- const hasDecorators = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText);
4434
+ const hasInlineSyntax = /(\*\*\*|___|(\*\*|__)|(\*|_))/.test(normalizedText) || hasCrossReference(normalizedText);
4281
4435
  const headingHasWeightRequest = options.fontWeight != null || options.bold === true;
4282
4436
  const headingEffectiveFamily = options.fontFamily ?? (headingHasWeightRequest ? resolveFontFamily(theme, "heading") : void 0);
4283
4437
  const headingWeighted = applyFontWeightAlias({
@@ -4344,7 +4498,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4344
4498
  "heading"
4345
4499
  );
4346
4500
  const headingTextChildren = [];
4347
- if (hasDecorators) {
4501
+ if (hasInlineSyntax) {
4348
4502
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
4349
4503
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
4350
4504
  enableHyperlinks: true,
@@ -4361,7 +4515,7 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4361
4515
  })
4362
4516
  );
4363
4517
  } else {
4364
- if (hasDecorators) {
4518
+ if (hasInlineSyntax) {
4365
4519
  const textRuns = parseTextWithDecorators(normalizedText, baseTextStyle, {
4366
4520
  boldColor: options.boldColor ? resolveColor(options.boldColor, theme) : void 0,
4367
4521
  enableHyperlinks: true,
@@ -4383,7 +4537,10 @@ function createHeading(text, level, theme, _themeName, options = {}) {
4383
4537
  spacing: hasExplicitSpacing ? spacing : void 0,
4384
4538
  ...options.keepNext !== void 0 && { keepNext: options.keepNext },
4385
4539
  ...options.keepLines !== void 0 && { keepLines: options.keepLines },
4386
- ...options.indent && { indent: options.indent }
4540
+ ...options.indent && { indent: options.indent },
4541
+ // docx creates the concrete numbering instance for the reference itself,
4542
+ // in ParagraphProperties.prepForXml.
4543
+ ...options.numbering !== void 0 && { numbering: options.numbering }
4387
4544
  });
4388
4545
  }
4389
4546
  async function createImage(path3, theme, themeName, options = {}) {
@@ -4428,7 +4585,7 @@ async function createImage(path3, theme, themeName, options = {}) {
4428
4585
  const { mapFloatingOptions: mapFloatingOptions2 } = await Promise.resolve().then(() => (init_docxImagePositioning(), docxImagePositioning_exports));
4429
4586
  const floating = isFloating ? mapFloatingOptions2(options.floating, theme, themeName) : void 0;
4430
4587
  const imageType = detectImageType(imagePath, responseContentType);
4431
- const imageRun = createTypedImageRun({
4588
+ const imageRun = await createTypedImageRun({
4432
4589
  type: imageType,
4433
4590
  data: imageBuffer,
4434
4591
  transformation: { width: dimensions.width, height: dimensions.height },
@@ -4543,9 +4700,17 @@ function createList(items, _theme, _themeName, options = {}) {
4543
4700
  } else if (options.spacing?.item) {
4544
4701
  spacing.after = pointsToTwips(options.spacing.item);
4545
4702
  }
4703
+ const itemId = typeof item === "object" ? item.id : void 0;
4704
+ let itemContent = textRuns;
4705
+ if (itemId) {
4706
+ globalBookmarkRegistry.register(itemId, itemText, "list-item");
4707
+ itemContent = [
4708
+ new Bookmark({ id: itemId, children: textRuns })
4709
+ ];
4710
+ }
4546
4711
  const paragraphChildren = [
4547
4712
  ...commentAnchor && index === firstRendered ? commentAnchor.start : [],
4548
- ...textRuns,
4713
+ ...itemContent,
4549
4714
  ...commentAnchor && index === lastRendered ? closeCommentRange(commentAnchor.ids) : []
4550
4715
  ];
4551
4716
  const paragraph = new Paragraph3({
@@ -4973,7 +5138,7 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
4973
5138
  // fallback height
4974
5139
  );
4975
5140
  const imgType = detectImageType(imageSource, imageResult.contentType);
4976
- const imageRun = createTypedImageRun({
5141
+ const imageRun = await createTypedImageRun({
4977
5142
  type: imgType,
4978
5143
  data: imageResult.buffer,
4979
5144
  transformation: {
@@ -6748,54 +6913,14 @@ async function renderComponentWithCache(component, theme, themeName, context, by
6748
6913
  return rendered;
6749
6914
  }
6750
6915
 
6751
- // src/components/heading.ts
6752
- function renderHeadingComponent(component, theme, themeName) {
6753
- if (!isHeadingComponent(component)) return [];
6754
- const config = component.props;
6755
- const bookmarkId = component.id || globalBookmarkRegistry.generateId(config.text, "heading");
6756
- const header = createHeading(
6757
- config.text,
6758
- config.level || 1,
6759
- theme,
6760
- themeName,
6761
- {
6762
- alignment: config.alignment,
6763
- spacing: config.spacing,
6764
- lineSpacing: config.lineSpacing,
6765
- columnBreak: config.columnBreak,
6766
- // Local font overrides
6767
- fontFamily: config.font?.family,
6768
- fontSize: config.font?.size,
6769
- fontColor: config.font?.color,
6770
- bold: config.font?.bold,
6771
- fontWeight: config.font?.fontWeight,
6772
- italic: config.font?.italic,
6773
- underline: config.font?.underline,
6774
- scale: config.font?.scale,
6775
- characterSpacing: config.font?.characterSpacing,
6776
- // Proofing: local language override + no-proof toggle + known-words list
6777
- language: config.language,
6778
- noProof: config.noProof,
6779
- noProofWords: config.noProofWords,
6780
- // Pagination control
6781
- keepNext: config.keepNext,
6782
- keepLines: config.keepLines,
6783
- // Paragraph indentation (w:ind) in twips
6784
- indent: config.indent,
6785
- // Bookmark ID for internal linking
6786
- bookmarkId,
6787
- // Tracked-change segments (rendered as native Word revisions)
6788
- revision: config.revision,
6789
- // Review comment anchored to this heading's text
6790
- comment: config.comment
6791
- }
6792
- );
6793
- return [header];
6794
- }
6795
-
6796
6916
  // src/utils/numberingConfig.ts
6797
- import { AlignmentType as AlignmentType3, convertInchesToTwip, LevelFormat } from "docx";
6798
- import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
6917
+ import {
6918
+ AlignmentType as AlignmentType3,
6919
+ convertInchesToTwip,
6920
+ LevelFormat,
6921
+ LevelSuffix
6922
+ } from "docx";
6923
+ import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
6799
6924
  var LEVEL_FORMAT_MAP = {
6800
6925
  decimal: LevelFormat.DECIMAL,
6801
6926
  upperRoman: LevelFormat.UPPER_ROMAN,
@@ -6923,12 +7048,37 @@ function createNumberingConfig(config) {
6923
7048
  levels
6924
7049
  };
6925
7050
  }
7051
+ var HEADING_NUMBERING_REFERENCE = "jto-heading-numbering";
7052
+ function createHeadingNumberingConfig() {
7053
+ const levels = [];
7054
+ for (let level = 0; level < 6; level++) {
7055
+ const text = Array.from({ length: level + 1 }, (_, i) => `%${i + 1}`).join(".") + ".";
7056
+ levels.push({
7057
+ level,
7058
+ format: LevelFormat.DECIMAL,
7059
+ text,
7060
+ alignment: AlignmentType3.LEFT,
7061
+ start: 1,
7062
+ // A space, not the default tab: a tab would push the heading text to the
7063
+ // next tab stop and misalign it against unnumbered headings.
7064
+ suffix: LevelSuffix.SPACE,
7065
+ style: {
7066
+ paragraph: { indent: { left: 0, hanging: 0 } },
7067
+ style: `Heading${level + 1}`
7068
+ }
7069
+ });
7070
+ }
7071
+ return { reference: HEADING_NUMBERING_REFERENCE, levels };
7072
+ }
7073
+ function headingNumberLabel(number) {
7074
+ return `${number}.`;
7075
+ }
6926
7076
  var NumberingRegistry = class {
6927
7077
  fallback = {
6928
7078
  configs: /* @__PURE__ */ new Map(),
6929
7079
  counter: 0
6930
7080
  };
6931
- scopes = new AsyncLocalStorage6();
7081
+ scopes = new AsyncLocalStorage7();
6932
7082
  get state() {
6933
7083
  return this.scopes.getStore() ?? this.fallback;
6934
7084
  }
@@ -6978,6 +7128,63 @@ var NumberingRegistry = class {
6978
7128
  };
6979
7129
  var globalNumberingRegistry = new NumberingRegistry();
6980
7130
 
7131
+ // src/components/heading.ts
7132
+ var MAX_HEADING_LEVEL = 6;
7133
+ function headingNumbering(numbering, level) {
7134
+ if (numbering === false) return false;
7135
+ if (numbering !== true) return void 0;
7136
+ if (!globalNumberingRegistry.has(HEADING_NUMBERING_REFERENCE)) {
7137
+ globalNumberingRegistry.register(createHeadingNumberingConfig());
7138
+ }
7139
+ const styleLevel = level >= 1 && level <= MAX_HEADING_LEVEL ? level : 1;
7140
+ return { reference: HEADING_NUMBERING_REFERENCE, level: styleLevel - 1 };
7141
+ }
7142
+ function renderHeadingComponent(component, theme, themeName) {
7143
+ if (!isHeadingComponent(component)) return [];
7144
+ const config = component.props;
7145
+ const bookmarkId = component.id || globalBookmarkRegistry.generateId(config.text, "heading");
7146
+ const header = createHeading(
7147
+ config.text,
7148
+ config.level || 1,
7149
+ theme,
7150
+ themeName,
7151
+ {
7152
+ alignment: config.alignment,
7153
+ spacing: config.spacing,
7154
+ lineSpacing: config.lineSpacing,
7155
+ columnBreak: config.columnBreak,
7156
+ // Local font overrides
7157
+ fontFamily: config.font?.family,
7158
+ fontSize: config.font?.size,
7159
+ fontColor: config.font?.color,
7160
+ bold: config.font?.bold,
7161
+ fontWeight: config.font?.fontWeight,
7162
+ italic: config.font?.italic,
7163
+ underline: config.font?.underline,
7164
+ scale: config.font?.scale,
7165
+ characterSpacing: config.font?.characterSpacing,
7166
+ // Proofing: local language override + no-proof toggle + known-words list
7167
+ language: config.language,
7168
+ noProof: config.noProof,
7169
+ noProofWords: config.noProofWords,
7170
+ // Pagination control
7171
+ keepNext: config.keepNext,
7172
+ keepLines: config.keepLines,
7173
+ // Paragraph indentation (w:ind) in twips
7174
+ indent: config.indent,
7175
+ // Bookmark ID for internal linking
7176
+ bookmarkId,
7177
+ // Auto-numbering (1., 1.1., …) through the shared heading definition
7178
+ numbering: headingNumbering(config.numbering, config.level || 1),
7179
+ // Tracked-change segments (rendered as native Word revisions)
7180
+ revision: config.revision,
7181
+ // Review comment anchored to this heading's text
7182
+ comment: config.comment
7183
+ }
7184
+ );
7185
+ return [header];
7186
+ }
7187
+
6981
7188
  // src/components/paragraph.ts
6982
7189
  function parseMarkdownList(text) {
6983
7190
  const lines = text.split("\n");
@@ -7117,8 +7324,7 @@ function renderParagraphComponent(component, theme, themeName) {
7117
7324
  return [text];
7118
7325
  }
7119
7326
 
7120
- // src/components/list.ts
7121
- init_colorUtils();
7327
+ // src/utils/listLevels.ts
7122
7328
  function createLevelsFromSimplifiedProps(props) {
7123
7329
  const levels = [];
7124
7330
  let format2;
@@ -7165,18 +7371,8 @@ function createLevelsFromSimplifiedProps(props) {
7165
7371
  }
7166
7372
  return levels;
7167
7373
  }
7168
- function resolveMarkerFonts(levels, theme) {
7169
- return levels.map((level) => {
7170
- const font = level.font;
7171
- if (!font?.color) return level;
7172
- return {
7173
- ...level,
7174
- font: { ...font, color: resolveColor(font.color, theme) }
7175
- };
7176
- });
7177
- }
7178
7374
  function applyListStart(levels, start) {
7179
- if (start === void 0) return levels;
7375
+ if (start === void 0) return [...levels];
7180
7376
  return levels.map(
7181
7377
  (level) => level.level === 0 && level.start === void 0 ? { ...level, start } : level
7182
7378
  );
@@ -7234,28 +7430,37 @@ function fillMissingLevels(levels, maxLevel) {
7234
7430
  }
7235
7431
  return result;
7236
7432
  }
7433
+ function resolveListLevels(props) {
7434
+ const maxLevel = getMaxLevelFromItems(props.items);
7435
+ if (props.levels && props.levels.length > 0) {
7436
+ return fillMissingLevels(
7437
+ applyListStart(props.levels, props.start),
7438
+ maxLevel
7439
+ );
7440
+ }
7441
+ return fillMissingLevels(createLevelsFromSimplifiedProps(props), maxLevel);
7442
+ }
7443
+
7444
+ // src/components/list.ts
7445
+ init_colorUtils();
7446
+ function resolveMarkerFonts(levels, theme) {
7447
+ return levels.map((level) => {
7448
+ const font = level.font;
7449
+ if (!font?.color) return level;
7450
+ return {
7451
+ ...level,
7452
+ font: { ...font, color: resolveColor(font.color, theme) }
7453
+ };
7454
+ });
7455
+ }
7237
7456
  function renderListComponent(component, theme, themeName) {
7238
7457
  if (!isListComponent(component)) return [];
7239
7458
  const resolvedConfig = component.props;
7240
- const maxLevel = getMaxLevelFromItems(resolvedConfig.items);
7241
7459
  const reference = resolvedConfig.reference || globalNumberingRegistry.generateReference("list");
7242
7460
  if (!globalNumberingRegistry.has(reference)) {
7243
- let levels;
7244
- if (resolvedConfig.levels && resolvedConfig.levels.length > 0) {
7245
- levels = fillMissingLevels(
7246
- applyListStart(
7247
- resolvedConfig.levels,
7248
- resolvedConfig.start
7249
- ),
7250
- maxLevel
7251
- );
7252
- } else {
7253
- const baseLevels = createLevelsFromSimplifiedProps(resolvedConfig);
7254
- levels = fillMissingLevels(baseLevels, maxLevel);
7255
- }
7256
7461
  const config = {
7257
7462
  reference,
7258
- levels: resolveMarkerFonts(levels, theme)
7463
+ levels: resolveMarkerFonts(resolveListLevels(resolvedConfig), theme)
7259
7464
  };
7260
7465
  const numberingConfig = createNumberingConfig(config);
7261
7466
  globalNumberingRegistry.register(numberingConfig);
@@ -7295,6 +7500,7 @@ async function renderImageComponent(component, theme, themeName) {
7295
7500
 
7296
7501
  // src/components/text-box.ts
7297
7502
  import {
7503
+ Paragraph as Paragraph5,
7298
7504
  Table as Table3,
7299
7505
  TableRow as TableRow2,
7300
7506
  TableCell as TableCell2,
@@ -7303,7 +7509,8 @@ import {
7303
7509
  RelativeHorizontalPosition,
7304
7510
  RelativeVerticalPosition,
7305
7511
  OverlapType,
7306
- TableLayoutType as TableLayoutType2
7512
+ TableLayoutType as TableLayoutType2,
7513
+ WpsShapeRun
7307
7514
  } from "docx";
7308
7515
 
7309
7516
  // src/styles/utils/borderUtils.ts
@@ -7380,6 +7587,8 @@ function buildCellOptions(children, styleCfg, theme) {
7380
7587
  }
7381
7588
 
7382
7589
  // src/components/text-box.ts
7590
+ init_colorUtils();
7591
+ init_docxImagePositioning();
7383
7592
  init_widthUtils();
7384
7593
  function mapTableFloatOptions(floating, theme, themeName) {
7385
7594
  if (!floating) return void 0;
@@ -7435,26 +7644,23 @@ function mapTableFloatOptions(floating, theme, themeName) {
7435
7644
  opt.overlap = OverlapType.OVERLAP;
7436
7645
  return opt;
7437
7646
  }
7438
- async function renderTextBoxComponent(component, theme, themeName, _context) {
7439
- if (!isTextBoxComponent(component)) return [];
7440
- const tb = component;
7647
+ async function renderTextBoxChildren(tb, theme, themeName, context) {
7648
+ const childContext = {
7649
+ ...context,
7650
+ parent: tb
7651
+ };
7652
+ const rendered = [];
7653
+ for (const child of tb.children || []) {
7654
+ rendered.push(
7655
+ ...await renderComponent(child, theme, themeName, childContext)
7656
+ );
7657
+ }
7658
+ return rendered;
7659
+ }
7660
+ async function renderTextBoxAsTable(tb, theme, themeName, _context, prerendered) {
7441
7661
  const isInline = !tb.props.floating;
7442
- const childComponents = tb.children || [];
7443
7662
  if (isInline) {
7444
- const cellChildren2 = [];
7445
- const childContext2 = {
7446
- ..._context,
7447
- parent: tb
7448
- };
7449
- for (const child of childComponents) {
7450
- const rendered = await renderComponent(
7451
- child,
7452
- theme,
7453
- themeName,
7454
- childContext2
7455
- );
7456
- cellChildren2.push(...rendered);
7457
- }
7663
+ const cellChildren2 = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
7458
7664
  const styleCfg2 = tb.props.style;
7459
7665
  const cellOpts2 = buildCellOptions(cellChildren2, styleCfg2, theme);
7460
7666
  const row2 = new TableRow2({ children: [new TableCell2(cellOpts2)] });
@@ -7467,20 +7673,7 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
7467
7673
  });
7468
7674
  return [table2];
7469
7675
  }
7470
- const cellChildren = [];
7471
- const childContext = {
7472
- ..._context,
7473
- parent: tb
7474
- };
7475
- for (const child of childComponents) {
7476
- const rendered = await renderComponent(
7477
- child,
7478
- theme,
7479
- themeName,
7480
- childContext
7481
- );
7482
- cellChildren.push(...rendered);
7483
- }
7676
+ const cellChildren = prerendered ?? await renderTextBoxChildren(tb, theme, themeName, _context);
7484
7677
  const styleCfg = tb.props.style;
7485
7678
  const cellOpts = buildCellOptions(cellChildren, styleCfg, theme);
7486
7679
  const row = new TableRow2({
@@ -7512,6 +7705,162 @@ async function renderTextBoxComponent(component, theme, themeName, _context) {
7512
7705
  });
7513
7706
  return [table];
7514
7707
  }
7708
+ var PIXELS_TO_EMU = 9525;
7709
+ var TWIPS_PER_PIXEL = 15;
7710
+ function resolveShapeSize(value, axis, theme, themeName) {
7711
+ if (typeof value === "number") {
7712
+ return { pixels: Math.round(value), resolvedPercentage: false };
7713
+ }
7714
+ if (typeof value !== "string") return { resolvedPercentage: false };
7715
+ const fraction = parseFloat(value) / 100;
7716
+ if (!Number.isFinite(fraction) || fraction <= 0) {
7717
+ return { resolvedPercentage: false };
7718
+ }
7719
+ const availableTwips = axis === "width" ? getAvailableWidthTwips(theme, themeName) : getAvailableHeightTwips(theme, themeName);
7720
+ return {
7721
+ pixels: Math.round(availableTwips * fraction / TWIPS_PER_PIXEL),
7722
+ resolvedPercentage: true
7723
+ };
7724
+ }
7725
+ function shapeColor(value, theme) {
7726
+ return resolveColor(value, theme).replace(/^#/, "");
7727
+ }
7728
+ function shapeOutline(style, theme) {
7729
+ const border = style?.border;
7730
+ if (!border) return { ignoredSides: [], unsupportedStyles: [] };
7731
+ const order = [
7732
+ "top",
7733
+ "left",
7734
+ "bottom",
7735
+ "right"
7736
+ ];
7737
+ const declared = order.map((side) => [side, border[side]]).filter(
7738
+ (entry) => Boolean(entry[1])
7739
+ );
7740
+ if (declared.length === 0) return { ignoredSides: [], unsupportedStyles: [] };
7741
+ const unsupportedStyles = [
7742
+ ...new Set(
7743
+ declared.map(([, config]) => config.style).filter(
7744
+ (value) => value === "dashed" || value === "dotted" || value === "double"
7745
+ )
7746
+ )
7747
+ ];
7748
+ if (unsupportedStyles.length > 0) {
7749
+ return { ignoredSides: [], unsupportedStyles };
7750
+ }
7751
+ const [, used] = declared[0];
7752
+ const differs = ({ style: s, width, color }) => s !== used.style || width !== used.width || color !== used.color;
7753
+ const ignoredSides = declared.slice(1).filter(([, config]) => differs(config)).map(([side]) => side);
7754
+ if (used.style === "none") return { ignoredSides, unsupportedStyles };
7755
+ return {
7756
+ outline: {
7757
+ type: "solidFill",
7758
+ solidFillType: "rgb",
7759
+ value: used.color ? shapeColor(used.color, theme) : "000000",
7760
+ ...used.width !== void 0 && {
7761
+ width: Math.round(used.width * PIXELS_TO_EMU)
7762
+ }
7763
+ },
7764
+ ignoredSides,
7765
+ unsupportedStyles
7766
+ };
7767
+ }
7768
+ function shapeBodyProperties(style) {
7769
+ const padding = style?.padding;
7770
+ if (!padding) return void 0;
7771
+ const toEmu = (value) => value === void 0 ? void 0 : Math.round(value * PIXELS_TO_EMU);
7772
+ return {
7773
+ margins: {
7774
+ ...padding.top !== void 0 && { top: toEmu(padding.top) },
7775
+ ...padding.bottom !== void 0 && { bottom: toEmu(padding.bottom) },
7776
+ ...padding.left !== void 0 && { left: toEmu(padding.left) },
7777
+ ...padding.right !== void 0 && { right: toEmu(padding.right) }
7778
+ }
7779
+ };
7780
+ }
7781
+ async function renderTextBoxAsShape(tb, theme, themeName, context) {
7782
+ const width = resolveShapeSize(tb.props.width, "width", theme, themeName);
7783
+ const height = resolveShapeSize(tb.props.height, "height", theme, themeName);
7784
+ if (width.pixels === void 0 || height.pixels === void 0) {
7785
+ console.warn(
7786
+ '[core-docx] text-box renderAs "shape" needs an explicit width and height (a shape has no autofit); falling back to table rendering.'
7787
+ );
7788
+ return { kind: "fallback" };
7789
+ }
7790
+ const style = tb.props.style;
7791
+ const { outline, ignoredSides, unsupportedStyles } = shapeOutline(
7792
+ style,
7793
+ theme
7794
+ );
7795
+ if (unsupportedStyles.length > 0) {
7796
+ console.warn(
7797
+ `[core-docx] text-box renderAs "shape" cannot draw a ${unsupportedStyles.join("/")} border (a shape outline has no dash pattern); falling back to table rendering, which draws it.`
7798
+ );
7799
+ return { kind: "fallback" };
7800
+ }
7801
+ const rendered = await renderTextBoxChildren(tb, theme, themeName, context);
7802
+ if (rendered.some((element) => !(element instanceof Paragraph5))) {
7803
+ console.warn(
7804
+ '[core-docx] text-box renderAs "shape" requires paragraph-only content; falling back to table rendering.'
7805
+ );
7806
+ return { kind: "fallback", rendered };
7807
+ }
7808
+ const children = rendered;
7809
+ if (width.resolvedPercentage || height.resolvedPercentage) {
7810
+ console.warn(
7811
+ '[core-docx] text-box renderAs "shape" resolves percentage sizes at generation time, against the current page content box; the shape will not reflow if the page size changes.'
7812
+ );
7813
+ }
7814
+ const fill = style?.shading?.fill;
7815
+ if (ignoredSides.length > 0) {
7816
+ console.warn(
7817
+ `[core-docx] text-box renderAs "shape" has one uniform outline; using the first declared border side and ignoring ${ignoredSides.join(", ")}.`
7818
+ );
7819
+ }
7820
+ const dropOutline = Boolean(fill) && Boolean(outline);
7821
+ if (dropOutline) {
7822
+ console.warn(
7823
+ '[core-docx] text-box renderAs "shape" cannot carry a fill and a border at once (docx emits invalid shape properties); keeping the fill and dropping the border.'
7824
+ );
7825
+ }
7826
+ const bodyProperties = shapeBodyProperties(style);
7827
+ const run = new WpsShapeRun({
7828
+ type: "wps",
7829
+ children,
7830
+ transformation: { width: width.pixels, height: height.pixels },
7831
+ ...fill && {
7832
+ solidFill: { type: "rgb", value: shapeColor(fill, theme) }
7833
+ },
7834
+ ...outline && !dropOutline && { outline },
7835
+ ...bodyProperties && { bodyProperties },
7836
+ // Absent `floating` makes it a `wp:inline` drawing.
7837
+ ...tb.props.floating && {
7838
+ floating: mapFloatingOptions(tb.props.floating, theme, themeName)
7839
+ }
7840
+ });
7841
+ return {
7842
+ kind: "shape",
7843
+ paragraphs: [
7844
+ new Paragraph5({ children: [run], spacing: { before: 0, after: 0 } })
7845
+ ]
7846
+ };
7847
+ }
7848
+ async function renderTextBoxComponent(component, theme, themeName, context) {
7849
+ if (!isTextBoxComponent(component)) return [];
7850
+ const tb = component;
7851
+ if (tb.props.renderAs === "shape") {
7852
+ const attempt = await renderTextBoxAsShape(tb, theme, themeName, context);
7853
+ if (attempt.kind === "shape") return attempt.paragraphs;
7854
+ return renderTextBoxAsTable(
7855
+ tb,
7856
+ theme,
7857
+ themeName,
7858
+ context,
7859
+ attempt.rendered
7860
+ );
7861
+ }
7862
+ return renderTextBoxAsTable(tb, theme, themeName, context);
7863
+ }
7515
7864
 
7516
7865
  // src/components/table.ts
7517
7866
  async function renderTableComponent(component, theme, themeName) {
@@ -7565,14 +7914,14 @@ async function renderTableComponent(component, theme, themeName) {
7565
7914
  import { Paragraph as Paragraph6, BookmarkStart, BookmarkEnd } from "docx";
7566
7915
 
7567
7916
  // src/core/sectionBookmarks.ts
7568
- import { AsyncLocalStorage as AsyncLocalStorage7 } from "async_hooks";
7917
+ import { AsyncLocalStorage as AsyncLocalStorage8 } from "async_hooks";
7569
7918
  var NESTED_LINK_ID_BASE = 1e6;
7570
7919
  function createState3() {
7571
7920
  return { nextNested: 1, resolved: /* @__PURE__ */ new WeakMap() };
7572
7921
  }
7573
7922
  var SectionBookmarkRegistry = class {
7574
7923
  fallback = createState3();
7575
- scopes = new AsyncLocalStorage7();
7924
+ scopes = new AsyncLocalStorage8();
7576
7925
  get state() {
7577
7926
  return this.scopes.getStore() ?? this.fallback;
7578
7927
  }
@@ -7830,7 +8179,10 @@ function selectCachedEntries(collected, options) {
7830
8179
  continue;
7831
8180
  }
7832
8181
  if (entry.level < depthStart || entry.level > depthEnd) continue;
7833
- entries.push({ title: entry.title, level: entry.level });
8182
+ entries.push({
8183
+ title: entry.number ? `${headingNumberLabel(entry.number)} ${entry.title}` : entry.title,
8184
+ level: entry.level
8185
+ });
7834
8186
  }
7835
8187
  return entries;
7836
8188
  }
@@ -8138,6 +8490,61 @@ function computeSectionOrdinals(sections) {
8138
8490
 
8139
8491
  // src/core/collectTocHeadings.ts
8140
8492
  import { getStandardComponent } from "@json-to-office/shared-docx";
8493
+
8494
+ // src/utils/numberFormatting.ts
8495
+ var ROMAN_NUMERALS = [
8496
+ [1e3, "m"],
8497
+ [900, "cm"],
8498
+ [500, "d"],
8499
+ [400, "cd"],
8500
+ [100, "c"],
8501
+ [90, "xc"],
8502
+ [50, "l"],
8503
+ [40, "xl"],
8504
+ [10, "x"],
8505
+ [9, "ix"],
8506
+ [5, "v"],
8507
+ [4, "iv"],
8508
+ [1, "i"]
8509
+ ];
8510
+ function toRoman(value) {
8511
+ let remaining = value;
8512
+ let out = "";
8513
+ for (const [amount, glyph] of ROMAN_NUMERALS) {
8514
+ while (remaining >= amount) {
8515
+ out += glyph;
8516
+ remaining -= amount;
8517
+ }
8518
+ }
8519
+ return out;
8520
+ }
8521
+ function toLetters(value) {
8522
+ const index = (value - 1) % 26;
8523
+ const repeats = Math.floor((value - 1) / 26) + 1;
8524
+ return String.fromCharCode(97 + index).repeat(repeats);
8525
+ }
8526
+ function formatNumberForLevel(value, format2) {
8527
+ if (!Number.isFinite(value) || value < 1) return void 0;
8528
+ const n = Math.floor(value);
8529
+ switch (format2) {
8530
+ case "decimal":
8531
+ return String(n);
8532
+ case "lowerLetter":
8533
+ return toLetters(n);
8534
+ case "upperLetter":
8535
+ return toLetters(n).toUpperCase();
8536
+ case "lowerRoman":
8537
+ return toRoman(n);
8538
+ case "upperRoman":
8539
+ return toRoman(n).toUpperCase();
8540
+ default:
8541
+ return void 0;
8542
+ }
8543
+ }
8544
+
8545
+ // src/core/collectTocHeadings.ts
8546
+ var MAX_HEADING_LEVEL2 = 6;
8547
+ var MAX_LIST_LEVEL = 9;
8141
8548
  function normalizeEntryTitle(text) {
8142
8549
  return normalizeUnicodeText(text).replace(/(\*\*\*|___)([\s\S]*?)\1/g, "$2").replace(/(\*\*|__)([\s\S]*?)\1/g, "$2").replace(/(\*|_)([\s\S]*?)\1/g, "$2").trim();
8143
8550
  }
@@ -8156,9 +8563,97 @@ function styleEntryKey(props) {
8156
8563
  }
8157
8564
  return themeStyle;
8158
8565
  }
8159
- function collectTocHeadings(sections) {
8566
+ function levelStart(levels, level) {
8567
+ return levels[level]?.start ?? 1;
8568
+ }
8569
+ function collectDocumentOutline(sections) {
8160
8570
  const entries = [];
8571
+ const numberedItems = /* @__PURE__ */ new Map();
8161
8572
  const ordinals = computeSectionOrdinals(sections);
8573
+ const takenIds = /* @__PURE__ */ new Set();
8574
+ const headingCounters = new Array(MAX_HEADING_LEVEL2).fill(0);
8575
+ const listCounters = /* @__PURE__ */ new Map();
8576
+ const visitHeading = (component, props, sectionBookmarkId) => {
8577
+ const text = typeof props.text === "string" ? props.text : "";
8578
+ const title = normalizeEntryTitle(text);
8579
+ const level = typeof props.level === "number" ? props.level : 1;
8580
+ const styleLevel = level >= 1 && level <= MAX_HEADING_LEVEL2 ? level : 1;
8581
+ let full;
8582
+ let own;
8583
+ if (props.numbering === true) {
8584
+ headingCounters[styleLevel - 1] += 1;
8585
+ for (let deeper = styleLevel; deeper < MAX_HEADING_LEVEL2; deeper++) {
8586
+ headingCounters[deeper] = 0;
8587
+ }
8588
+ full = headingCounters.slice(0, styleLevel).join(".");
8589
+ own = String(headingCounters[styleLevel - 1]);
8590
+ }
8591
+ const explicitId = component.id;
8592
+ const bookmarkId = typeof explicitId === "string" && explicitId ? explicitId : dedupeBookmarkId(slugifyBookmarkText(text), (id) => takenIds.has(id));
8593
+ takenIds.add(bookmarkId);
8594
+ numberedItems.set(bookmarkId, {
8595
+ kind: "heading",
8596
+ text: title,
8597
+ ...full !== void 0 && { full, own }
8598
+ });
8599
+ if (title) {
8600
+ entries.push({
8601
+ title,
8602
+ level,
8603
+ sectionBookmarkId,
8604
+ ...full !== void 0 && { number: full }
8605
+ });
8606
+ }
8607
+ };
8608
+ const visitList = (props) => {
8609
+ const items = Array.isArray(props.items) ? props.items : [];
8610
+ if (items.length === 0) return;
8611
+ const reference = typeof props.reference === "string" && props.reference ? props.reference : void 0;
8612
+ const freshState = () => {
8613
+ const levels = resolveListLevels(
8614
+ props
8615
+ );
8616
+ return {
8617
+ levels,
8618
+ counters: Array.from(
8619
+ { length: MAX_LIST_LEVEL },
8620
+ (_, level) => levelStart(levels, level) - 1
8621
+ )
8622
+ };
8623
+ };
8624
+ let state;
8625
+ if (reference === void 0) {
8626
+ state = freshState();
8627
+ } else {
8628
+ state = listCounters.get(reference) ?? freshState();
8629
+ listCounters.set(reference, state);
8630
+ }
8631
+ for (const item of items) {
8632
+ const isObject = typeof item === "object" && item !== null;
8633
+ const raw = isObject ? item.text : item;
8634
+ const text = typeof raw === "string" ? raw : "";
8635
+ if (!text.trim() && !(isObject && item.revision)) continue;
8636
+ const rawLevel = isObject ? item.level : void 0;
8637
+ const level = typeof rawLevel === "number" && rawLevel >= 0 ? rawLevel : 0;
8638
+ if (level >= MAX_LIST_LEVEL) continue;
8639
+ state.counters[level] += 1;
8640
+ for (let deeper = level + 1; deeper < MAX_LIST_LEVEL; deeper++) {
8641
+ state.counters[deeper] = levelStart(state.levels, deeper) - 1;
8642
+ }
8643
+ const id = isObject ? item.id : void 0;
8644
+ if (typeof id !== "string" || !id) continue;
8645
+ takenIds.add(id);
8646
+ const number = formatNumberForLevel(
8647
+ state.counters[level],
8648
+ state.levels[level]?.format
8649
+ );
8650
+ numberedItems.set(id, {
8651
+ kind: "list-item",
8652
+ text: normalizeUnicodeText(text).trim(),
8653
+ ...number !== void 0 && { full: number, own: number }
8654
+ });
8655
+ }
8656
+ };
8162
8657
  sections.forEach((section, index) => {
8163
8658
  const ordinal = ordinals[index]?.ordinal;
8164
8659
  const sectionBookmarkId = ordinal ? globalSectionBookmarkRegistry.forLayoutSection(ordinal).id : void 0;
@@ -8166,15 +8661,11 @@ function collectTocHeadings(sections) {
8166
8661
  if (!isEnabled(component)) return;
8167
8662
  const props = component.props ?? {};
8168
8663
  if (component.name === "heading") {
8169
- const text = typeof props.text === "string" ? props.text : "";
8170
- const title = normalizeEntryTitle(text);
8171
- if (title) {
8172
- const level = typeof props.level === "number" ? props.level : 1;
8173
- entries.push({ title, level, sectionBookmarkId });
8174
- }
8664
+ visitHeading(component, props, sectionBookmarkId);
8175
8665
  return;
8176
8666
  }
8177
8667
  if (component.name === "paragraph") {
8668
+ if (typeof props.id === "string" && props.id) takenIds.add(props.id);
8178
8669
  const styleId = styleEntryKey(props);
8179
8670
  const text = typeof props.text === "string" ? props.text : "";
8180
8671
  const title = normalizeEntryTitle(text);
@@ -8183,13 +8674,17 @@ function collectTocHeadings(sections) {
8183
8674
  }
8184
8675
  return;
8185
8676
  }
8677
+ if (component.name === "list") {
8678
+ visitList(props);
8679
+ return;
8680
+ }
8186
8681
  if (!isContainer(component.name)) return;
8187
8682
  const children = component.children;
8188
8683
  if (Array.isArray(children)) children.forEach(visit);
8189
8684
  };
8190
8685
  section.components.forEach(visit);
8191
8686
  });
8192
- return entries;
8687
+ return { entries, numberedItems };
8193
8688
  }
8194
8689
 
8195
8690
  // src/core/render.ts
@@ -8224,23 +8719,32 @@ function coreProperties(metadata) {
8224
8719
  async function renderDocument(structure, layout, options) {
8225
8720
  return runWithGenerationDate(
8226
8721
  structure.metadata.date,
8227
- () => runWithBaseDir(
8228
- options?.baseDir,
8229
- () => globalBookmarkRegistry.runScoped(
8230
- () => globalRevisionIdRegistry.runScoped(
8231
- () => globalNumberingRegistry.runScoped(
8232
- () => globalSectionBookmarkRegistry.runScoped(
8233
- () => (
8234
- // Comment ids are a separate OOXML namespace from w:ins/w:del,
8235
- // but they need the same per-render isolation: outside this nest
8236
- // concurrent generations would interleave counters and an anchor
8237
- // would point at another document's comment body.
8238
- globalCommentRegistry.runScoped(
8239
- () => (
8240
- // Footnote ids are document-scoped too: a reference resolved
8241
- // against another render's counter points at the wrong body.
8242
- globalNoteRegistry.runScoped(
8243
- () => renderDocumentScoped(structure, layout, options)
8722
+ () => runWithWarnings(
8723
+ options?.warnings,
8724
+ () => runWithBaseDir(
8725
+ options?.baseDir,
8726
+ () => globalBookmarkRegistry.runScoped(
8727
+ () => globalRevisionIdRegistry.runScoped(
8728
+ () => globalNumberingRegistry.runScoped(
8729
+ () => globalSectionBookmarkRegistry.runScoped(
8730
+ () => (
8731
+ // Comment ids are a separate OOXML namespace from w:ins/w:del,
8732
+ // but they need the same per-render isolation: outside this nest
8733
+ // concurrent generations would interleave counters and an anchor
8734
+ // would point at another document's comment body.
8735
+ globalCommentRegistry.runScoped(
8736
+ () => (
8737
+ // Footnote ids are document-scoped too: a reference resolved
8738
+ // against another render's counter points at the wrong body.
8739
+ globalNoteRegistry.runScoped(
8740
+ () => (
8741
+ // Cross-reference targets are keyed by bookmark id, which
8742
+ // is only unique within one document.
8743
+ globalNumberedItemsRegistry.runScoped(
8744
+ () => renderDocumentScoped(structure, layout, options)
8745
+ )
8746
+ )
8747
+ )
8244
8748
  )
8245
8749
  )
8246
8750
  )
@@ -8282,13 +8786,14 @@ async function renderDocumentScoped(structure, layout, options) {
8282
8786
  );
8283
8787
  }
8284
8788
  try {
8285
- const tocHeadings = collectTocHeadings(layout.sections);
8286
- if (tocHeadings.length > 0) {
8287
- context.tocHeadings = tocHeadings;
8789
+ const outline = collectDocumentOutline(layout.sections);
8790
+ if (outline.entries.length > 0) {
8791
+ context.tocHeadings = outline.entries;
8288
8792
  }
8793
+ globalNumberedItemsRegistry.seed(outline.numberedItems);
8289
8794
  } catch (error) {
8290
8795
  console.warn(
8291
- "[core-docx] TOC entry collection failed; the TOC field will rely on the reader refreshing it:",
8796
+ "[core-docx] Document outline collection failed; the TOC field will rely on the reader refreshing it:",
8292
8797
  error instanceof Error ? error.message : error
8293
8798
  );
8294
8799
  }
@@ -8460,7 +8965,7 @@ async function renderHeaderFooterComponents(components, theme, themeName, contex
8460
8965
  themeName
8461
8966
  );
8462
8967
  const imageType = detectImageType(imageSource, responseContentType);
8463
- const imageRun = createTypedImageRun({
8968
+ const imageRun = await createTypedImageRun({
8464
8969
  type: imageType,
8465
8970
  data: imageBuffer,
8466
8971
  transformation: {
@@ -9098,6 +9603,7 @@ function createBuilderImpl(state) {
9098
9603
  services: state.services,
9099
9604
  bypassCache: !state.enableCache,
9100
9605
  baseDir: options?.baseDir ?? state.baseDir,
9606
+ warnings,
9101
9607
  ...visualFonts.length > 0 && { visualFonts }
9102
9608
  });
9103
9609
  const preservedDefinition = preserveSet ? {