@json-to-office/core-docx 1.0.0 → 1.3.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 (35) hide show
  1. package/dist/components/visual.d.ts +6 -6
  2. package/dist/components/visual.d.ts.map +1 -1
  3. package/dist/core/desugarExternals.d.ts.map +1 -1
  4. package/dist/core/flattenVisuals.d.ts.map +1 -1
  5. package/dist/core/imageResources.d.ts.map +1 -1
  6. package/dist/core/prerasterizeVisuals.d.ts +10 -4
  7. package/dist/core/prerasterizeVisuals.d.ts.map +1 -1
  8. package/dist/index.js +1222 -98
  9. package/dist/index.js.map +1 -1
  10. package/dist/ir/compiler.d.ts.map +1 -1
  11. package/dist/ir/features.d.ts +1 -1
  12. package/dist/ir/features.d.ts.map +1 -1
  13. package/dist/ir/nativeVisual.d.ts +79 -0
  14. package/dist/ir/nativeVisual.d.ts.map +1 -0
  15. package/dist/ir/types.d.ts +155 -1
  16. package/dist/ir/types.d.ts.map +1 -1
  17. package/dist/ir/units.d.ts +12 -0
  18. package/dist/ir/units.d.ts.map +1 -1
  19. package/dist/ir/validation.d.ts.map +1 -1
  20. package/dist/plugin/example/index.js +1215 -96
  21. package/dist/plugin/example/index.js.map +1 -1
  22. package/dist/renderers/docxjs/emit.d.ts.map +1 -1
  23. package/dist/renderers/docxjs/index.d.ts.map +1 -1
  24. package/dist/renderers/office-open/chartParts.d.ts +28 -0
  25. package/dist/renderers/office-open/chartParts.d.ts.map +1 -0
  26. package/dist/renderers/office-open/emit.d.ts +47 -6
  27. package/dist/renderers/office-open/emit.d.ts.map +1 -1
  28. package/dist/renderers/office-open/index.d.ts +2 -2
  29. package/dist/renderers/office-open/index.d.ts.map +1 -1
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/dist/utils/chartWorkbook.d.ts +19 -0
  32. package/dist/utils/chartWorkbook.d.ts.map +1 -0
  33. package/dist/utils/packageDocument.d.ts +2 -0
  34. package/dist/utils/packageDocument.d.ts.map +1 -1
  35. package/package.json +3 -3
@@ -1977,6 +1977,12 @@ function pointsToEighthPoints(points) {
1977
1977
  function inchesToTwips(inches) {
1978
1978
  return Math.round(inches * TWIPS_PER_INCH);
1979
1979
  }
1980
+ function inchesToEmu(inches) {
1981
+ return Math.round(inches * EMU_PER_INCH);
1982
+ }
1983
+ function pointsToEmu(points) {
1984
+ return Math.round(points * EMU_PER_INCH / 72);
1985
+ }
1980
1986
  function twipsToEmu(twips) {
1981
1987
  return Math.round(twips * EMU_PER_TWIP);
1982
1988
  }
@@ -2179,6 +2185,14 @@ function inlineChildren(children, resources = /* @__PURE__ */ new Map()) {
2179
2185
  case "shape":
2180
2186
  out.push(emitShape(child, resources));
2181
2187
  break;
2188
+ case "drawingGroup":
2189
+ throw new Error(
2190
+ "the docxjs renderer has no emitter for a drawing group; this document should have been refused by the capability check"
2191
+ );
2192
+ case "chart":
2193
+ throw new Error(
2194
+ "the docxjs renderer has no emitter for a chart; this document should have been refused by the capability check"
2195
+ );
2182
2196
  case "noteReference":
2183
2197
  out.push(
2184
2198
  child.noteKind === "endnote" ? new EndnoteReferenceRun(child.id) : new FootnoteReferenceRun(child.id)
@@ -2280,7 +2294,7 @@ function paragraphOptions(formatting) {
2280
2294
  const options = {};
2281
2295
  if (!formatting) return options;
2282
2296
  if (formatting.alignment) {
2283
- options.alignment = ALIGNMENT[formatting.alignment];
2297
+ options.alignment = ALIGNMENT2[formatting.alignment];
2284
2298
  }
2285
2299
  if (formatting.spacing) {
2286
2300
  const spacing = {};
@@ -2514,11 +2528,11 @@ function emitBorder(border3) {
2514
2528
  color: border3.color?.hex ?? "000000"
2515
2529
  };
2516
2530
  }
2517
- var ALIGNMENT, PAGE_FIELD, WRAP_TYPE, VERTICAL_ALIGN, TABLE_ANCHOR, HORIZONTAL_POSITION, VERTICAL_POSITION, BORDER_STYLE;
2531
+ var ALIGNMENT2, PAGE_FIELD, WRAP_TYPE, VERTICAL_ALIGN, TABLE_ANCHOR, HORIZONTAL_POSITION, VERTICAL_POSITION, BORDER_STYLE;
2518
2532
  var init_emit = __esm({
2519
2533
  "src/renderers/docxjs/emit.ts"() {
2520
2534
  "use strict";
2521
- ALIGNMENT = {
2535
+ ALIGNMENT2 = {
2522
2536
  left: AlignmentType.LEFT,
2523
2537
  center: AlignmentType.CENTER,
2524
2538
  right: AlignmentType.RIGHT,
@@ -2641,7 +2655,7 @@ function paragraphProperties(formatting) {
2641
2655
  out.spacing = spacing;
2642
2656
  }
2643
2657
  if (formatting.alignment !== void 0) {
2644
- out.alignment = ALIGNMENT[formatting.alignment] ?? formatting.alignment;
2658
+ out.alignment = ALIGNMENT2[formatting.alignment] ?? formatting.alignment;
2645
2659
  }
2646
2660
  if (formatting.keepNext !== void 0) out.keepNext = formatting.keepNext;
2647
2661
  if (formatting.keepLines !== void 0) out.keepLines = formatting.keepLines;
@@ -2770,6 +2784,26 @@ var init_features = __esm({
2770
2784
  "text-frames",
2771
2785
  /** Native shape text boxes. */
2772
2786
  "text-boxes",
2787
+ /**
2788
+ * A DrawingML group: shapes, text boxes and pictures sharing one child
2789
+ * coordinate space, drawn as a single anchored or inline object.
2790
+ *
2791
+ * Separate from `text-boxes` because a lone `wps:wsp` run is a far smaller
2792
+ * ask than `wpg:wgp` with child transforms, preset geometry and grouped
2793
+ * pictures — a backend can plausibly have the first and not the second.
2794
+ */
2795
+ "drawing-groups",
2796
+ /**
2797
+ * A native chart part: a `c:chartSpace` with its own embedded workbook.
2798
+ *
2799
+ * Separate from `images` because a chart is not a picture with extra data —
2800
+ * it is its own part, its own relationship and its own workbook, and a
2801
+ * backend either writes all three or writes none. docx.js has no chart
2802
+ * primitive at all, which is what turns a `chart` component sent to that
2803
+ * backend into a named capability error rather than a document missing a
2804
+ * figure.
2805
+ */
2806
+ "charts",
2773
2807
  /** A table-of-contents field. */
2774
2808
  "toc",
2775
2809
  /** TOC entries baked in so an unrefreshed reader still shows them. */
@@ -3016,7 +3050,7 @@ function numberingConfig(numbering) {
3016
3050
  level: level.level,
3017
3051
  format: level.format,
3018
3052
  text: level.text,
3019
- alignment: level.alignment ? ALIGNMENT[level.alignment] : void 0,
3053
+ alignment: level.alignment ? ALIGNMENT2[level.alignment] : void 0,
3020
3054
  ...level.suffix ? { suffix: level.suffix } : {},
3021
3055
  style: {
3022
3056
  ...level.indent ? {
@@ -3234,7 +3268,16 @@ var init_docxjs = __esm({
3234
3268
  "cached-fields",
3235
3269
  "shading",
3236
3270
  "borders",
3237
- "rtl"
3271
+ "rtl",
3272
+ // `drawing-groups` is the one exclusion that is a real backend gap rather
3273
+ // than a slice boundary: docx.js has no `wpg:wgp`. Leaving it out is what
3274
+ // turns a native `visual` sent to this backend into a named capability
3275
+ // error instead of a document with the graphic missing.
3276
+ "drawing-groups",
3277
+ // `charts` is the second real backend gap: docx.js has no chart primitive at
3278
+ // all, so a `chart` component sent here becomes a named capability error
3279
+ // rather than a document missing a figure.
3280
+ "charts"
3238
3281
  ]);
3239
3282
  DOCXJS_CAPABILITIES = new Set(
3240
3283
  [...ALL_DOCX_FEATURES].filter((feature) => !NOT_YET_EMITTED.has(feature))
@@ -3242,11 +3285,105 @@ var init_docxjs = __esm({
3242
3285
  }
3243
3286
  });
3244
3287
 
3288
+ // src/utils/chartWorkbook.ts
3289
+ import AdmZip3 from "adm-zip";
3290
+ import { chartWorkbookParts } from "@json-to-office/shared/rendering";
3291
+ import {
3292
+ CHART_WORKBOOK_SHEET_NAME,
3293
+ categoryReference,
3294
+ columnLetter,
3295
+ seriesNameReference,
3296
+ seriesValueReference
3297
+ } from "@json-to-office/shared/rendering";
3298
+ function buildChartWorkbook(series) {
3299
+ const zip = new AdmZip3();
3300
+ for (const [name, content] of chartWorkbookParts(series)) {
3301
+ zip.addFile(name, Buffer.from(content, "utf8"));
3302
+ }
3303
+ for (const entry of zip.getEntries()) {
3304
+ entry.header.timeval = WORKBOOK_TIMESTAMP;
3305
+ }
3306
+ return new Uint8Array(zip.toBuffer());
3307
+ }
3308
+ var WORKBOOK_TIMESTAMP;
3309
+ var init_chartWorkbook = __esm({
3310
+ "src/utils/chartWorkbook.ts"() {
3311
+ "use strict";
3312
+ init_packageDocument();
3313
+ WORKBOOK_TIMESTAMP = toDosTime(DEFAULT_GENERATION_DATE);
3314
+ }
3315
+ });
3316
+
3317
+ // src/renderers/office-open/chartParts.ts
3318
+ import {
3319
+ CHART_WORKBOOK_CONTENT_TYPE,
3320
+ chartWorkbookRelsXml,
3321
+ matchChartParts,
3322
+ spliceChartXml
3323
+ } from "@json-to-office/shared/rendering";
3324
+ function spliceInput(chart) {
3325
+ return {
3326
+ chartType: chart.chartType,
3327
+ series: chart.series,
3328
+ colors: chart.colors,
3329
+ ...chart.legendPosition ? { legendPosition: chart.legendPosition } : {},
3330
+ ...chart.categoryAxisTitle ? { categoryAxis: { title: chart.categoryAxisTitle } } : {},
3331
+ ...chart.valueAxisTitle ? { valueAxis: { title: chart.valueAxisTitle } } : {}
3332
+ };
3333
+ }
3334
+ function declareWorkbookContentType(zip) {
3335
+ const entry = zip.getEntry("[Content_Types].xml");
3336
+ if (!entry) return;
3337
+ const xml = entry.getData().toString("utf8");
3338
+ if (xml.includes(`Extension="xlsx"`)) return;
3339
+ zip.updateFile(
3340
+ entry,
3341
+ Buffer.from(
3342
+ xml.replace(
3343
+ '<Default Extension="xml"',
3344
+ `<Default Extension="xlsx" ContentType="${CHART_WORKBOOK_CONTENT_TYPE}"/><Default Extension="xml"`
3345
+ ),
3346
+ "utf8"
3347
+ )
3348
+ );
3349
+ }
3350
+ function spliceChartParts(zip, charts) {
3351
+ if (charts.length === 0) return;
3352
+ const parts = zip.getEntries().map((entry) => entry.entryName).map((name) => name.match(/^word\/charts\/chart(\d+)\.xml$/)).filter((match) => match !== null).map(
3353
+ (match) => [
3354
+ Number(match[1]),
3355
+ zip.getEntry(match[0]).getData().toString("utf8")
3356
+ ]
3357
+ );
3358
+ for (const { ordinal, xml, chart } of matchChartParts(parts, charts)) {
3359
+ const workbookName = `chart${ordinal}.xlsx`;
3360
+ zip.updateFile(
3361
+ zip.getEntry(`word/charts/chart${ordinal}.xml`),
3362
+ Buffer.from(spliceChartXml(xml, spliceInput(chart)), "utf8")
3363
+ );
3364
+ zip.addFile(
3365
+ `word/embeddings/${workbookName}`,
3366
+ Buffer.from(buildChartWorkbook(chart.series))
3367
+ );
3368
+ zip.addFile(
3369
+ `word/charts/_rels/chart${ordinal}.xml.rels`,
3370
+ Buffer.from(chartWorkbookRelsXml(workbookName), "utf8")
3371
+ );
3372
+ }
3373
+ declareWorkbookContentType(zip);
3374
+ }
3375
+ var init_chartParts = __esm({
3376
+ "src/renderers/office-open/chartParts.ts"() {
3377
+ "use strict";
3378
+ init_chartWorkbook();
3379
+ }
3380
+ });
3381
+
3245
3382
  // src/renderers/office-open/emit.ts
3246
3383
  import { assertNever as assertNever2 } from "@json-to-office/shared/rendering";
3247
3384
  function emptyContext() {
3248
3385
  let next = 1;
3249
- return { pictures: /* @__PURE__ */ new Map(), nextDrawingId: () => next++ };
3386
+ return { pictures: /* @__PURE__ */ new Map(), nextDrawingId: () => next++, charts: [] };
3250
3387
  }
3251
3388
  function simpleField(instruction, cachedText) {
3252
3389
  return {
@@ -3345,15 +3482,15 @@ function inlineChildren2(children, ctx = emptyContext()) {
3345
3482
  out.push({ bookmarkEnd: { id: child.id } });
3346
3483
  break;
3347
3484
  case "image": {
3348
- const build = ctx.pictures.get(child.resourceId);
3349
- if (!build) {
3350
- throw new Error(
3351
- `no image was prepared for resource "${child.resourceId}"`
3352
- );
3353
- }
3354
3485
  const pending = breakOption();
3355
3486
  if (pending.break) out.push(pending);
3356
- out.push({ picture: build(child, ctx.nextDrawingId()) });
3487
+ out.push({ picture: pictureOptions(child, ctx) });
3488
+ break;
3489
+ }
3490
+ case "drawingGroup": {
3491
+ const pending = breakOption();
3492
+ if (pending.break) out.push(pending);
3493
+ out.push({ wpgGroup: drawingGroupOptions(child, ctx) });
3357
3494
  break;
3358
3495
  }
3359
3496
  case "hyperlink":
@@ -3382,6 +3519,10 @@ function inlineChildren2(children, ctx = emptyContext()) {
3382
3519
  case "shape":
3383
3520
  out.push({ wpsShape: shapeOptions(child, ctx) });
3384
3521
  break;
3522
+ case "chart":
3523
+ ctx.charts?.push(child);
3524
+ out.push({ chart: chartOptions(child, ctx) });
3525
+ break;
3385
3526
  case "noteReference":
3386
3527
  out.push(
3387
3528
  child.noteKind === "endnote" ? { endnoteReference: child.id } : { footnoteReference: child.id }
@@ -3448,6 +3589,185 @@ function floatingOptions2(floating) {
3448
3589
  zIndex: floating.zIndex
3449
3590
  };
3450
3591
  }
3592
+ function imageMedia(ctx, resourceId, placement) {
3593
+ const build = ctx.pictures.get(resourceId);
3594
+ if (!build) {
3595
+ throw new Error(`no image was prepared for resource "${resourceId}"`);
3596
+ }
3597
+ return build(placement);
3598
+ }
3599
+ function pictureOptions(image, ctx) {
3600
+ const media = imageMedia(ctx, image.resourceId, image);
3601
+ return {
3602
+ type: media.type,
3603
+ data: media.data,
3604
+ // No `fileName`: at run level the backend allocates one, and stating our
3605
+ // own would only fight it.
3606
+ ...media.fallback ? { fallback: media.fallback } : {},
3607
+ // Raw numbers are EMUs in @office-open/docx. Passing pixels here makes a
3608
+ // normal image only a few hundred EMUs wide, effectively a dot.
3609
+ transformation: { width: image.widthEmu, height: image.heightEmu },
3610
+ // The id is stated rather than left to the backend's process-global
3611
+ // counter. `name` stays empty, which is what it was before and what the
3612
+ // backend falls back to.
3613
+ altText: { id: String(ctx.nextDrawingId()) },
3614
+ ...image.floating ? { floating: floatingOptions2(image.floating) } : {}
3615
+ // No `description` or `title`: no DOCX this pipeline has produced carries
3616
+ // `wp:docPr` alt text, and the compiler warns so the gap is visible rather
3617
+ // than silent.
3618
+ };
3619
+ }
3620
+ function drawingGroupOptions(group, ctx) {
3621
+ const id = ctx.nextDrawingId();
3622
+ return {
3623
+ altText: {
3624
+ id: String(id),
3625
+ ...group.altText ? { description: group.altText } : {}
3626
+ },
3627
+ transformation: { width: group.widthEmu, height: group.heightEmu },
3628
+ childOffset: { x: 0, y: 0 },
3629
+ childExtent: { cx: group.canvasWidthEmu, cy: group.canvasHeightEmu },
3630
+ children: group.children.map((child) => groupChild(child, ctx)),
3631
+ ...group.floating ? { floating: floatingOptions2(group.floating) } : {}
3632
+ };
3633
+ }
3634
+ function chartOptions(chart, ctx) {
3635
+ const id = ctx.nextDrawingId();
3636
+ return {
3637
+ type: chart.chartType,
3638
+ categories: chart.series[0]?.labels ?? [],
3639
+ series: chart.series.map((entry, index) => ({
3640
+ name: entry.name ?? `Series ${index + 1}`,
3641
+ values: entry.values
3642
+ })),
3643
+ ...chart.title && chart.showTitle !== false ? { title: chart.title } : {},
3644
+ ...chart.showLegend !== void 0 ? { showLegend: chart.showLegend } : {},
3645
+ transformation: { width: chart.widthEmu, height: chart.heightEmu },
3646
+ altText: {
3647
+ id: String(id),
3648
+ ...chart.altText ? { description: chart.altText } : {}
3649
+ },
3650
+ ...chart.floating ? { floating: floatingOptions2(chart.floating) } : {}
3651
+ };
3652
+ }
3653
+ function groupChild(child, ctx) {
3654
+ return child.kind === "shape" ? groupShape(child, ctx) : groupPicture(child, ctx);
3655
+ }
3656
+ function groupShape(shape, ctx) {
3657
+ const id = ctx.nextDrawingId();
3658
+ return {
3659
+ type: "wps",
3660
+ transformation: childTransformation(shape.frame),
3661
+ data: {
3662
+ nonVisualProperties: {
3663
+ id,
3664
+ ...shape.name ? { name: shape.name } : {},
3665
+ // `txBox="1"` is how Word tells a text box from a shape that happens
3666
+ // to hold text, and it changes how the object behaves on selection.
3667
+ ...shape.isTextBox ? { textBox: "1" } : {}
3668
+ },
3669
+ presetGeometry: { preset: shape.geometry },
3670
+ ...shape.fill ? { fill: drawingFill(shape.fill) } : {},
3671
+ ...shape.outline ? { outline: drawingOutline(shape.outline) } : {},
3672
+ children: (shape.text?.paragraphs ?? []).map(
3673
+ (child) => paragraph(child, ctx)
3674
+ ),
3675
+ ...shape.text ? { bodyProperties: bodyProperties(shape.text) } : {}
3676
+ }
3677
+ };
3678
+ }
3679
+ function groupPicture(picture2, ctx) {
3680
+ const id = ctx.nextDrawingId();
3681
+ const media = imageMedia(ctx, picture2.resourceId, {
3682
+ widthEmu: picture2.frame.widthEmu,
3683
+ heightEmu: picture2.frame.heightEmu
3684
+ });
3685
+ return {
3686
+ type: media.type,
3687
+ data: media.data,
3688
+ fileName: media.fileName,
3689
+ ...media.fallback ? {
3690
+ fallback: {
3691
+ ...media.fallback,
3692
+ fileName: media.fallbackFileName
3693
+ }
3694
+ } : {},
3695
+ transformation: childTransformation(picture2.frame),
3696
+ nonVisualProperties: {
3697
+ id,
3698
+ ...picture2.name ? { name: picture2.name } : {},
3699
+ ...picture2.altText ? { description: picture2.altText } : {}
3700
+ },
3701
+ ...picture2.crop ? { sourceRectangle: sourceRectangle(picture2.crop) } : {}
3702
+ };
3703
+ }
3704
+ function childTransformation(frame) {
3705
+ const flip = {
3706
+ ...frame.flipHorizontal ? { horizontal: true } : {},
3707
+ ...frame.flipVertical ? { vertical: true } : {}
3708
+ };
3709
+ return {
3710
+ offset: {
3711
+ emus: { x: frame.xEmu, y: frame.yEmu },
3712
+ pixels: {
3713
+ x: Math.round(emuToPixels(frame.xEmu)),
3714
+ y: Math.round(emuToPixels(frame.yEmu))
3715
+ }
3716
+ },
3717
+ emus: { x: frame.widthEmu, y: frame.heightEmu },
3718
+ pixels: {
3719
+ x: Math.round(emuToPixels(frame.widthEmu)),
3720
+ y: Math.round(emuToPixels(frame.heightEmu))
3721
+ },
3722
+ // Degrees: the backend multiplies by 60000 on the way into `@rot`.
3723
+ ...frame.rotationDegrees !== void 0 ? { rotation: frame.rotationDegrees } : {},
3724
+ ...Object.keys(flip).length > 0 ? { flip } : {}
3725
+ };
3726
+ }
3727
+ function drawingFill(fill) {
3728
+ if (fill.kind === "none") return { type: "none" };
3729
+ return {
3730
+ type: "solid",
3731
+ color: {
3732
+ type: "rgb",
3733
+ value: fill.color.hex,
3734
+ // Transparency becomes an alpha transform on the colour. DrawingML
3735
+ // states *opacity*, so the value is the complement of what the author
3736
+ // wrote; the backend scales the percentage into `a:alpha`'s thousandths
3737
+ // itself, so it must be handed a plain percentage here.
3738
+ ...fill.transparencyPercent !== void 0 ? { transforms: { alpha: 100 - fill.transparencyPercent } } : {}
3739
+ }
3740
+ };
3741
+ }
3742
+ function drawingOutline(outline) {
3743
+ return {
3744
+ ...outline.widthEmu !== void 0 ? { width: outline.widthEmu } : {},
3745
+ ...outline.dash ? { dash: outline.dash } : {},
3746
+ ...outline.color ? { type: "solidFill", color: { value: outline.color.hex } } : {}
3747
+ };
3748
+ }
3749
+ function bodyProperties(text) {
3750
+ const insets = text.insetsEmu;
3751
+ return {
3752
+ ...text.anchor ? { anchor: BODY_ANCHOR[text.anchor] } : {},
3753
+ // `lIns`/`tIns`/`rIns`/`bIns` is the vocabulary `a:bodyPr` actually has;
3754
+ // the backend also accepts a `margins` object and folds it into the same
3755
+ // attributes.
3756
+ ...insets?.left !== void 0 ? { lIns: insets.left } : {},
3757
+ ...insets?.top !== void 0 ? { tIns: insets.top } : {},
3758
+ ...insets?.right !== void 0 ? { rIns: insets.right } : {},
3759
+ ...insets?.bottom !== void 0 ? { bIns: insets.bottom } : {}
3760
+ };
3761
+ }
3762
+ function sourceRectangle(crop) {
3763
+ const thousandths = (fraction) => Math.round(fraction * 1e5);
3764
+ return {
3765
+ ...crop.left !== void 0 ? { left: thousandths(crop.left) } : {},
3766
+ ...crop.top !== void 0 ? { top: thousandths(crop.top) } : {},
3767
+ ...crop.right !== void 0 ? { right: thousandths(crop.right) } : {},
3768
+ ...crop.bottom !== void 0 ? { bottom: thousandths(crop.bottom) } : {}
3769
+ };
3770
+ }
3451
3771
  function shapeOptions(shape, ctx) {
3452
3772
  const id = String(ctx.nextDrawingId());
3453
3773
  return {
@@ -3465,23 +3785,8 @@ function shapeOptions(shape, ctx) {
3465
3785
  color: { type: "rgb", value: shape.fill.hex }
3466
3786
  }
3467
3787
  } : {},
3468
- ...shape.outline ? {
3469
- outline: {
3470
- ...shape.outline.widthEmu !== void 0 ? { width: shape.outline.widthEmu } : {},
3471
- fill: {
3472
- type: "solid",
3473
- color: { type: "rgb", value: shape.outline.color.hex }
3474
- }
3475
- }
3476
- } : {},
3477
- ...shape.insetsEmu ? {
3478
- bodyProperties: {
3479
- ...shape.insetsEmu.top !== void 0 ? { topInset: shape.insetsEmu.top } : {},
3480
- ...shape.insetsEmu.bottom !== void 0 ? { bottomInset: shape.insetsEmu.bottom } : {},
3481
- ...shape.insetsEmu.left !== void 0 ? { leftInset: shape.insetsEmu.left } : {},
3482
- ...shape.insetsEmu.right !== void 0 ? { rightInset: shape.insetsEmu.right } : {}
3483
- }
3484
- } : {},
3788
+ ...shape.outline ? { outline: drawingOutline(shape.outline) } : {},
3789
+ ...shape.insetsEmu ? { bodyProperties: bodyProperties({ insetsEmu: shape.insetsEmu }) } : {},
3485
3790
  ...shape.floating ? { floating: floatingOptions2(shape.floating) } : {}
3486
3791
  };
3487
3792
  }
@@ -3838,7 +4143,7 @@ function numberingConfig2(numbering) {
3838
4143
  }))
3839
4144
  };
3840
4145
  }
3841
- var WRAP_TYPE2, TOC_PAGE_TAB_TWIPS;
4146
+ var WRAP_TYPE2, BODY_ANCHOR, TOC_PAGE_TAB_TWIPS;
3842
4147
  var init_emit2 = __esm({
3843
4148
  "src/renderers/office-open/emit.ts"() {
3844
4149
  "use strict";
@@ -3849,6 +4154,11 @@ var init_emit2 = __esm({
3849
4154
  tight: 2,
3850
4155
  topAndBottom: 3
3851
4156
  };
4157
+ BODY_ANCHOR = {
4158
+ top: "t",
4159
+ middle: "ctr",
4160
+ bottom: "b"
4161
+ };
3852
4162
  TOC_PAGE_TAB_TWIPS = 9025;
3853
4163
  }
3854
4164
  });
@@ -3912,6 +4222,7 @@ __export(office_open_exports, {
3912
4222
  buildDocumentOptions: () => buildDocumentOptions,
3913
4223
  createOfficeOpenDocxRenderer: () => createOfficeOpenDocxRenderer
3914
4224
  });
4225
+ import AdmZip4 from "adm-zip";
3915
4226
  async function createOfficeOpenDocxRenderer() {
3916
4227
  const backend = await import(
3917
4228
  /* @vite-ignore */
@@ -3927,13 +4238,19 @@ async function createOfficeOpenDocxRenderer() {
3927
4238
  format: "docx",
3928
4239
  capabilities: OFFICE_OPEN_CAPABILITIES,
3929
4240
  async render(ir, options) {
3930
- const document = await buildDocumentOptions(ir);
4241
+ const charts = [];
4242
+ const document = await buildDocumentOptions(ir, charts);
3931
4243
  const bytes = await backend.generateDocument(document, {
3932
4244
  type: "uint8array"
3933
4245
  });
3934
- const raw = Buffer.from(
4246
+ let raw = Buffer.from(
3935
4247
  bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
3936
4248
  );
4249
+ if (charts.length > 0) {
4250
+ const zip = new AdmZip4(raw);
4251
+ spliceChartParts(zip, charts);
4252
+ raw = zip.toBuffer();
4253
+ }
3937
4254
  if (options?.deterministic === false) return new Uint8Array(raw);
3938
4255
  return new Uint8Array(
3939
4256
  canonicalizeDocxBuffer(
@@ -3947,11 +4264,12 @@ async function createOfficeOpenDocxRenderer() {
3947
4264
  }
3948
4265
  };
3949
4266
  }
3950
- async function buildDocumentOptions(ir) {
4267
+ async function buildDocumentOptions(ir, charts = []) {
3951
4268
  let nextDrawingId = 1;
3952
4269
  const ctx = {
3953
4270
  pictures: await prepareImages2(ir),
3954
- nextDrawingId: () => nextDrawingId++
4271
+ nextDrawingId: () => nextDrawingId++,
4272
+ charts
3955
4273
  };
3956
4274
  return {
3957
4275
  styles: emitStyles2(ir.styles),
@@ -4026,18 +4344,9 @@ async function prepareImages2(ir) {
4026
4344
  index === 0 ? data : distinguishImageBytes(data, type, size)
4027
4345
  );
4028
4346
  });
4029
- resources.set(resource.id, (image, drawingId) => {
4030
- const rasterSize = {
4031
- width: emuToPixels(image.widthEmu),
4032
- height: emuToPixels(image.heightEmu)
4033
- };
4034
- const sizeKey = `${rasterSize.width}x${rasterSize.height}`;
4035
- const transformation = {
4036
- // Raw numbers are EMUs in @office-open/docx. Passing pixels here makes
4037
- // a normal image only a few hundred EMUs wide, effectively a dot.
4038
- width: image.widthEmu,
4039
- height: image.heightEmu
4040
- };
4347
+ resources.set(resource.id, (placement) => {
4348
+ const sizeKey = placementKey(placement);
4349
+ const stem = `${resource.id}-${sizeKey}`;
4041
4350
  return {
4042
4351
  type,
4043
4352
  // @office-open/docx stores a drawing transformation on its deduplicated
@@ -4045,29 +4354,28 @@ async function prepareImages2(ir) {
4045
4354
  // reuse the first size, so each distinct placement size gets equivalent
4046
4355
  // image bytes carrying a harmless format-native marker.
4047
4356
  data: placementData.get(sizeKey) ?? data,
4048
- transformation,
4049
- // The id is stated rather than left to the backend's process-global
4050
- // counter. `name` stays empty, which is what it was before and what
4051
- // the backend falls back to.
4052
- altText: { id: String(drawingId) },
4357
+ // Named after the resource and the size it is drawn at, so two
4358
+ // placements of one image at one size share a single part and a third
4359
+ // at another size gets its own.
4360
+ fileName: `${stem}.${type}`,
4053
4361
  ...type === "svg" ? {
4054
4362
  // Word before 2016 draws the fallback rather than the vector. A
4055
4363
  // raster that could not be produced falls back to the SVG bytes,
4056
4364
  // which is what this pipeline has always shipped.
4057
4365
  fallback: {
4058
4366
  type: "png",
4059
- data: rasters.get(`${image.resourceId}:${sizeKey}`) ?? data
4060
- }
4061
- } : {},
4062
- ...image.floating ? { floating: floatingOptions2(image.floating) } : {}
4063
- // No `description` or `title`: no DOCX this pipeline has produced
4064
- // carries `wp:docPr` alt text, and the compiler warns so the gap is
4065
- // visible rather than silent.
4367
+ data: rasters.get(`${resource.id}:${sizeKey}`) ?? data
4368
+ },
4369
+ fallbackFileName: `${stem}-fallback.png`
4370
+ } : {}
4066
4371
  };
4067
4372
  });
4068
4373
  }
4069
4374
  return resources;
4070
4375
  }
4376
+ function placementKey(placement) {
4377
+ return `${emuToPixels(placement.widthEmu)}x${emuToPixels(placement.heightEmu)}`;
4378
+ }
4071
4379
  function distinguishImageBytes(data, mediaType, placement) {
4072
4380
  const marker = Buffer.from(`json-to-office:${placement}`, "utf8");
4073
4381
  switch (mediaType) {
@@ -4166,12 +4474,27 @@ function crc32(data) {
4166
4474
  }
4167
4475
  function collectImagePlacements2(ir) {
4168
4476
  const placements = /* @__PURE__ */ new Map();
4477
+ const record = (resourceId, placement) => {
4478
+ const sizes = placements.get(resourceId) ?? /* @__PURE__ */ new Set();
4479
+ sizes.add(placementKey(placement));
4480
+ placements.set(resourceId, sizes);
4481
+ };
4169
4482
  const visitInline = (inline) => {
4170
4483
  if (inline.kind === "image") {
4171
- const key = `${emuToPixels(inline.widthEmu)}x${emuToPixels(inline.heightEmu)}`;
4172
- const sizes = placements.get(inline.resourceId) ?? /* @__PURE__ */ new Set();
4173
- sizes.add(key);
4174
- placements.set(inline.resourceId, sizes);
4484
+ record(inline.resourceId, inline);
4485
+ return;
4486
+ }
4487
+ if (inline.kind === "drawingGroup") {
4488
+ for (const child of inline.children) {
4489
+ if (child.kind === "picture") {
4490
+ record(child.resourceId, {
4491
+ widthEmu: child.frame.widthEmu,
4492
+ heightEmu: child.frame.heightEmu
4493
+ });
4494
+ continue;
4495
+ }
4496
+ child.text?.paragraphs.forEach(visitBlock);
4497
+ }
4175
4498
  return;
4176
4499
  }
4177
4500
  if (inline.kind === "hyperlink" || inline.kind === "revision") {
@@ -4221,6 +4544,7 @@ var init_office_open = __esm({
4221
4544
  "src/renderers/office-open/index.ts"() {
4222
4545
  "use strict";
4223
4546
  init_features();
4547
+ init_chartParts();
4224
4548
  init_imageUtils();
4225
4549
  init_packageDocument();
4226
4550
  init_emit2();
@@ -4556,6 +4880,9 @@ import {
4556
4880
  DEFAULT_VISUAL_DPI,
4557
4881
  MAX_RASTERIZE_BATCH_SLIDES
4558
4882
  } from "@json-to-office/shared";
4883
+ import {
4884
+ isNativeVisualProps
4885
+ } from "@json-to-office/shared-docx";
4559
4886
 
4560
4887
  // src/components/visual.ts
4561
4888
  import { createHash } from "crypto";
@@ -4753,7 +5080,9 @@ function collectVisualProps(root) {
4753
5080
  const obj = node;
4754
5081
  if (typeof obj.name === "string" && obj.enabled === false) return;
4755
5082
  if (obj.name === "visual" && obj.props && typeof obj.props === "object") {
4756
- found.push(obj.props);
5083
+ if (!isNativeVisualProps(obj.props)) {
5084
+ found.push(obj.props);
5085
+ }
4757
5086
  return;
4758
5087
  }
4759
5088
  for (const value of Object.values(obj)) visit(value);
@@ -5033,6 +5362,9 @@ import {
5033
5362
  clampVisualDpi as clampVisualDpi2,
5034
5363
  DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI2
5035
5364
  } from "@json-to-office/shared";
5365
+ import {
5366
+ isNativeVisualProps as isNativeVisualProps2
5367
+ } from "@json-to-office/shared-docx";
5036
5368
 
5037
5369
  // src/components/highcharts.ts
5038
5370
  init_colorUtils();
@@ -5194,6 +5526,7 @@ async function desugarExternals(document, options) {
5194
5526
  return transformComponents(document, async (node) => {
5195
5527
  if (node.enabled === false) return void 0;
5196
5528
  if (node.name === "visual") {
5529
+ if (isNativeVisualProps2(node.props)) return void 0;
5197
5530
  return withNodeIdentity(node, {
5198
5531
  name: "image",
5199
5532
  props: await visualImageProps(
@@ -5246,6 +5579,9 @@ async function visualImageProps(props, prerastered, options) {
5246
5579
 
5247
5580
  // src/core/imageResources.ts
5248
5581
  init_imageUtils();
5582
+ import {
5583
+ isNativeVisualProps as isNativeVisualProps3
5584
+ } from "@json-to-office/shared-docx";
5249
5585
  async function loadImageResources(components) {
5250
5586
  const sources = /* @__PURE__ */ new Set();
5251
5587
  for (const component of components) collectSources(component, sources);
@@ -5284,6 +5620,9 @@ function collectSources(component, out) {
5284
5620
  const children = component.children;
5285
5621
  for (const child of children ?? []) collectSources(child, out);
5286
5622
  const props = component.props ?? {};
5623
+ if (component.name === "visual" && isNativeVisualProps3(props)) {
5624
+ collectNativeVisualSources(props, out);
5625
+ }
5287
5626
  for (const key of ["header", "footer"]) {
5288
5627
  const part = props[key];
5289
5628
  if (Array.isArray(part)) {
@@ -5305,6 +5644,23 @@ function collectSources(component, out) {
5305
5644
  }
5306
5645
  }
5307
5646
  }
5647
+ function collectNativeVisualSources(props, out) {
5648
+ const canvas = props.canvas;
5649
+ const background = canvas?.background?.image;
5650
+ if (background) {
5651
+ const source = resolveImageSource(background);
5652
+ if (source) out.add(source);
5653
+ }
5654
+ const elements = props.elements;
5655
+ if (!Array.isArray(elements)) return;
5656
+ for (const element of elements) {
5657
+ if (!element || element.name !== "image") continue;
5658
+ const source = resolveImageSource(
5659
+ element.props ?? {}
5660
+ );
5661
+ if (source) out.add(source);
5662
+ }
5663
+ }
5308
5664
  function collectCellSource(content, out) {
5309
5665
  if (content && typeof content === "object") {
5310
5666
  collectSources(content, out);
@@ -5316,7 +5672,520 @@ init_colorUtils();
5316
5672
  import {
5317
5673
  FeatureRequirementCollector
5318
5674
  } from "@json-to-office/shared/rendering";
5319
- import { synthesizeFamilyName } from "@json-to-office/shared";
5675
+ import {
5676
+ synthesizeFamilyName,
5677
+ DEFAULT_CHART_THEME_COLORS as DEFAULT_CHART_THEME_COLORS2
5678
+ } from "@json-to-office/shared";
5679
+ import {
5680
+ isNativeVisualProps as isNativeVisualProps4
5681
+ } from "@json-to-office/shared-docx";
5682
+
5683
+ // src/ir/nativeVisual.ts
5684
+ init_units();
5685
+ init_imageUtils();
5686
+ var PIXELS_PER_INCH2 = 96;
5687
+ var DEFAULT_WIDTH_FRACTION = 0.75;
5688
+ var ASSUMED_FONT_SIZE_POINTS = 18;
5689
+ var MIN_DERIVED_TEXT_HEIGHT_INCHES = 0.5;
5690
+ var DERIVED_LINE_HEIGHT_RATIO = 1.6;
5691
+ var GEOMETRY_ALIASES = {
5692
+ arrow: "rightArrow",
5693
+ lightning: "lightningBolt"
5694
+ };
5695
+ var DASH_VALUES = /* @__PURE__ */ new Set([
5696
+ "solid",
5697
+ "dash",
5698
+ "dot",
5699
+ "dashDot"
5700
+ ]);
5701
+ var ANCHOR = {
5702
+ top: "top",
5703
+ middle: "middle",
5704
+ bottom: "bottom"
5705
+ };
5706
+ var ALIGNMENT = {
5707
+ left: "left",
5708
+ center: "center",
5709
+ right: "right",
5710
+ justify: "justified"
5711
+ };
5712
+ function compileNativeVisualGroup(props, outer) {
5713
+ const canvas = {
5714
+ widthEmu: inchesToEmu(props.canvas.width),
5715
+ heightEmu: inchesToEmu(props.canvas.height)
5716
+ };
5717
+ const children = [];
5718
+ let refused = false;
5719
+ const deps = {
5720
+ ...outer,
5721
+ reject: (detail) => {
5722
+ refused = true;
5723
+ outer.reject(detail);
5724
+ }
5725
+ };
5726
+ const background = props.canvas.background;
5727
+ if (background?.color) {
5728
+ const color = deps.color(background.color);
5729
+ if (!color) {
5730
+ deps.reject(`canvas.background.color "${background.color}"`);
5731
+ } else {
5732
+ children.push({
5733
+ kind: "shape",
5734
+ frame: fullBleed(canvas),
5735
+ geometry: "rect",
5736
+ fill: { kind: "solid", color },
5737
+ name: "Canvas background"
5738
+ });
5739
+ }
5740
+ }
5741
+ if (background?.image) {
5742
+ const source = resolveImageSource(background.image);
5743
+ const resource = source ? deps.picture(source) : void 0;
5744
+ if (!resource) {
5745
+ deps.reject("canvas.background.image could not be loaded");
5746
+ } else {
5747
+ children.push({
5748
+ kind: "picture",
5749
+ frame: fullBleed(canvas),
5750
+ resourceId: resource.resourceId,
5751
+ name: "Canvas background"
5752
+ });
5753
+ }
5754
+ }
5755
+ for (const [index, element] of (props.elements ?? []).entries()) {
5756
+ if (element.enabled === false) continue;
5757
+ const child = compileElement(element, index, canvas, deps);
5758
+ if (child) children.push(child);
5759
+ }
5760
+ return refused ? void 0 : { canvas, children };
5761
+ }
5762
+ function compileElement(element, index, canvas, deps) {
5763
+ const at = `elements[${index}]`;
5764
+ switch (element.name) {
5765
+ case "text":
5766
+ return compileText(element.props, at, canvas, deps);
5767
+ case "shape":
5768
+ return compileShape(element.props, at, canvas, deps);
5769
+ case "image":
5770
+ return compileImage(element.props, at, canvas, deps);
5771
+ default:
5772
+ deps.reject(`${at} is a "${element.name}"`);
5773
+ return void 0;
5774
+ }
5775
+ }
5776
+ function compileText(props, at, canvas, deps) {
5777
+ const runs = textRuns(props);
5778
+ if (!runs) {
5779
+ deps.warn(
5780
+ `[core-docx] native visual ${at} has neither "text" nor "runs"; nothing was drawn for it.`
5781
+ );
5782
+ return void 0;
5783
+ }
5784
+ const colors = resolveColors(
5785
+ [
5786
+ ["color", props.color],
5787
+ ["fill.color", props.fill?.color],
5788
+ ...runs.map((run, i) => [`runs[${i}].color`, run.color]),
5789
+ ...runs.map(
5790
+ (run, i) => [
5791
+ `runs[${i}].underline.color`,
5792
+ underlineColor(run.underline)
5793
+ ]
5794
+ ),
5795
+ ["underline.color", underlineColor(props.underline)]
5796
+ ],
5797
+ at,
5798
+ deps
5799
+ );
5800
+ if (!colors) return void 0;
5801
+ const frame = resolveFrame(props, canvas, {
5802
+ defaultHeightInches: derivedTextHeightInches(props, runs)
5803
+ });
5804
+ const paragraphs = textParagraphs(runs, props, colors, at);
5805
+ return {
5806
+ kind: "shape",
5807
+ frame,
5808
+ geometry: "rect",
5809
+ isTextBox: true,
5810
+ // A text box with no fill is transparent, which is what the rasterized
5811
+ // path draws; saying so explicitly stops Word applying its own default.
5812
+ fill: props.fill?.color ? solidFill(colors.get("fill.color"), props.fill.transparency) : { kind: "none" },
5813
+ text: {
5814
+ paragraphs,
5815
+ // pptx anchors a text box at the top unless told otherwise, and a
5816
+ // drawing that changes anchor between render modes moves its text.
5817
+ anchor: ANCHOR[props.valign ?? "top"] ?? "top",
5818
+ // A text box's insets default to nothing, again matching pptx. A shape
5819
+ // deliberately leaves them unstated so OOXML's own defaults apply.
5820
+ insetsEmu: marginInsets(props.margin) ?? ZERO_INSETS
5821
+ },
5822
+ name: `Text ${at}`
5823
+ };
5824
+ }
5825
+ var ZERO_INSETS = { top: 0, bottom: 0, left: 0, right: 0 };
5826
+ function textRuns(props) {
5827
+ if (props.runs?.length) return props.runs;
5828
+ if (props.text === void 0) return void 0;
5829
+ return [{ text: props.text }];
5830
+ }
5831
+ function textParagraphs(runs, props, colors, at) {
5832
+ const paragraphs = [];
5833
+ let current = [];
5834
+ const flush = () => {
5835
+ paragraphs.push({
5836
+ kind: "paragraph",
5837
+ id: `${at}.p${paragraphs.length}`,
5838
+ path: `${at}.paragraphs[${paragraphs.length}]`,
5839
+ children: current,
5840
+ formatting: {
5841
+ ...props.align ? { alignment: ALIGNMENT[props.align] } : {},
5842
+ // A drawing's text is set flush against the shape; the document's
5843
+ // paragraph spacing would push it away from the top edge, which is not
5844
+ // what an absolutely-placed box means.
5845
+ spacing: { beforeTwips: 0, afterTwips: 0 }
5846
+ }
5847
+ });
5848
+ current = [];
5849
+ };
5850
+ runs.forEach((run, index) => {
5851
+ const formatting = runFormatting(run, props, colors, index);
5852
+ const lines = run.text.split("\n");
5853
+ lines.forEach((line, lineIndex) => {
5854
+ if (lineIndex > 0) current.push({ kind: "lineBreak" });
5855
+ if (line) {
5856
+ current.push({ kind: "text", text: line, ...formatting });
5857
+ }
5858
+ });
5859
+ if (run.breakLine && index < runs.length - 1) flush();
5860
+ });
5861
+ flush();
5862
+ return paragraphs;
5863
+ }
5864
+ function runFormatting(run, props, colors, index) {
5865
+ const fontFamily = run.fontFace ?? props.fontFace;
5866
+ const sizePoints = run.fontSize ?? props.fontSize;
5867
+ const color = colors.get(`runs[${index}].color`) ?? (run.color === void 0 ? colors.get("color") : void 0);
5868
+ const bold = run.bold ?? props.bold;
5869
+ const italic = run.italic ?? props.italic;
5870
+ const strike = run.strike ?? props.strike;
5871
+ const underline = compileUnderline(
5872
+ run.underline ?? props.underline,
5873
+ colors.get(
5874
+ run.underline !== void 0 ? `runs[${index}].underline.color` : "underline.color"
5875
+ )
5876
+ );
5877
+ const formatting = {
5878
+ ...fontFamily ? { fontFamily } : {},
5879
+ ...sizePoints !== void 0 ? { sizeHalfPoints: pointsToHalfPoints(sizePoints) } : {},
5880
+ ...color ? { color } : {},
5881
+ ...bold !== void 0 ? { bold } : {},
5882
+ ...italic !== void 0 ? { italic } : {},
5883
+ ...strike !== void 0 ? { strike } : {},
5884
+ ...underline ? { underline } : {}
5885
+ };
5886
+ return Object.keys(formatting).length > 0 ? { formatting } : {};
5887
+ }
5888
+ var UNDERLINE_TYPES = {
5889
+ sng: "single",
5890
+ dbl: "double",
5891
+ dash: "dash",
5892
+ dotted: "dotted"
5893
+ };
5894
+ function compileUnderline(value, color) {
5895
+ if (value === void 0 || value === false) return void 0;
5896
+ const style = value === true ? "sng" : value.style ?? "sng";
5897
+ return {
5898
+ type: UNDERLINE_TYPES[style] ?? "single",
5899
+ ...color ? { color } : {}
5900
+ };
5901
+ }
5902
+ function underlineColor(value) {
5903
+ return typeof value === "object" ? value.color : void 0;
5904
+ }
5905
+ function derivedTextHeightInches(props, runs) {
5906
+ const fontSize = props.fontSize ?? ASSUMED_FONT_SIZE_POINTS;
5907
+ const lineCount = runs.reduce(
5908
+ (total, run) => total + (run.breakLine ? 1 : 0) + (run.text.match(/\n/g)?.length ?? 0),
5909
+ 1
5910
+ );
5911
+ return Math.max(
5912
+ MIN_DERIVED_TEXT_HEIGHT_INCHES,
5913
+ fontSize / 72 * DERIVED_LINE_HEIGHT_RATIO * lineCount
5914
+ );
5915
+ }
5916
+ function compileShape(props, at, canvas, deps) {
5917
+ const segments = shapeSegments(props);
5918
+ const colors = resolveColors(
5919
+ [
5920
+ ["fill.color", props.fill?.color],
5921
+ ["line.color", props.line?.color],
5922
+ ["fontColor", props.fontColor],
5923
+ ...segments.map(
5924
+ (segment, i) => [`text[${i}].color`, segment.color]
5925
+ )
5926
+ ],
5927
+ at,
5928
+ deps
5929
+ );
5930
+ if (!colors) return void 0;
5931
+ const frame = resolveFrame(props, canvas, { defaultHeightInches: 0 });
5932
+ if (props.flipH) frame.flipHorizontal = true;
5933
+ if (props.flipV) frame.flipVertical = true;
5934
+ const dash = props.line?.dashType;
5935
+ if (dash !== void 0 && !DASH_VALUES.has(dash)) {
5936
+ deps.reject(`${at}.line.dashType "${dash}"`);
5937
+ return void 0;
5938
+ }
5939
+ const outline = props.line ? {
5940
+ ...colors.get("line.color") ? { color: colors.get("line.color") } : {},
5941
+ ...props.line.width !== void 0 ? { widthEmu: pointsToEmu(props.line.width) } : {},
5942
+ ...dash && dash !== "solid" ? { dash } : {}
5943
+ } : void 0;
5944
+ const text = segments.length ? {
5945
+ paragraphs: shapeParagraphs(segments, props, colors, at),
5946
+ anchor: ANCHOR[props.valign ?? "middle"] ?? "middle",
5947
+ // Unstated insets are left unstated so OOXML's own shape defaults
5948
+ // apply, which is what the raster path draws.
5949
+ ...marginInsets(props.margin) ? { insetsEmu: marginInsets(props.margin) } : {}
5950
+ } : void 0;
5951
+ return {
5952
+ kind: "shape",
5953
+ frame,
5954
+ geometry: GEOMETRY_ALIASES[props.type] ?? props.type,
5955
+ ...props.fill ? {
5956
+ fill: props.fill.color ? solidFill(colors.get("fill.color"), props.fill.transparency) : { kind: "none" }
5957
+ } : {},
5958
+ ...outline && Object.keys(outline).length > 0 ? { outline } : {},
5959
+ ...text ? { text } : {},
5960
+ name: `Shape ${at}`
5961
+ };
5962
+ }
5963
+ function shapeSegments(props) {
5964
+ if (props.text === void 0) return [];
5965
+ if (typeof props.text === "string") {
5966
+ return props.text ? [{ text: props.text }] : [];
5967
+ }
5968
+ return props.text;
5969
+ }
5970
+ function shapeParagraphs(segments, props, colors, at) {
5971
+ const paragraphs = [];
5972
+ let current = [];
5973
+ const flush = () => {
5974
+ paragraphs.push({
5975
+ kind: "paragraph",
5976
+ id: `${at}.p${paragraphs.length}`,
5977
+ path: `${at}.paragraphs[${paragraphs.length}]`,
5978
+ children: current,
5979
+ formatting: {
5980
+ // A shape centres its text unless told otherwise, which is what the
5981
+ // rasterized shape draws.
5982
+ alignment: props.align ? ALIGNMENT[props.align] : "center",
5983
+ spacing: { beforeTwips: 0, afterTwips: 0 }
5984
+ }
5985
+ });
5986
+ current = [];
5987
+ };
5988
+ segments.forEach((segment, index) => {
5989
+ const fontFamily = segment.fontFace ?? props.fontFace;
5990
+ const sizePoints = segment.fontSize ?? props.fontSize;
5991
+ const color = colors.get(`text[${index}].color`) ?? (segment.color === void 0 ? colors.get("fontColor") : void 0);
5992
+ const bold = segment.bold ?? props.bold;
5993
+ const italic = segment.italic ?? props.italic;
5994
+ const formatting = {
5995
+ ...fontFamily ? { fontFamily } : {},
5996
+ ...sizePoints !== void 0 ? { sizeHalfPoints: pointsToHalfPoints(sizePoints) } : {},
5997
+ ...color ? { color } : {},
5998
+ ...bold !== void 0 ? { bold } : {},
5999
+ ...italic !== void 0 ? { italic } : {}
6000
+ };
6001
+ const wrapped = Object.keys(formatting).length > 0 ? { formatting } : void 0;
6002
+ segment.text.split("\n").forEach((line, lineIndex) => {
6003
+ if (lineIndex > 0) current.push({ kind: "lineBreak" });
6004
+ if (line) current.push({ kind: "text", text: line, ...wrapped });
6005
+ });
6006
+ if (segment.breakLine && index < segments.length - 1) flush();
6007
+ });
6008
+ flush();
6009
+ return paragraphs;
6010
+ }
6011
+ function compileImage(props, at, canvas, deps) {
6012
+ const source = resolveImageSource(props);
6013
+ if (!source) {
6014
+ deps.reject(`${at} has none of "path", "base64" or "svg"`);
6015
+ return void 0;
6016
+ }
6017
+ const resource = deps.picture(source);
6018
+ if (!resource) {
6019
+ deps.reject(`${at} image could not be loaded`);
6020
+ return void 0;
6021
+ }
6022
+ const aspect = resource.intrinsic ? resource.intrinsic.width / resource.intrinsic.height : void 0;
6023
+ const intrinsicInches = resource.intrinsic ? {
6024
+ width: resource.intrinsic.width / PIXELS_PER_INCH2,
6025
+ height: resource.intrinsic.height / PIXELS_PER_INCH2
6026
+ } : void 0;
6027
+ const box = resolveImageBox(props, canvas, aspect, intrinsicInches);
6028
+ const sizing = props.sizing;
6029
+ if (!sizing || !aspect) {
6030
+ if (sizing && !aspect) {
6031
+ deps.warn(
6032
+ `[core-docx] native visual ${at} asks for "${sizing.type}" sizing, but the image's stored size could not be read; it was stretched to its box instead.`
6033
+ );
6034
+ }
6035
+ return picture(box, props, resource, at);
6036
+ }
6037
+ const boxAspect = box.widthEmu / box.heightEmu;
6038
+ if (sizing.type === "contain") {
6039
+ const fitted = boxAspect > aspect ? {
6040
+ widthEmu: Math.round(box.heightEmu * aspect),
6041
+ heightEmu: box.heightEmu
6042
+ } : {
6043
+ widthEmu: box.widthEmu,
6044
+ heightEmu: Math.round(box.widthEmu / aspect)
6045
+ };
6046
+ return picture(
6047
+ {
6048
+ ...box,
6049
+ xEmu: box.xEmu + Math.round((box.widthEmu - fitted.widthEmu) / 2),
6050
+ yEmu: box.yEmu + Math.round((box.heightEmu - fitted.heightEmu) / 2),
6051
+ ...fitted
6052
+ },
6053
+ props,
6054
+ resource,
6055
+ at
6056
+ );
6057
+ }
6058
+ const visible = boxAspect > aspect ? { widthFraction: 1, heightFraction: aspect / boxAspect } : { widthFraction: boxAspect / aspect, heightFraction: 1 };
6059
+ const crop = sizing.type === "cover" ? {
6060
+ ...visible.widthFraction < 1 ? {
6061
+ left: (1 - visible.widthFraction) / 2,
6062
+ right: (1 - visible.widthFraction) / 2
6063
+ } : {},
6064
+ ...visible.heightFraction < 1 ? {
6065
+ top: (1 - visible.heightFraction) / 2,
6066
+ bottom: (1 - visible.heightFraction) / 2
6067
+ } : {}
6068
+ } : {
6069
+ ...visible.widthFraction < 1 ? { right: 1 - visible.widthFraction } : {},
6070
+ ...visible.heightFraction < 1 ? { bottom: 1 - visible.heightFraction } : {}
6071
+ };
6072
+ return picture(box, props, resource, at, crop);
6073
+ }
6074
+ function picture(frame, props, resource, at, crop) {
6075
+ return {
6076
+ kind: "picture",
6077
+ frame,
6078
+ resourceId: resource.resourceId,
6079
+ ...props.alt ? { altText: props.alt } : {},
6080
+ ...crop && Object.keys(crop).length > 0 ? { crop } : {},
6081
+ name: `Image ${at}`
6082
+ };
6083
+ }
6084
+ function resolveImageBox(props, canvas, aspect, intrinsicInches) {
6085
+ const width = props.sizing?.w ?? props.w;
6086
+ const height = props.sizing?.h ?? props.h;
6087
+ const widthEmu = width !== void 0 ? resolveExtent(width, canvas.widthEmu) : void 0;
6088
+ const heightEmu = height !== void 0 ? resolveExtent(height, canvas.heightEmu) : void 0;
6089
+ const resolved = (() => {
6090
+ if (widthEmu !== void 0 && heightEmu !== void 0) {
6091
+ return { widthEmu, heightEmu };
6092
+ }
6093
+ if (widthEmu !== void 0) {
6094
+ return {
6095
+ widthEmu,
6096
+ heightEmu: aspect ? Math.round(widthEmu / aspect) : Math.round(widthEmu * (3 / 4))
6097
+ };
6098
+ }
6099
+ if (heightEmu !== void 0) {
6100
+ return {
6101
+ widthEmu: aspect ? Math.round(heightEmu * aspect) : Math.round(heightEmu * (4 / 3)),
6102
+ heightEmu
6103
+ };
6104
+ }
6105
+ if (intrinsicInches) {
6106
+ return {
6107
+ widthEmu: inchesToEmu(intrinsicInches.width),
6108
+ heightEmu: inchesToEmu(intrinsicInches.height)
6109
+ };
6110
+ }
6111
+ const fallbackWidth = Math.round(canvas.widthEmu * DEFAULT_WIDTH_FRACTION);
6112
+ return {
6113
+ widthEmu: fallbackWidth,
6114
+ heightEmu: Math.round(fallbackWidth * (3 / 4))
6115
+ };
6116
+ })();
6117
+ return {
6118
+ xEmu: props.x !== void 0 ? resolveOffset(props.x, canvas.widthEmu) : 0,
6119
+ yEmu: props.y !== void 0 ? resolveOffset(props.y, canvas.heightEmu) : 0,
6120
+ ...resolved,
6121
+ ...rotation(props.rotate)
6122
+ };
6123
+ }
6124
+ function fullBleed(canvas) {
6125
+ return {
6126
+ xEmu: 0,
6127
+ yEmu: 0,
6128
+ widthEmu: canvas.widthEmu,
6129
+ heightEmu: canvas.heightEmu
6130
+ };
6131
+ }
6132
+ function resolveFrame(props, canvas, options) {
6133
+ return {
6134
+ xEmu: props.x !== void 0 ? resolveOffset(props.x, canvas.widthEmu) : 0,
6135
+ yEmu: props.y !== void 0 ? resolveOffset(props.y, canvas.heightEmu) : 0,
6136
+ widthEmu: props.w !== void 0 ? resolveExtent(props.w, canvas.widthEmu) : Math.round(canvas.widthEmu * DEFAULT_WIDTH_FRACTION),
6137
+ heightEmu: props.h !== void 0 ? resolveExtent(props.h, canvas.heightEmu) : inchesToEmu(options.defaultHeightInches),
6138
+ ...rotation(props.rotate)
6139
+ };
6140
+ }
6141
+ function rotation(degrees) {
6142
+ if (degrees === void 0 || degrees % 360 === 0) return {};
6143
+ return { rotationDegrees: degrees };
6144
+ }
6145
+ function resolveOffset(value, axisEmu) {
6146
+ return resolveLength(value, axisEmu);
6147
+ }
6148
+ function resolveExtent(value, axisEmu) {
6149
+ return Math.max(0, resolveLength(value, axisEmu));
6150
+ }
6151
+ function resolveLength(value, axisEmu) {
6152
+ if (typeof value === "number") return inchesToEmu(value);
6153
+ const percent = Number.parseFloat(value);
6154
+ if (!Number.isFinite(percent)) return 0;
6155
+ return Math.round(percent / 100 * axisEmu);
6156
+ }
6157
+ function marginInsets(margin) {
6158
+ if (margin === void 0) return void 0;
6159
+ const [top, right, bottom, left] = Array.isArray(margin) ? margin : [margin, margin, margin, margin];
6160
+ return {
6161
+ top: pointsToEmu(top ?? 0),
6162
+ right: pointsToEmu(right ?? 0),
6163
+ bottom: pointsToEmu(bottom ?? 0),
6164
+ left: pointsToEmu(left ?? 0)
6165
+ };
6166
+ }
6167
+ function solidFill(color, transparency) {
6168
+ return {
6169
+ kind: "solid",
6170
+ color,
6171
+ ...transparency !== void 0 && transparency > 0 ? { transparencyPercent: transparency } : {}
6172
+ };
6173
+ }
6174
+ function resolveColors(entries, at, deps) {
6175
+ const resolved = /* @__PURE__ */ new Map();
6176
+ let failed = false;
6177
+ for (const [key, value] of entries) {
6178
+ if (value === void 0) continue;
6179
+ const color = deps.color(value);
6180
+ if (!color) {
6181
+ deps.reject(`${at}.${key} "${value}"`);
6182
+ failed = true;
6183
+ continue;
6184
+ }
6185
+ resolved.set(key, color);
6186
+ }
6187
+ return failed ? void 0 : resolved;
6188
+ }
5320
6189
 
5321
6190
  // src/styles/themeToStyles.ts
5322
6191
  init_themes();
@@ -7159,7 +8028,7 @@ function decoratedStyle(match, base) {
7159
8028
  function pushSegment(out, text, options, override = {}) {
7160
8029
  const bold = override.bold ?? options.base.bold;
7161
8030
  const italic = override.italic ?? options.base.italic;
7162
- const formatting = runFormatting(options, { bold, italic });
8031
+ const formatting = runFormatting2(options, { bold, italic });
7163
8032
  const lines = text.split("\n");
7164
8033
  for (const [lineIndex, line] of lines.entries()) {
7165
8034
  if (!line && lineIndex === 0) continue;
@@ -7237,7 +8106,7 @@ function pushNoProofWords(out, text, formatting, options) {
7237
8106
  out.push({ kind: "text", text: text.slice(lastIndex), formatting });
7238
8107
  }
7239
8108
  }
7240
- function runFormatting(options, state) {
8109
+ function runFormatting2(options, state) {
7241
8110
  const formatting = { ...options.base };
7242
8111
  if (state.bold !== void 0) formatting.bold = state.bold;
7243
8112
  if (state.italic !== void 0) formatting.italic = state.italic;
@@ -7609,7 +8478,11 @@ function compileComponent(component, scope) {
7609
8478
  case "columns":
7610
8479
  return compileColumns(component, scope);
7611
8480
  case "image":
7612
- return compileImage(component, scope);
8481
+ return compileImage2(component, scope);
8482
+ case "visual":
8483
+ return compileVisual(component, scope);
8484
+ case "chart":
8485
+ return compileChart(component, scope);
7613
8486
  case "table":
7614
8487
  return compileTable(component, scope);
7615
8488
  default:
@@ -7853,7 +8726,7 @@ function compileTextBox(component, scope) {
7853
8726
  const style = props.style;
7854
8727
  const padding = style?.padding;
7855
8728
  if (props.renderAs === "shape") {
7856
- const shape = compileShape(props, children, scope);
8729
+ const shape = compileShape2(props, children, scope);
7857
8730
  if (shape) return shape;
7858
8731
  }
7859
8732
  ctx.features.require("tables", path3);
@@ -7907,7 +8780,7 @@ function compileTextBox(component, scope) {
7907
8780
  ];
7908
8781
  }
7909
8782
  var EMU_PER_PIXEL = 9525;
7910
- function compileShape(props, children, scope) {
8783
+ function compileShape2(props, children, scope) {
7911
8784
  const { ctx, path: path3 } = scope;
7912
8785
  const width = shapeSize(props.width, "width", ctx);
7913
8786
  const height = shapeSize(props.height, "height", ctx);
@@ -8401,7 +9274,7 @@ function reportUnlowered(component, props, text, scope) {
8401
9274
  }
8402
9275
  function compileRuns(props, text, ctx, path3, mode = "inline") {
8403
9276
  const font = props.font ?? {};
8404
- const base = runFormatting2(font, props, ctx);
9277
+ const base = runFormatting3(font, props, ctx);
8405
9278
  if (base.language) ctx.features.require("proofing-language", path3);
8406
9279
  if (base.noProof) ctx.features.require("proofing-language", path3);
8407
9280
  const words = mergeNoProofWords(noProofWords(ctx.theme), props.noProofWords);
@@ -8671,7 +9544,7 @@ function isoDate(date) {
8671
9544
  function isoDateTime(date) {
8672
9545
  return date.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
8673
9546
  }
8674
- function runFormatting2(font, props, ctx) {
9547
+ function runFormatting3(font, props, ctx) {
8675
9548
  const hasWeightRequest = font.fontWeight != null || font.bold === true;
8676
9549
  const effectiveFamily = font.family ?? (hasWeightRequest ? resolveBodyFamily(ctx.theme) : void 0);
8677
9550
  const weighted = applyFontWeightAlias({
@@ -8834,8 +9707,107 @@ function headingLevel(level) {
8834
9707
  const value = typeof level === "number" ? level : 1;
8835
9708
  return value >= 1 && value <= 6 ? value : 1;
8836
9709
  }
9710
+ function compileVisual(component, scope) {
9711
+ const { ctx, path: path3 } = scope;
9712
+ const props = component.props ?? {};
9713
+ if (!isNativeVisualProps4(props)) {
9714
+ ctx.unsupported.push({
9715
+ name: "visual",
9716
+ path: path3,
9717
+ detail: "a raster visual reached the compiler; it should have been rasterized during desugaring"
9718
+ });
9719
+ return [];
9720
+ }
9721
+ const group = compileNativeVisualGroup(
9722
+ props,
9723
+ nativeVisualDeps(ctx, path3)
9724
+ );
9725
+ if (!group) return [];
9726
+ const size = visualPlacementPixels(props, group.canvas, {
9727
+ widthPx: Math.round(twipsToPixels(getAvailableWidthTwips(ctx.theme))),
9728
+ heightPx: Math.round(twipsToPixels(getAvailableHeightTwips(ctx.theme)))
9729
+ });
9730
+ const drawing = {
9731
+ kind: "drawingGroup",
9732
+ widthEmu: pixelsToEmu(size.width),
9733
+ heightEmu: pixelsToEmu(size.height),
9734
+ canvasWidthEmu: group.canvas.widthEmu,
9735
+ canvasHeightEmu: group.canvas.heightEmu,
9736
+ children: group.children,
9737
+ ...typeof props.alt === "string" && props.alt ? { altText: props.alt } : {},
9738
+ ...props.floating ? { floating: compileFloating(props.floating, ctx, path3) } : {}
9739
+ };
9740
+ ctx.features.require("drawing-groups", path3);
9741
+ if (props.floating) ctx.features.require("floating-images", path3);
9742
+ const spacing = {};
9743
+ if (props.spacing?.before !== void 0) {
9744
+ spacing.beforeTwips = pointsToTwips2(props.spacing.before);
9745
+ }
9746
+ if (props.spacing?.after !== void 0) {
9747
+ spacing.afterTwips = pointsToTwips2(props.spacing.after);
9748
+ }
9749
+ const blocks = [
9750
+ {
9751
+ kind: "paragraph",
9752
+ id: scope.id,
9753
+ path: path3,
9754
+ children: [drawing],
9755
+ formatting: {
9756
+ // A floating drawing is anchored, so aligning its paragraph would move
9757
+ // the anchor rather than the graphic — the same rule an image follows.
9758
+ ...props.floating ? {} : { alignment: compileAlignment(props.alignment) ?? "center" },
9759
+ ...Object.keys(spacing).length > 0 ? { spacing } : {},
9760
+ ...props.keepNext !== void 0 ? { keepNext: props.keepNext } : {},
9761
+ ...props.keepLines !== void 0 ? { keepLines: props.keepLines } : {}
9762
+ }
9763
+ }
9764
+ ];
9765
+ const caption = captionBlock(props.caption, scope, "visual");
9766
+ if (caption === void 0) return [];
9767
+ if (caption) blocks.push(caption);
9768
+ return blocks;
9769
+ }
9770
+ function nativeVisualDeps(ctx, path3) {
9771
+ return {
9772
+ color: (value) => {
9773
+ try {
9774
+ return irColor(resolveColor(value, ctx.theme));
9775
+ } catch {
9776
+ return void 0;
9777
+ }
9778
+ },
9779
+ picture: (source) => {
9780
+ const loaded = ctx.images.get(source);
9781
+ if (!loaded) return void 0;
9782
+ const mediaType = detectImageType(source, loaded.contentType);
9783
+ if (mediaType === "svg") ctx.features.require("svg-images", path3);
9784
+ return {
9785
+ resourceId: declareResource(loaded, mediaType, ctx),
9786
+ mediaType,
9787
+ ...loaded.intrinsic ? { intrinsic: loaded.intrinsic } : {}
9788
+ };
9789
+ },
9790
+ reject: (detail) => ctx.unsupported.push({ name: "visual", path: path3, detail }),
9791
+ warn: (message) => warnOnce(ctx, `visual:${message}`, message)
9792
+ };
9793
+ }
9794
+ function visualPlacementPixels(props, canvas, reference) {
9795
+ const canvasWidthPx = Math.round(emuToPixels(canvas.widthEmu));
9796
+ const canvasHeightPx = Math.round(emuToPixels(canvas.heightEmu));
9797
+ const targetWidth = props.width !== void 0 ? parseWidthValue(props.width, reference.widthPx) : void 0;
9798
+ const targetHeight = props.height !== void 0 ? parseDimensionValue(props.height, reference.heightPx) : void 0;
9799
+ if (targetWidth === void 0 && targetHeight === void 0) {
9800
+ return { width: canvasWidthPx, height: canvasHeightPx };
9801
+ }
9802
+ return calculateMissingDimension(
9803
+ canvasWidthPx,
9804
+ canvasHeightPx,
9805
+ targetWidth,
9806
+ targetHeight
9807
+ );
9808
+ }
8837
9809
  var UNLOWERED_IMAGE_PROPS = ["comment"];
8838
- function compileImage(component, scope) {
9810
+ function compileImage2(component, scope) {
8839
9811
  const { ctx, path: path3 } = scope;
8840
9812
  const props = component.props ?? {};
8841
9813
  for (const prop of UNLOWERED_IMAGE_PROPS) {
@@ -8896,32 +9868,142 @@ function compileImage(component, scope) {
8896
9868
  }
8897
9869
  }
8898
9870
  ];
8899
- if (props.caption) {
8900
- const caption = String(props.caption);
8901
- const syntax = containsUnsupportedSyntax(caption);
8902
- if (syntax) {
8903
- ctx.unsupported.push({ name: "image", path: path3, detail: syntax });
8904
- return [];
9871
+ const caption = captionBlock(props.caption, scope, "image");
9872
+ if (caption === void 0) return [];
9873
+ if (caption) blocks.push(caption);
9874
+ return blocks;
9875
+ }
9876
+ var DEFAULT_CHART_HEIGHT_INCHES = 3;
9877
+ function compileChart(component, scope) {
9878
+ const { ctx, path: path3 } = scope;
9879
+ const props = component.props ?? {};
9880
+ if (props.type === "bubble") {
9881
+ throw new Error(
9882
+ `Chart at ${path3} is a bubble chart, which no docx renderer draws; use the pptx \`chart\` component on the pptxgenjs renderer for one.`
9883
+ );
9884
+ }
9885
+ const rawSeries = Array.isArray(props.data) ? props.data : [];
9886
+ if (rawSeries.length === 0) {
9887
+ throw new Error(`Chart at ${path3} has no data series.`);
9888
+ }
9889
+ const series = rawSeries.map((entry, index) => {
9890
+ const raw = entry ?? {};
9891
+ const label = typeof raw.name === "string" ? raw.name : `series ${index}`;
9892
+ const labels = raw.labels;
9893
+ const values = raw.values;
9894
+ if (!Array.isArray(labels) || !Array.isArray(values)) {
9895
+ throw new Error(
9896
+ `Chart series "${label}" at ${path3} needs both "labels" and "values"; every series carries its own, not just the first.`
9897
+ );
8905
9898
  }
8906
- blocks.push({
8907
- kind: "paragraph",
8908
- id: `${scope.id}:caption`,
8909
- path: `${path3}.caption`,
8910
- styleId: "Normal",
8911
- // Captions default to left alignment, whatever the figure did.
8912
- formatting: { alignment: "left" },
8913
- // A caption reaches the parser only when it carries a decorator, which
8914
- // is why a link in an otherwise plain caption stays literal.
8915
- children: DECORATED.test(caption) ? parseInline(caption, {
8916
- base: {},
8917
- hyperlinks: true,
8918
- resolvePlaceholder: placeholderResolver(ctx),
8919
- resolveCrossReference: crossReferenceResolver(ctx, path3)
8920
- }) : parseLiteral(caption, { base: {} })
8921
- });
9899
+ if (labels.length !== values.length) {
9900
+ throw new Error(
9901
+ `Chart series "${label}" at ${path3} has ${labels.length} labels and ${values.length} values; they must be the same length.`
9902
+ );
9903
+ }
9904
+ return {
9905
+ ...typeof raw.name === "string" ? { name: raw.name } : {},
9906
+ labels: labels.map((value) => String(value)),
9907
+ values: values.map((value) => Number(value))
9908
+ };
9909
+ });
9910
+ const categories = series[0].labels;
9911
+ for (const [index, entry] of series.entries()) {
9912
+ if (index === 0) continue;
9913
+ if (entry.labels.length !== categories.length || entry.labels.some((label, i) => label !== categories[i])) {
9914
+ throw new Error(
9915
+ `Chart series "${entry.name ?? `series ${index}`}" at ${path3} has different labels from the first series. A chart has one category axis, so every series must name the same categories in the same order.`
9916
+ );
9917
+ }
9918
+ }
9919
+ const colors = Array.isArray(props.chartColors) ? props.chartColors.map(
9920
+ (color) => resolveColor(String(color), ctx.theme)
9921
+ ) : DEFAULT_CHART_THEME_COLORS2.map((token) => {
9922
+ const value = ctx.theme?.colors?.[token];
9923
+ if (typeof value !== "string" || value.length === 0) return void 0;
9924
+ try {
9925
+ return resolveColor(token, ctx.theme);
9926
+ } catch {
9927
+ return void 0;
9928
+ }
9929
+ }).filter((color) => color !== void 0);
9930
+ const page = getPageSetup(ctx.theme);
9931
+ const contentWidthInches = (page.size.width - page.margin.left - page.margin.right) / 1440;
9932
+ const widthInches = typeof props.width === "number" ? props.width : contentWidthInches;
9933
+ const heightInches = typeof props.height === "number" ? props.height : DEFAULT_CHART_HEIGHT_INCHES;
9934
+ const chart = {
9935
+ kind: "chart",
9936
+ chartType: props.type,
9937
+ series,
9938
+ colors,
9939
+ widthEmu: inchesToEmu(widthInches),
9940
+ heightEmu: inchesToEmu(heightInches),
9941
+ ...typeof props.title === "string" ? { title: props.title } : {},
9942
+ ...props.showTitle !== void 0 ? { showTitle: props.showTitle } : {},
9943
+ ...props.showLegend !== void 0 ? { showLegend: props.showLegend } : {},
9944
+ ...props.legendPos ? { legendPosition: props.legendPos } : {},
9945
+ ...typeof props.catAxisTitle === "string" ? { categoryAxisTitle: props.catAxisTitle } : {},
9946
+ ...typeof props.valAxisTitle === "string" ? { valueAxisTitle: props.valAxisTitle } : {},
9947
+ ...typeof props.alt === "string" && props.alt ? { altText: props.alt } : {},
9948
+ ...props.floating ? { floating: compileFloating(props.floating, ctx, path3) } : {}
9949
+ };
9950
+ ctx.features.require("charts", path3);
9951
+ if (props.floating) ctx.features.require("floating-images", path3);
9952
+ const spacing = {};
9953
+ if (props.spacing?.before !== void 0) {
9954
+ spacing.beforeTwips = pointsToTwips2(props.spacing.before);
8922
9955
  }
9956
+ if (props.spacing?.after !== void 0) {
9957
+ spacing.afterTwips = pointsToTwips2(props.spacing.after);
9958
+ }
9959
+ const blocks = [
9960
+ {
9961
+ kind: "paragraph",
9962
+ id: scope.id,
9963
+ path: path3,
9964
+ children: [chart],
9965
+ formatting: {
9966
+ // An anchored chart moves with its anchor, so aligning the paragraph
9967
+ // would move the anchor rather than the chart — the same reasoning as
9968
+ // a floating image.
9969
+ ...props.floating ? {} : { alignment: compileAlignment(props.alignment) ?? "center" },
9970
+ ...Object.keys(spacing).length > 0 ? { spacing } : {},
9971
+ ...props.keepNext !== void 0 ? { keepNext: props.keepNext } : {},
9972
+ ...props.keepLines !== void 0 ? { keepLines: props.keepLines } : {}
9973
+ }
9974
+ }
9975
+ ];
9976
+ const caption = captionBlock(props.caption, scope, "chart");
9977
+ if (caption === void 0) return [];
9978
+ if (caption) blocks.push(caption);
8923
9979
  return blocks;
8924
9980
  }
9981
+ function captionBlock(value, scope, componentName) {
9982
+ if (!value) return null;
9983
+ const { ctx, path: path3 } = scope;
9984
+ const caption = String(value);
9985
+ const syntax = containsUnsupportedSyntax(caption);
9986
+ if (syntax) {
9987
+ ctx.unsupported.push({ name: componentName, path: path3, detail: syntax });
9988
+ return void 0;
9989
+ }
9990
+ return {
9991
+ kind: "paragraph",
9992
+ id: `${scope.id}:caption`,
9993
+ path: `${path3}.caption`,
9994
+ styleId: "Normal",
9995
+ // Captions default to left alignment, whatever the figure did.
9996
+ formatting: { alignment: "left" },
9997
+ // A caption reaches the parser only when it carries a decorator, which
9998
+ // is why a link in an otherwise plain caption stays literal.
9999
+ children: DECORATED.test(caption) ? parseInline(caption, {
10000
+ base: {},
10001
+ hyperlinks: true,
10002
+ resolvePlaceholder: placeholderResolver(ctx),
10003
+ resolveCrossReference: crossReferenceResolver(ctx, path3)
10004
+ }) : parseLiteral(caption, { base: {} })
10005
+ };
10006
+ }
8925
10007
  function relativeFrom(value, axis) {
8926
10008
  const allowed = axis === "horizontal" ? ["character", "column", "margin", "page"] : ["margin", "page", "paragraph", "line"];
8927
10009
  return typeof value === "string" && allowed.includes(value) ? value : void 0;
@@ -9345,6 +10427,9 @@ function cellChildren(cell, baseStyle, scope, rowRevision) {
9345
10427
  if (content.name === "image") {
9346
10428
  return wrap(cellImage(content, base, ctx));
9347
10429
  }
10430
+ if (content.name === "visual") {
10431
+ return wrap(cellVisual(content, scope));
10432
+ }
9348
10433
  if (content.name !== "paragraph") {
9349
10434
  return wrap([
9350
10435
  {
@@ -9384,6 +10469,40 @@ function cellChildren(cell, baseStyle, scope, rowRevision) {
9384
10469
  })
9385
10470
  );
9386
10471
  }
10472
+ function cellVisual(component, scope) {
10473
+ const { ctx, path: path3 } = scope;
10474
+ const props = component.props ?? {};
10475
+ if (!isNativeVisualProps4(props)) {
10476
+ ctx.unsupported.push({
10477
+ name: "visual",
10478
+ path: path3,
10479
+ detail: "a raster visual reached the compiler; it should have been rasterized during desugaring"
10480
+ });
10481
+ return [];
10482
+ }
10483
+ const group = compileNativeVisualGroup(
10484
+ props,
10485
+ nativeVisualDeps(ctx, path3)
10486
+ );
10487
+ if (!group) return [];
10488
+ const size = visualPlacementPixels(props, group.canvas, {
10489
+ widthPx: CELL_DRAWING_REFERENCE.width,
10490
+ heightPx: CELL_DRAWING_REFERENCE.height
10491
+ });
10492
+ ctx.features.require("drawing-groups", path3);
10493
+ return [
10494
+ {
10495
+ kind: "drawingGroup",
10496
+ widthEmu: pixelsToEmu(size.width),
10497
+ heightEmu: pixelsToEmu(size.height),
10498
+ canvasWidthEmu: group.canvas.widthEmu,
10499
+ canvasHeightEmu: group.canvas.heightEmu,
10500
+ children: group.children,
10501
+ ...typeof props.alt === "string" && props.alt ? { altText: props.alt } : {}
10502
+ }
10503
+ ];
10504
+ }
10505
+ var CELL_DRAWING_REFERENCE = { width: 300, height: 200 };
9387
10506
  function cellImage(component, base, ctx) {
9388
10507
  const props = component.props ?? {};
9389
10508
  const source = resolveImageSource(props);