@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
package/dist/index.js CHANGED
@@ -2005,6 +2005,12 @@ function pointsToEighthPoints(points) {
2005
2005
  function inchesToTwips(inches) {
2006
2006
  return Math.round(inches * TWIPS_PER_INCH);
2007
2007
  }
2008
+ function inchesToEmu(inches) {
2009
+ return Math.round(inches * EMU_PER_INCH);
2010
+ }
2011
+ function pointsToEmu(points) {
2012
+ return Math.round(points * EMU_PER_INCH / 72);
2013
+ }
2008
2014
  function twipsToEmu(twips) {
2009
2015
  return Math.round(twips * EMU_PER_TWIP);
2010
2016
  }
@@ -2207,6 +2213,14 @@ function inlineChildren(children, resources = /* @__PURE__ */ new Map()) {
2207
2213
  case "shape":
2208
2214
  out.push(emitShape(child, resources));
2209
2215
  break;
2216
+ case "drawingGroup":
2217
+ throw new Error(
2218
+ "the docxjs renderer has no emitter for a drawing group; this document should have been refused by the capability check"
2219
+ );
2220
+ case "chart":
2221
+ throw new Error(
2222
+ "the docxjs renderer has no emitter for a chart; this document should have been refused by the capability check"
2223
+ );
2210
2224
  case "noteReference":
2211
2225
  out.push(
2212
2226
  child.noteKind === "endnote" ? new EndnoteReferenceRun(child.id) : new FootnoteReferenceRun(child.id)
@@ -2308,7 +2322,7 @@ function paragraphOptions(formatting) {
2308
2322
  const options = {};
2309
2323
  if (!formatting) return options;
2310
2324
  if (formatting.alignment) {
2311
- options.alignment = ALIGNMENT[formatting.alignment];
2325
+ options.alignment = ALIGNMENT2[formatting.alignment];
2312
2326
  }
2313
2327
  if (formatting.spacing) {
2314
2328
  const spacing = {};
@@ -2542,11 +2556,11 @@ function emitBorder(border3) {
2542
2556
  color: border3.color?.hex ?? "000000"
2543
2557
  };
2544
2558
  }
2545
- var ALIGNMENT, PAGE_FIELD, WRAP_TYPE, VERTICAL_ALIGN, TABLE_ANCHOR, HORIZONTAL_POSITION, VERTICAL_POSITION, BORDER_STYLE;
2559
+ var ALIGNMENT2, PAGE_FIELD, WRAP_TYPE, VERTICAL_ALIGN, TABLE_ANCHOR, HORIZONTAL_POSITION, VERTICAL_POSITION, BORDER_STYLE;
2546
2560
  var init_emit = __esm({
2547
2561
  "src/renderers/docxjs/emit.ts"() {
2548
2562
  "use strict";
2549
- ALIGNMENT = {
2563
+ ALIGNMENT2 = {
2550
2564
  left: AlignmentType.LEFT,
2551
2565
  center: AlignmentType.CENTER,
2552
2566
  right: AlignmentType.RIGHT,
@@ -2669,7 +2683,7 @@ function paragraphProperties(formatting) {
2669
2683
  out.spacing = spacing;
2670
2684
  }
2671
2685
  if (formatting.alignment !== void 0) {
2672
- out.alignment = ALIGNMENT[formatting.alignment] ?? formatting.alignment;
2686
+ out.alignment = ALIGNMENT2[formatting.alignment] ?? formatting.alignment;
2673
2687
  }
2674
2688
  if (formatting.keepNext !== void 0) out.keepNext = formatting.keepNext;
2675
2689
  if (formatting.keepLines !== void 0) out.keepLines = formatting.keepLines;
@@ -2798,6 +2812,26 @@ var init_features = __esm({
2798
2812
  "text-frames",
2799
2813
  /** Native shape text boxes. */
2800
2814
  "text-boxes",
2815
+ /**
2816
+ * A DrawingML group: shapes, text boxes and pictures sharing one child
2817
+ * coordinate space, drawn as a single anchored or inline object.
2818
+ *
2819
+ * Separate from `text-boxes` because a lone `wps:wsp` run is a far smaller
2820
+ * ask than `wpg:wgp` with child transforms, preset geometry and grouped
2821
+ * pictures — a backend can plausibly have the first and not the second.
2822
+ */
2823
+ "drawing-groups",
2824
+ /**
2825
+ * A native chart part: a `c:chartSpace` with its own embedded workbook.
2826
+ *
2827
+ * Separate from `images` because a chart is not a picture with extra data —
2828
+ * it is its own part, its own relationship and its own workbook, and a
2829
+ * backend either writes all three or writes none. docx.js has no chart
2830
+ * primitive at all, which is what turns a `chart` component sent to that
2831
+ * backend into a named capability error rather than a document missing a
2832
+ * figure.
2833
+ */
2834
+ "charts",
2801
2835
  /** A table-of-contents field. */
2802
2836
  "toc",
2803
2837
  /** TOC entries baked in so an unrefreshed reader still shows them. */
@@ -3044,7 +3078,7 @@ function numberingConfig(numbering) {
3044
3078
  level: level.level,
3045
3079
  format: level.format,
3046
3080
  text: level.text,
3047
- alignment: level.alignment ? ALIGNMENT[level.alignment] : void 0,
3081
+ alignment: level.alignment ? ALIGNMENT2[level.alignment] : void 0,
3048
3082
  ...level.suffix ? { suffix: level.suffix } : {},
3049
3083
  style: {
3050
3084
  ...level.indent ? {
@@ -3262,7 +3296,16 @@ var init_docxjs = __esm({
3262
3296
  "cached-fields",
3263
3297
  "shading",
3264
3298
  "borders",
3265
- "rtl"
3299
+ "rtl",
3300
+ // `drawing-groups` is the one exclusion that is a real backend gap rather
3301
+ // than a slice boundary: docx.js has no `wpg:wgp`. Leaving it out is what
3302
+ // turns a native `visual` sent to this backend into a named capability
3303
+ // error instead of a document with the graphic missing.
3304
+ "drawing-groups",
3305
+ // `charts` is the second real backend gap: docx.js has no chart primitive at
3306
+ // all, so a `chart` component sent here becomes a named capability error
3307
+ // rather than a document missing a figure.
3308
+ "charts"
3266
3309
  ]);
3267
3310
  DOCXJS_CAPABILITIES = new Set(
3268
3311
  [...ALL_DOCX_FEATURES].filter((feature) => !NOT_YET_EMITTED.has(feature))
@@ -3270,11 +3313,105 @@ var init_docxjs = __esm({
3270
3313
  }
3271
3314
  });
3272
3315
 
3316
+ // src/utils/chartWorkbook.ts
3317
+ import AdmZip3 from "adm-zip";
3318
+ import { chartWorkbookParts } from "@json-to-office/shared/rendering";
3319
+ import {
3320
+ CHART_WORKBOOK_SHEET_NAME,
3321
+ categoryReference,
3322
+ columnLetter,
3323
+ seriesNameReference,
3324
+ seriesValueReference
3325
+ } from "@json-to-office/shared/rendering";
3326
+ function buildChartWorkbook(series) {
3327
+ const zip = new AdmZip3();
3328
+ for (const [name, content] of chartWorkbookParts(series)) {
3329
+ zip.addFile(name, Buffer.from(content, "utf8"));
3330
+ }
3331
+ for (const entry of zip.getEntries()) {
3332
+ entry.header.timeval = WORKBOOK_TIMESTAMP;
3333
+ }
3334
+ return new Uint8Array(zip.toBuffer());
3335
+ }
3336
+ var WORKBOOK_TIMESTAMP;
3337
+ var init_chartWorkbook = __esm({
3338
+ "src/utils/chartWorkbook.ts"() {
3339
+ "use strict";
3340
+ init_packageDocument();
3341
+ WORKBOOK_TIMESTAMP = toDosTime(DEFAULT_GENERATION_DATE);
3342
+ }
3343
+ });
3344
+
3345
+ // src/renderers/office-open/chartParts.ts
3346
+ import {
3347
+ CHART_WORKBOOK_CONTENT_TYPE,
3348
+ chartWorkbookRelsXml,
3349
+ matchChartParts,
3350
+ spliceChartXml
3351
+ } from "@json-to-office/shared/rendering";
3352
+ function spliceInput(chart) {
3353
+ return {
3354
+ chartType: chart.chartType,
3355
+ series: chart.series,
3356
+ colors: chart.colors,
3357
+ ...chart.legendPosition ? { legendPosition: chart.legendPosition } : {},
3358
+ ...chart.categoryAxisTitle ? { categoryAxis: { title: chart.categoryAxisTitle } } : {},
3359
+ ...chart.valueAxisTitle ? { valueAxis: { title: chart.valueAxisTitle } } : {}
3360
+ };
3361
+ }
3362
+ function declareWorkbookContentType(zip) {
3363
+ const entry = zip.getEntry("[Content_Types].xml");
3364
+ if (!entry) return;
3365
+ const xml = entry.getData().toString("utf8");
3366
+ if (xml.includes(`Extension="xlsx"`)) return;
3367
+ zip.updateFile(
3368
+ entry,
3369
+ Buffer.from(
3370
+ xml.replace(
3371
+ '<Default Extension="xml"',
3372
+ `<Default Extension="xlsx" ContentType="${CHART_WORKBOOK_CONTENT_TYPE}"/><Default Extension="xml"`
3373
+ ),
3374
+ "utf8"
3375
+ )
3376
+ );
3377
+ }
3378
+ function spliceChartParts(zip, charts) {
3379
+ if (charts.length === 0) return;
3380
+ const parts = zip.getEntries().map((entry) => entry.entryName).map((name) => name.match(/^word\/charts\/chart(\d+)\.xml$/)).filter((match) => match !== null).map(
3381
+ (match) => [
3382
+ Number(match[1]),
3383
+ zip.getEntry(match[0]).getData().toString("utf8")
3384
+ ]
3385
+ );
3386
+ for (const { ordinal, xml, chart } of matchChartParts(parts, charts)) {
3387
+ const workbookName = `chart${ordinal}.xlsx`;
3388
+ zip.updateFile(
3389
+ zip.getEntry(`word/charts/chart${ordinal}.xml`),
3390
+ Buffer.from(spliceChartXml(xml, spliceInput(chart)), "utf8")
3391
+ );
3392
+ zip.addFile(
3393
+ `word/embeddings/${workbookName}`,
3394
+ Buffer.from(buildChartWorkbook(chart.series))
3395
+ );
3396
+ zip.addFile(
3397
+ `word/charts/_rels/chart${ordinal}.xml.rels`,
3398
+ Buffer.from(chartWorkbookRelsXml(workbookName), "utf8")
3399
+ );
3400
+ }
3401
+ declareWorkbookContentType(zip);
3402
+ }
3403
+ var init_chartParts = __esm({
3404
+ "src/renderers/office-open/chartParts.ts"() {
3405
+ "use strict";
3406
+ init_chartWorkbook();
3407
+ }
3408
+ });
3409
+
3273
3410
  // src/renderers/office-open/emit.ts
3274
3411
  import { assertNever as assertNever2 } from "@json-to-office/shared/rendering";
3275
3412
  function emptyContext() {
3276
3413
  let next = 1;
3277
- return { pictures: /* @__PURE__ */ new Map(), nextDrawingId: () => next++ };
3414
+ return { pictures: /* @__PURE__ */ new Map(), nextDrawingId: () => next++, charts: [] };
3278
3415
  }
3279
3416
  function simpleField(instruction, cachedText) {
3280
3417
  return {
@@ -3373,15 +3510,15 @@ function inlineChildren2(children, ctx = emptyContext()) {
3373
3510
  out.push({ bookmarkEnd: { id: child.id } });
3374
3511
  break;
3375
3512
  case "image": {
3376
- const build = ctx.pictures.get(child.resourceId);
3377
- if (!build) {
3378
- throw new Error(
3379
- `no image was prepared for resource "${child.resourceId}"`
3380
- );
3381
- }
3382
3513
  const pending = breakOption();
3383
3514
  if (pending.break) out.push(pending);
3384
- out.push({ picture: build(child, ctx.nextDrawingId()) });
3515
+ out.push({ picture: pictureOptions(child, ctx) });
3516
+ break;
3517
+ }
3518
+ case "drawingGroup": {
3519
+ const pending = breakOption();
3520
+ if (pending.break) out.push(pending);
3521
+ out.push({ wpgGroup: drawingGroupOptions(child, ctx) });
3385
3522
  break;
3386
3523
  }
3387
3524
  case "hyperlink":
@@ -3410,6 +3547,10 @@ function inlineChildren2(children, ctx = emptyContext()) {
3410
3547
  case "shape":
3411
3548
  out.push({ wpsShape: shapeOptions(child, ctx) });
3412
3549
  break;
3550
+ case "chart":
3551
+ ctx.charts?.push(child);
3552
+ out.push({ chart: chartOptions(child, ctx) });
3553
+ break;
3413
3554
  case "noteReference":
3414
3555
  out.push(
3415
3556
  child.noteKind === "endnote" ? { endnoteReference: child.id } : { footnoteReference: child.id }
@@ -3476,6 +3617,185 @@ function floatingOptions2(floating) {
3476
3617
  zIndex: floating.zIndex
3477
3618
  };
3478
3619
  }
3620
+ function imageMedia(ctx, resourceId, placement) {
3621
+ const build = ctx.pictures.get(resourceId);
3622
+ if (!build) {
3623
+ throw new Error(`no image was prepared for resource "${resourceId}"`);
3624
+ }
3625
+ return build(placement);
3626
+ }
3627
+ function pictureOptions(image, ctx) {
3628
+ const media = imageMedia(ctx, image.resourceId, image);
3629
+ return {
3630
+ type: media.type,
3631
+ data: media.data,
3632
+ // No `fileName`: at run level the backend allocates one, and stating our
3633
+ // own would only fight it.
3634
+ ...media.fallback ? { fallback: media.fallback } : {},
3635
+ // Raw numbers are EMUs in @office-open/docx. Passing pixels here makes a
3636
+ // normal image only a few hundred EMUs wide, effectively a dot.
3637
+ transformation: { width: image.widthEmu, height: image.heightEmu },
3638
+ // The id is stated rather than left to the backend's process-global
3639
+ // counter. `name` stays empty, which is what it was before and what the
3640
+ // backend falls back to.
3641
+ altText: { id: String(ctx.nextDrawingId()) },
3642
+ ...image.floating ? { floating: floatingOptions2(image.floating) } : {}
3643
+ // No `description` or `title`: no DOCX this pipeline has produced carries
3644
+ // `wp:docPr` alt text, and the compiler warns so the gap is visible rather
3645
+ // than silent.
3646
+ };
3647
+ }
3648
+ function drawingGroupOptions(group, ctx) {
3649
+ const id = ctx.nextDrawingId();
3650
+ return {
3651
+ altText: {
3652
+ id: String(id),
3653
+ ...group.altText ? { description: group.altText } : {}
3654
+ },
3655
+ transformation: { width: group.widthEmu, height: group.heightEmu },
3656
+ childOffset: { x: 0, y: 0 },
3657
+ childExtent: { cx: group.canvasWidthEmu, cy: group.canvasHeightEmu },
3658
+ children: group.children.map((child) => groupChild(child, ctx)),
3659
+ ...group.floating ? { floating: floatingOptions2(group.floating) } : {}
3660
+ };
3661
+ }
3662
+ function chartOptions(chart, ctx) {
3663
+ const id = ctx.nextDrawingId();
3664
+ return {
3665
+ type: chart.chartType,
3666
+ categories: chart.series[0]?.labels ?? [],
3667
+ series: chart.series.map((entry, index) => ({
3668
+ name: entry.name ?? `Series ${index + 1}`,
3669
+ values: entry.values
3670
+ })),
3671
+ ...chart.title && chart.showTitle !== false ? { title: chart.title } : {},
3672
+ ...chart.showLegend !== void 0 ? { showLegend: chart.showLegend } : {},
3673
+ transformation: { width: chart.widthEmu, height: chart.heightEmu },
3674
+ altText: {
3675
+ id: String(id),
3676
+ ...chart.altText ? { description: chart.altText } : {}
3677
+ },
3678
+ ...chart.floating ? { floating: floatingOptions2(chart.floating) } : {}
3679
+ };
3680
+ }
3681
+ function groupChild(child, ctx) {
3682
+ return child.kind === "shape" ? groupShape(child, ctx) : groupPicture(child, ctx);
3683
+ }
3684
+ function groupShape(shape, ctx) {
3685
+ const id = ctx.nextDrawingId();
3686
+ return {
3687
+ type: "wps",
3688
+ transformation: childTransformation(shape.frame),
3689
+ data: {
3690
+ nonVisualProperties: {
3691
+ id,
3692
+ ...shape.name ? { name: shape.name } : {},
3693
+ // `txBox="1"` is how Word tells a text box from a shape that happens
3694
+ // to hold text, and it changes how the object behaves on selection.
3695
+ ...shape.isTextBox ? { textBox: "1" } : {}
3696
+ },
3697
+ presetGeometry: { preset: shape.geometry },
3698
+ ...shape.fill ? { fill: drawingFill(shape.fill) } : {},
3699
+ ...shape.outline ? { outline: drawingOutline(shape.outline) } : {},
3700
+ children: (shape.text?.paragraphs ?? []).map(
3701
+ (child) => paragraph(child, ctx)
3702
+ ),
3703
+ ...shape.text ? { bodyProperties: bodyProperties(shape.text) } : {}
3704
+ }
3705
+ };
3706
+ }
3707
+ function groupPicture(picture2, ctx) {
3708
+ const id = ctx.nextDrawingId();
3709
+ const media = imageMedia(ctx, picture2.resourceId, {
3710
+ widthEmu: picture2.frame.widthEmu,
3711
+ heightEmu: picture2.frame.heightEmu
3712
+ });
3713
+ return {
3714
+ type: media.type,
3715
+ data: media.data,
3716
+ fileName: media.fileName,
3717
+ ...media.fallback ? {
3718
+ fallback: {
3719
+ ...media.fallback,
3720
+ fileName: media.fallbackFileName
3721
+ }
3722
+ } : {},
3723
+ transformation: childTransformation(picture2.frame),
3724
+ nonVisualProperties: {
3725
+ id,
3726
+ ...picture2.name ? { name: picture2.name } : {},
3727
+ ...picture2.altText ? { description: picture2.altText } : {}
3728
+ },
3729
+ ...picture2.crop ? { sourceRectangle: sourceRectangle(picture2.crop) } : {}
3730
+ };
3731
+ }
3732
+ function childTransformation(frame) {
3733
+ const flip = {
3734
+ ...frame.flipHorizontal ? { horizontal: true } : {},
3735
+ ...frame.flipVertical ? { vertical: true } : {}
3736
+ };
3737
+ return {
3738
+ offset: {
3739
+ emus: { x: frame.xEmu, y: frame.yEmu },
3740
+ pixels: {
3741
+ x: Math.round(emuToPixels(frame.xEmu)),
3742
+ y: Math.round(emuToPixels(frame.yEmu))
3743
+ }
3744
+ },
3745
+ emus: { x: frame.widthEmu, y: frame.heightEmu },
3746
+ pixels: {
3747
+ x: Math.round(emuToPixels(frame.widthEmu)),
3748
+ y: Math.round(emuToPixels(frame.heightEmu))
3749
+ },
3750
+ // Degrees: the backend multiplies by 60000 on the way into `@rot`.
3751
+ ...frame.rotationDegrees !== void 0 ? { rotation: frame.rotationDegrees } : {},
3752
+ ...Object.keys(flip).length > 0 ? { flip } : {}
3753
+ };
3754
+ }
3755
+ function drawingFill(fill) {
3756
+ if (fill.kind === "none") return { type: "none" };
3757
+ return {
3758
+ type: "solid",
3759
+ color: {
3760
+ type: "rgb",
3761
+ value: fill.color.hex,
3762
+ // Transparency becomes an alpha transform on the colour. DrawingML
3763
+ // states *opacity*, so the value is the complement of what the author
3764
+ // wrote; the backend scales the percentage into `a:alpha`'s thousandths
3765
+ // itself, so it must be handed a plain percentage here.
3766
+ ...fill.transparencyPercent !== void 0 ? { transforms: { alpha: 100 - fill.transparencyPercent } } : {}
3767
+ }
3768
+ };
3769
+ }
3770
+ function drawingOutline(outline) {
3771
+ return {
3772
+ ...outline.widthEmu !== void 0 ? { width: outline.widthEmu } : {},
3773
+ ...outline.dash ? { dash: outline.dash } : {},
3774
+ ...outline.color ? { type: "solidFill", color: { value: outline.color.hex } } : {}
3775
+ };
3776
+ }
3777
+ function bodyProperties(text) {
3778
+ const insets = text.insetsEmu;
3779
+ return {
3780
+ ...text.anchor ? { anchor: BODY_ANCHOR[text.anchor] } : {},
3781
+ // `lIns`/`tIns`/`rIns`/`bIns` is the vocabulary `a:bodyPr` actually has;
3782
+ // the backend also accepts a `margins` object and folds it into the same
3783
+ // attributes.
3784
+ ...insets?.left !== void 0 ? { lIns: insets.left } : {},
3785
+ ...insets?.top !== void 0 ? { tIns: insets.top } : {},
3786
+ ...insets?.right !== void 0 ? { rIns: insets.right } : {},
3787
+ ...insets?.bottom !== void 0 ? { bIns: insets.bottom } : {}
3788
+ };
3789
+ }
3790
+ function sourceRectangle(crop) {
3791
+ const thousandths = (fraction) => Math.round(fraction * 1e5);
3792
+ return {
3793
+ ...crop.left !== void 0 ? { left: thousandths(crop.left) } : {},
3794
+ ...crop.top !== void 0 ? { top: thousandths(crop.top) } : {},
3795
+ ...crop.right !== void 0 ? { right: thousandths(crop.right) } : {},
3796
+ ...crop.bottom !== void 0 ? { bottom: thousandths(crop.bottom) } : {}
3797
+ };
3798
+ }
3479
3799
  function shapeOptions(shape, ctx) {
3480
3800
  const id = String(ctx.nextDrawingId());
3481
3801
  return {
@@ -3493,23 +3813,8 @@ function shapeOptions(shape, ctx) {
3493
3813
  color: { type: "rgb", value: shape.fill.hex }
3494
3814
  }
3495
3815
  } : {},
3496
- ...shape.outline ? {
3497
- outline: {
3498
- ...shape.outline.widthEmu !== void 0 ? { width: shape.outline.widthEmu } : {},
3499
- fill: {
3500
- type: "solid",
3501
- color: { type: "rgb", value: shape.outline.color.hex }
3502
- }
3503
- }
3504
- } : {},
3505
- ...shape.insetsEmu ? {
3506
- bodyProperties: {
3507
- ...shape.insetsEmu.top !== void 0 ? { topInset: shape.insetsEmu.top } : {},
3508
- ...shape.insetsEmu.bottom !== void 0 ? { bottomInset: shape.insetsEmu.bottom } : {},
3509
- ...shape.insetsEmu.left !== void 0 ? { leftInset: shape.insetsEmu.left } : {},
3510
- ...shape.insetsEmu.right !== void 0 ? { rightInset: shape.insetsEmu.right } : {}
3511
- }
3512
- } : {},
3816
+ ...shape.outline ? { outline: drawingOutline(shape.outline) } : {},
3817
+ ...shape.insetsEmu ? { bodyProperties: bodyProperties({ insetsEmu: shape.insetsEmu }) } : {},
3513
3818
  ...shape.floating ? { floating: floatingOptions2(shape.floating) } : {}
3514
3819
  };
3515
3820
  }
@@ -3866,7 +4171,7 @@ function numberingConfig2(numbering) {
3866
4171
  }))
3867
4172
  };
3868
4173
  }
3869
- var WRAP_TYPE2, TOC_PAGE_TAB_TWIPS;
4174
+ var WRAP_TYPE2, BODY_ANCHOR, TOC_PAGE_TAB_TWIPS;
3870
4175
  var init_emit2 = __esm({
3871
4176
  "src/renderers/office-open/emit.ts"() {
3872
4177
  "use strict";
@@ -3877,6 +4182,11 @@ var init_emit2 = __esm({
3877
4182
  tight: 2,
3878
4183
  topAndBottom: 3
3879
4184
  };
4185
+ BODY_ANCHOR = {
4186
+ top: "t",
4187
+ middle: "ctr",
4188
+ bottom: "b"
4189
+ };
3880
4190
  TOC_PAGE_TAB_TWIPS = 9025;
3881
4191
  }
3882
4192
  });
@@ -3940,6 +4250,7 @@ __export(office_open_exports, {
3940
4250
  buildDocumentOptions: () => buildDocumentOptions,
3941
4251
  createOfficeOpenDocxRenderer: () => createOfficeOpenDocxRenderer
3942
4252
  });
4253
+ import AdmZip4 from "adm-zip";
3943
4254
  async function createOfficeOpenDocxRenderer() {
3944
4255
  const backend = await import(
3945
4256
  /* @vite-ignore */
@@ -3955,13 +4266,19 @@ async function createOfficeOpenDocxRenderer() {
3955
4266
  format: "docx",
3956
4267
  capabilities: OFFICE_OPEN_CAPABILITIES,
3957
4268
  async render(ir, options) {
3958
- const document = await buildDocumentOptions(ir);
4269
+ const charts = [];
4270
+ const document = await buildDocumentOptions(ir, charts);
3959
4271
  const bytes = await backend.generateDocument(document, {
3960
4272
  type: "uint8array"
3961
4273
  });
3962
- const raw = Buffer.from(
4274
+ let raw = Buffer.from(
3963
4275
  bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
3964
4276
  );
4277
+ if (charts.length > 0) {
4278
+ const zip = new AdmZip4(raw);
4279
+ spliceChartParts(zip, charts);
4280
+ raw = zip.toBuffer();
4281
+ }
3965
4282
  if (options?.deterministic === false) return new Uint8Array(raw);
3966
4283
  return new Uint8Array(
3967
4284
  canonicalizeDocxBuffer(
@@ -3975,11 +4292,12 @@ async function createOfficeOpenDocxRenderer() {
3975
4292
  }
3976
4293
  };
3977
4294
  }
3978
- async function buildDocumentOptions(ir) {
4295
+ async function buildDocumentOptions(ir, charts = []) {
3979
4296
  let nextDrawingId = 1;
3980
4297
  const ctx = {
3981
4298
  pictures: await prepareImages2(ir),
3982
- nextDrawingId: () => nextDrawingId++
4299
+ nextDrawingId: () => nextDrawingId++,
4300
+ charts
3983
4301
  };
3984
4302
  return {
3985
4303
  styles: emitStyles2(ir.styles),
@@ -4054,18 +4372,9 @@ async function prepareImages2(ir) {
4054
4372
  index === 0 ? data : distinguishImageBytes(data, type, size)
4055
4373
  );
4056
4374
  });
4057
- resources.set(resource.id, (image, drawingId) => {
4058
- const rasterSize = {
4059
- width: emuToPixels(image.widthEmu),
4060
- height: emuToPixels(image.heightEmu)
4061
- };
4062
- const sizeKey = `${rasterSize.width}x${rasterSize.height}`;
4063
- const transformation = {
4064
- // Raw numbers are EMUs in @office-open/docx. Passing pixels here makes
4065
- // a normal image only a few hundred EMUs wide, effectively a dot.
4066
- width: image.widthEmu,
4067
- height: image.heightEmu
4068
- };
4375
+ resources.set(resource.id, (placement) => {
4376
+ const sizeKey = placementKey(placement);
4377
+ const stem = `${resource.id}-${sizeKey}`;
4069
4378
  return {
4070
4379
  type,
4071
4380
  // @office-open/docx stores a drawing transformation on its deduplicated
@@ -4073,29 +4382,28 @@ async function prepareImages2(ir) {
4073
4382
  // reuse the first size, so each distinct placement size gets equivalent
4074
4383
  // image bytes carrying a harmless format-native marker.
4075
4384
  data: placementData.get(sizeKey) ?? data,
4076
- transformation,
4077
- // The id is stated rather than left to the backend's process-global
4078
- // counter. `name` stays empty, which is what it was before and what
4079
- // the backend falls back to.
4080
- altText: { id: String(drawingId) },
4385
+ // Named after the resource and the size it is drawn at, so two
4386
+ // placements of one image at one size share a single part and a third
4387
+ // at another size gets its own.
4388
+ fileName: `${stem}.${type}`,
4081
4389
  ...type === "svg" ? {
4082
4390
  // Word before 2016 draws the fallback rather than the vector. A
4083
4391
  // raster that could not be produced falls back to the SVG bytes,
4084
4392
  // which is what this pipeline has always shipped.
4085
4393
  fallback: {
4086
4394
  type: "png",
4087
- data: rasters.get(`${image.resourceId}:${sizeKey}`) ?? data
4088
- }
4089
- } : {},
4090
- ...image.floating ? { floating: floatingOptions2(image.floating) } : {}
4091
- // No `description` or `title`: no DOCX this pipeline has produced
4092
- // carries `wp:docPr` alt text, and the compiler warns so the gap is
4093
- // visible rather than silent.
4395
+ data: rasters.get(`${resource.id}:${sizeKey}`) ?? data
4396
+ },
4397
+ fallbackFileName: `${stem}-fallback.png`
4398
+ } : {}
4094
4399
  };
4095
4400
  });
4096
4401
  }
4097
4402
  return resources;
4098
4403
  }
4404
+ function placementKey(placement) {
4405
+ return `${emuToPixels(placement.widthEmu)}x${emuToPixels(placement.heightEmu)}`;
4406
+ }
4099
4407
  function distinguishImageBytes(data, mediaType, placement) {
4100
4408
  const marker = Buffer.from(`json-to-office:${placement}`, "utf8");
4101
4409
  switch (mediaType) {
@@ -4194,12 +4502,27 @@ function crc32(data) {
4194
4502
  }
4195
4503
  function collectImagePlacements2(ir) {
4196
4504
  const placements = /* @__PURE__ */ new Map();
4505
+ const record = (resourceId, placement) => {
4506
+ const sizes = placements.get(resourceId) ?? /* @__PURE__ */ new Set();
4507
+ sizes.add(placementKey(placement));
4508
+ placements.set(resourceId, sizes);
4509
+ };
4197
4510
  const visitInline = (inline) => {
4198
4511
  if (inline.kind === "image") {
4199
- const key = `${emuToPixels(inline.widthEmu)}x${emuToPixels(inline.heightEmu)}`;
4200
- const sizes = placements.get(inline.resourceId) ?? /* @__PURE__ */ new Set();
4201
- sizes.add(key);
4202
- placements.set(inline.resourceId, sizes);
4512
+ record(inline.resourceId, inline);
4513
+ return;
4514
+ }
4515
+ if (inline.kind === "drawingGroup") {
4516
+ for (const child of inline.children) {
4517
+ if (child.kind === "picture") {
4518
+ record(child.resourceId, {
4519
+ widthEmu: child.frame.widthEmu,
4520
+ heightEmu: child.frame.heightEmu
4521
+ });
4522
+ continue;
4523
+ }
4524
+ child.text?.paragraphs.forEach(visitBlock);
4525
+ }
4203
4526
  return;
4204
4527
  }
4205
4528
  if (inline.kind === "hyperlink" || inline.kind === "revision") {
@@ -4249,6 +4572,7 @@ var init_office_open = __esm({
4249
4572
  "src/renderers/office-open/index.ts"() {
4250
4573
  "use strict";
4251
4574
  init_features();
4575
+ init_chartParts();
4252
4576
  init_imageUtils();
4253
4577
  init_packageDocument();
4254
4578
  init_emit2();
@@ -4372,6 +4696,9 @@ import {
4372
4696
  DEFAULT_VISUAL_DPI,
4373
4697
  MAX_RASTERIZE_BATCH_SLIDES
4374
4698
  } from "@json-to-office/shared";
4699
+ import {
4700
+ isNativeVisualProps
4701
+ } from "@json-to-office/shared-docx";
4375
4702
 
4376
4703
  // src/components/visual.ts
4377
4704
  import { createHash } from "crypto";
@@ -4584,7 +4911,9 @@ function collectVisualProps(root) {
4584
4911
  const obj = node;
4585
4912
  if (typeof obj.name === "string" && obj.enabled === false) return;
4586
4913
  if (obj.name === "visual" && obj.props && typeof obj.props === "object") {
4587
- found.push(obj.props);
4914
+ if (!isNativeVisualProps(obj.props)) {
4915
+ found.push(obj.props);
4916
+ }
4588
4917
  return;
4589
4918
  }
4590
4919
  for (const value of Object.values(obj)) visit(value);
@@ -4864,6 +5193,9 @@ import {
4864
5193
  clampVisualDpi as clampVisualDpi2,
4865
5194
  DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI2
4866
5195
  } from "@json-to-office/shared";
5196
+ import {
5197
+ isNativeVisualProps as isNativeVisualProps2
5198
+ } from "@json-to-office/shared-docx";
4867
5199
 
4868
5200
  // src/components/highcharts.ts
4869
5201
  init_colorUtils();
@@ -5025,6 +5357,7 @@ async function desugarExternals(document, options) {
5025
5357
  return transformComponents(document, async (node) => {
5026
5358
  if (node.enabled === false) return void 0;
5027
5359
  if (node.name === "visual") {
5360
+ if (isNativeVisualProps2(node.props)) return void 0;
5028
5361
  return withNodeIdentity(node, {
5029
5362
  name: "image",
5030
5363
  props: await visualImageProps(
@@ -5077,6 +5410,9 @@ async function visualImageProps(props, prerastered, options) {
5077
5410
 
5078
5411
  // src/core/imageResources.ts
5079
5412
  init_imageUtils();
5413
+ import {
5414
+ isNativeVisualProps as isNativeVisualProps3
5415
+ } from "@json-to-office/shared-docx";
5080
5416
  async function loadImageResources(components) {
5081
5417
  const sources = /* @__PURE__ */ new Set();
5082
5418
  for (const component of components) collectSources(component, sources);
@@ -5115,6 +5451,9 @@ function collectSources(component, out) {
5115
5451
  const children = component.children;
5116
5452
  for (const child of children ?? []) collectSources(child, out);
5117
5453
  const props = component.props ?? {};
5454
+ if (component.name === "visual" && isNativeVisualProps3(props)) {
5455
+ collectNativeVisualSources(props, out);
5456
+ }
5118
5457
  for (const key of ["header", "footer"]) {
5119
5458
  const part = props[key];
5120
5459
  if (Array.isArray(part)) {
@@ -5136,6 +5475,23 @@ function collectSources(component, out) {
5136
5475
  }
5137
5476
  }
5138
5477
  }
5478
+ function collectNativeVisualSources(props, out) {
5479
+ const canvas = props.canvas;
5480
+ const background = canvas?.background?.image;
5481
+ if (background) {
5482
+ const source = resolveImageSource(background);
5483
+ if (source) out.add(source);
5484
+ }
5485
+ const elements = props.elements;
5486
+ if (!Array.isArray(elements)) return;
5487
+ for (const element of elements) {
5488
+ if (!element || element.name !== "image") continue;
5489
+ const source = resolveImageSource(
5490
+ element.props ?? {}
5491
+ );
5492
+ if (source) out.add(source);
5493
+ }
5494
+ }
5139
5495
  function collectCellSource(content, out) {
5140
5496
  if (content && typeof content === "object") {
5141
5497
  collectSources(content, out);
@@ -5147,7 +5503,520 @@ init_colorUtils();
5147
5503
  import {
5148
5504
  FeatureRequirementCollector
5149
5505
  } from "@json-to-office/shared/rendering";
5150
- import { synthesizeFamilyName } from "@json-to-office/shared";
5506
+ import {
5507
+ synthesizeFamilyName,
5508
+ DEFAULT_CHART_THEME_COLORS as DEFAULT_CHART_THEME_COLORS2
5509
+ } from "@json-to-office/shared";
5510
+ import {
5511
+ isNativeVisualProps as isNativeVisualProps4
5512
+ } from "@json-to-office/shared-docx";
5513
+
5514
+ // src/ir/nativeVisual.ts
5515
+ init_units();
5516
+ init_imageUtils();
5517
+ var PIXELS_PER_INCH2 = 96;
5518
+ var DEFAULT_WIDTH_FRACTION = 0.75;
5519
+ var ASSUMED_FONT_SIZE_POINTS = 18;
5520
+ var MIN_DERIVED_TEXT_HEIGHT_INCHES = 0.5;
5521
+ var DERIVED_LINE_HEIGHT_RATIO = 1.6;
5522
+ var GEOMETRY_ALIASES = {
5523
+ arrow: "rightArrow",
5524
+ lightning: "lightningBolt"
5525
+ };
5526
+ var DASH_VALUES = /* @__PURE__ */ new Set([
5527
+ "solid",
5528
+ "dash",
5529
+ "dot",
5530
+ "dashDot"
5531
+ ]);
5532
+ var ANCHOR = {
5533
+ top: "top",
5534
+ middle: "middle",
5535
+ bottom: "bottom"
5536
+ };
5537
+ var ALIGNMENT = {
5538
+ left: "left",
5539
+ center: "center",
5540
+ right: "right",
5541
+ justify: "justified"
5542
+ };
5543
+ function compileNativeVisualGroup(props, outer) {
5544
+ const canvas = {
5545
+ widthEmu: inchesToEmu(props.canvas.width),
5546
+ heightEmu: inchesToEmu(props.canvas.height)
5547
+ };
5548
+ const children = [];
5549
+ let refused = false;
5550
+ const deps = {
5551
+ ...outer,
5552
+ reject: (detail) => {
5553
+ refused = true;
5554
+ outer.reject(detail);
5555
+ }
5556
+ };
5557
+ const background = props.canvas.background;
5558
+ if (background?.color) {
5559
+ const color = deps.color(background.color);
5560
+ if (!color) {
5561
+ deps.reject(`canvas.background.color "${background.color}"`);
5562
+ } else {
5563
+ children.push({
5564
+ kind: "shape",
5565
+ frame: fullBleed(canvas),
5566
+ geometry: "rect",
5567
+ fill: { kind: "solid", color },
5568
+ name: "Canvas background"
5569
+ });
5570
+ }
5571
+ }
5572
+ if (background?.image) {
5573
+ const source = resolveImageSource(background.image);
5574
+ const resource = source ? deps.picture(source) : void 0;
5575
+ if (!resource) {
5576
+ deps.reject("canvas.background.image could not be loaded");
5577
+ } else {
5578
+ children.push({
5579
+ kind: "picture",
5580
+ frame: fullBleed(canvas),
5581
+ resourceId: resource.resourceId,
5582
+ name: "Canvas background"
5583
+ });
5584
+ }
5585
+ }
5586
+ for (const [index, element] of (props.elements ?? []).entries()) {
5587
+ if (element.enabled === false) continue;
5588
+ const child = compileElement(element, index, canvas, deps);
5589
+ if (child) children.push(child);
5590
+ }
5591
+ return refused ? void 0 : { canvas, children };
5592
+ }
5593
+ function compileElement(element, index, canvas, deps) {
5594
+ const at = `elements[${index}]`;
5595
+ switch (element.name) {
5596
+ case "text":
5597
+ return compileText(element.props, at, canvas, deps);
5598
+ case "shape":
5599
+ return compileShape(element.props, at, canvas, deps);
5600
+ case "image":
5601
+ return compileImage(element.props, at, canvas, deps);
5602
+ default:
5603
+ deps.reject(`${at} is a "${element.name}"`);
5604
+ return void 0;
5605
+ }
5606
+ }
5607
+ function compileText(props, at, canvas, deps) {
5608
+ const runs = textRuns(props);
5609
+ if (!runs) {
5610
+ deps.warn(
5611
+ `[core-docx] native visual ${at} has neither "text" nor "runs"; nothing was drawn for it.`
5612
+ );
5613
+ return void 0;
5614
+ }
5615
+ const colors = resolveColors(
5616
+ [
5617
+ ["color", props.color],
5618
+ ["fill.color", props.fill?.color],
5619
+ ...runs.map((run, i) => [`runs[${i}].color`, run.color]),
5620
+ ...runs.map(
5621
+ (run, i) => [
5622
+ `runs[${i}].underline.color`,
5623
+ underlineColor(run.underline)
5624
+ ]
5625
+ ),
5626
+ ["underline.color", underlineColor(props.underline)]
5627
+ ],
5628
+ at,
5629
+ deps
5630
+ );
5631
+ if (!colors) return void 0;
5632
+ const frame = resolveFrame(props, canvas, {
5633
+ defaultHeightInches: derivedTextHeightInches(props, runs)
5634
+ });
5635
+ const paragraphs = textParagraphs(runs, props, colors, at);
5636
+ return {
5637
+ kind: "shape",
5638
+ frame,
5639
+ geometry: "rect",
5640
+ isTextBox: true,
5641
+ // A text box with no fill is transparent, which is what the rasterized
5642
+ // path draws; saying so explicitly stops Word applying its own default.
5643
+ fill: props.fill?.color ? solidFill(colors.get("fill.color"), props.fill.transparency) : { kind: "none" },
5644
+ text: {
5645
+ paragraphs,
5646
+ // pptx anchors a text box at the top unless told otherwise, and a
5647
+ // drawing that changes anchor between render modes moves its text.
5648
+ anchor: ANCHOR[props.valign ?? "top"] ?? "top",
5649
+ // A text box's insets default to nothing, again matching pptx. A shape
5650
+ // deliberately leaves them unstated so OOXML's own defaults apply.
5651
+ insetsEmu: marginInsets(props.margin) ?? ZERO_INSETS
5652
+ },
5653
+ name: `Text ${at}`
5654
+ };
5655
+ }
5656
+ var ZERO_INSETS = { top: 0, bottom: 0, left: 0, right: 0 };
5657
+ function textRuns(props) {
5658
+ if (props.runs?.length) return props.runs;
5659
+ if (props.text === void 0) return void 0;
5660
+ return [{ text: props.text }];
5661
+ }
5662
+ function textParagraphs(runs, props, colors, at) {
5663
+ const paragraphs = [];
5664
+ let current = [];
5665
+ const flush = () => {
5666
+ paragraphs.push({
5667
+ kind: "paragraph",
5668
+ id: `${at}.p${paragraphs.length}`,
5669
+ path: `${at}.paragraphs[${paragraphs.length}]`,
5670
+ children: current,
5671
+ formatting: {
5672
+ ...props.align ? { alignment: ALIGNMENT[props.align] } : {},
5673
+ // A drawing's text is set flush against the shape; the document's
5674
+ // paragraph spacing would push it away from the top edge, which is not
5675
+ // what an absolutely-placed box means.
5676
+ spacing: { beforeTwips: 0, afterTwips: 0 }
5677
+ }
5678
+ });
5679
+ current = [];
5680
+ };
5681
+ runs.forEach((run, index) => {
5682
+ const formatting = runFormatting(run, props, colors, index);
5683
+ const lines = run.text.split("\n");
5684
+ lines.forEach((line, lineIndex) => {
5685
+ if (lineIndex > 0) current.push({ kind: "lineBreak" });
5686
+ if (line) {
5687
+ current.push({ kind: "text", text: line, ...formatting });
5688
+ }
5689
+ });
5690
+ if (run.breakLine && index < runs.length - 1) flush();
5691
+ });
5692
+ flush();
5693
+ return paragraphs;
5694
+ }
5695
+ function runFormatting(run, props, colors, index) {
5696
+ const fontFamily = run.fontFace ?? props.fontFace;
5697
+ const sizePoints = run.fontSize ?? props.fontSize;
5698
+ const color = colors.get(`runs[${index}].color`) ?? (run.color === void 0 ? colors.get("color") : void 0);
5699
+ const bold = run.bold ?? props.bold;
5700
+ const italic = run.italic ?? props.italic;
5701
+ const strike = run.strike ?? props.strike;
5702
+ const underline = compileUnderline(
5703
+ run.underline ?? props.underline,
5704
+ colors.get(
5705
+ run.underline !== void 0 ? `runs[${index}].underline.color` : "underline.color"
5706
+ )
5707
+ );
5708
+ const formatting = {
5709
+ ...fontFamily ? { fontFamily } : {},
5710
+ ...sizePoints !== void 0 ? { sizeHalfPoints: pointsToHalfPoints(sizePoints) } : {},
5711
+ ...color ? { color } : {},
5712
+ ...bold !== void 0 ? { bold } : {},
5713
+ ...italic !== void 0 ? { italic } : {},
5714
+ ...strike !== void 0 ? { strike } : {},
5715
+ ...underline ? { underline } : {}
5716
+ };
5717
+ return Object.keys(formatting).length > 0 ? { formatting } : {};
5718
+ }
5719
+ var UNDERLINE_TYPES = {
5720
+ sng: "single",
5721
+ dbl: "double",
5722
+ dash: "dash",
5723
+ dotted: "dotted"
5724
+ };
5725
+ function compileUnderline(value, color) {
5726
+ if (value === void 0 || value === false) return void 0;
5727
+ const style = value === true ? "sng" : value.style ?? "sng";
5728
+ return {
5729
+ type: UNDERLINE_TYPES[style] ?? "single",
5730
+ ...color ? { color } : {}
5731
+ };
5732
+ }
5733
+ function underlineColor(value) {
5734
+ return typeof value === "object" ? value.color : void 0;
5735
+ }
5736
+ function derivedTextHeightInches(props, runs) {
5737
+ const fontSize = props.fontSize ?? ASSUMED_FONT_SIZE_POINTS;
5738
+ const lineCount = runs.reduce(
5739
+ (total, run) => total + (run.breakLine ? 1 : 0) + (run.text.match(/\n/g)?.length ?? 0),
5740
+ 1
5741
+ );
5742
+ return Math.max(
5743
+ MIN_DERIVED_TEXT_HEIGHT_INCHES,
5744
+ fontSize / 72 * DERIVED_LINE_HEIGHT_RATIO * lineCount
5745
+ );
5746
+ }
5747
+ function compileShape(props, at, canvas, deps) {
5748
+ const segments = shapeSegments(props);
5749
+ const colors = resolveColors(
5750
+ [
5751
+ ["fill.color", props.fill?.color],
5752
+ ["line.color", props.line?.color],
5753
+ ["fontColor", props.fontColor],
5754
+ ...segments.map(
5755
+ (segment, i) => [`text[${i}].color`, segment.color]
5756
+ )
5757
+ ],
5758
+ at,
5759
+ deps
5760
+ );
5761
+ if (!colors) return void 0;
5762
+ const frame = resolveFrame(props, canvas, { defaultHeightInches: 0 });
5763
+ if (props.flipH) frame.flipHorizontal = true;
5764
+ if (props.flipV) frame.flipVertical = true;
5765
+ const dash = props.line?.dashType;
5766
+ if (dash !== void 0 && !DASH_VALUES.has(dash)) {
5767
+ deps.reject(`${at}.line.dashType "${dash}"`);
5768
+ return void 0;
5769
+ }
5770
+ const outline = props.line ? {
5771
+ ...colors.get("line.color") ? { color: colors.get("line.color") } : {},
5772
+ ...props.line.width !== void 0 ? { widthEmu: pointsToEmu(props.line.width) } : {},
5773
+ ...dash && dash !== "solid" ? { dash } : {}
5774
+ } : void 0;
5775
+ const text = segments.length ? {
5776
+ paragraphs: shapeParagraphs(segments, props, colors, at),
5777
+ anchor: ANCHOR[props.valign ?? "middle"] ?? "middle",
5778
+ // Unstated insets are left unstated so OOXML's own shape defaults
5779
+ // apply, which is what the raster path draws.
5780
+ ...marginInsets(props.margin) ? { insetsEmu: marginInsets(props.margin) } : {}
5781
+ } : void 0;
5782
+ return {
5783
+ kind: "shape",
5784
+ frame,
5785
+ geometry: GEOMETRY_ALIASES[props.type] ?? props.type,
5786
+ ...props.fill ? {
5787
+ fill: props.fill.color ? solidFill(colors.get("fill.color"), props.fill.transparency) : { kind: "none" }
5788
+ } : {},
5789
+ ...outline && Object.keys(outline).length > 0 ? { outline } : {},
5790
+ ...text ? { text } : {},
5791
+ name: `Shape ${at}`
5792
+ };
5793
+ }
5794
+ function shapeSegments(props) {
5795
+ if (props.text === void 0) return [];
5796
+ if (typeof props.text === "string") {
5797
+ return props.text ? [{ text: props.text }] : [];
5798
+ }
5799
+ return props.text;
5800
+ }
5801
+ function shapeParagraphs(segments, props, colors, at) {
5802
+ const paragraphs = [];
5803
+ let current = [];
5804
+ const flush = () => {
5805
+ paragraphs.push({
5806
+ kind: "paragraph",
5807
+ id: `${at}.p${paragraphs.length}`,
5808
+ path: `${at}.paragraphs[${paragraphs.length}]`,
5809
+ children: current,
5810
+ formatting: {
5811
+ // A shape centres its text unless told otherwise, which is what the
5812
+ // rasterized shape draws.
5813
+ alignment: props.align ? ALIGNMENT[props.align] : "center",
5814
+ spacing: { beforeTwips: 0, afterTwips: 0 }
5815
+ }
5816
+ });
5817
+ current = [];
5818
+ };
5819
+ segments.forEach((segment, index) => {
5820
+ const fontFamily = segment.fontFace ?? props.fontFace;
5821
+ const sizePoints = segment.fontSize ?? props.fontSize;
5822
+ const color = colors.get(`text[${index}].color`) ?? (segment.color === void 0 ? colors.get("fontColor") : void 0);
5823
+ const bold = segment.bold ?? props.bold;
5824
+ const italic = segment.italic ?? props.italic;
5825
+ const formatting = {
5826
+ ...fontFamily ? { fontFamily } : {},
5827
+ ...sizePoints !== void 0 ? { sizeHalfPoints: pointsToHalfPoints(sizePoints) } : {},
5828
+ ...color ? { color } : {},
5829
+ ...bold !== void 0 ? { bold } : {},
5830
+ ...italic !== void 0 ? { italic } : {}
5831
+ };
5832
+ const wrapped = Object.keys(formatting).length > 0 ? { formatting } : void 0;
5833
+ segment.text.split("\n").forEach((line, lineIndex) => {
5834
+ if (lineIndex > 0) current.push({ kind: "lineBreak" });
5835
+ if (line) current.push({ kind: "text", text: line, ...wrapped });
5836
+ });
5837
+ if (segment.breakLine && index < segments.length - 1) flush();
5838
+ });
5839
+ flush();
5840
+ return paragraphs;
5841
+ }
5842
+ function compileImage(props, at, canvas, deps) {
5843
+ const source = resolveImageSource(props);
5844
+ if (!source) {
5845
+ deps.reject(`${at} has none of "path", "base64" or "svg"`);
5846
+ return void 0;
5847
+ }
5848
+ const resource = deps.picture(source);
5849
+ if (!resource) {
5850
+ deps.reject(`${at} image could not be loaded`);
5851
+ return void 0;
5852
+ }
5853
+ const aspect = resource.intrinsic ? resource.intrinsic.width / resource.intrinsic.height : void 0;
5854
+ const intrinsicInches = resource.intrinsic ? {
5855
+ width: resource.intrinsic.width / PIXELS_PER_INCH2,
5856
+ height: resource.intrinsic.height / PIXELS_PER_INCH2
5857
+ } : void 0;
5858
+ const box = resolveImageBox(props, canvas, aspect, intrinsicInches);
5859
+ const sizing = props.sizing;
5860
+ if (!sizing || !aspect) {
5861
+ if (sizing && !aspect) {
5862
+ deps.warn(
5863
+ `[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.`
5864
+ );
5865
+ }
5866
+ return picture(box, props, resource, at);
5867
+ }
5868
+ const boxAspect = box.widthEmu / box.heightEmu;
5869
+ if (sizing.type === "contain") {
5870
+ const fitted = boxAspect > aspect ? {
5871
+ widthEmu: Math.round(box.heightEmu * aspect),
5872
+ heightEmu: box.heightEmu
5873
+ } : {
5874
+ widthEmu: box.widthEmu,
5875
+ heightEmu: Math.round(box.widthEmu / aspect)
5876
+ };
5877
+ return picture(
5878
+ {
5879
+ ...box,
5880
+ xEmu: box.xEmu + Math.round((box.widthEmu - fitted.widthEmu) / 2),
5881
+ yEmu: box.yEmu + Math.round((box.heightEmu - fitted.heightEmu) / 2),
5882
+ ...fitted
5883
+ },
5884
+ props,
5885
+ resource,
5886
+ at
5887
+ );
5888
+ }
5889
+ const visible = boxAspect > aspect ? { widthFraction: 1, heightFraction: aspect / boxAspect } : { widthFraction: boxAspect / aspect, heightFraction: 1 };
5890
+ const crop = sizing.type === "cover" ? {
5891
+ ...visible.widthFraction < 1 ? {
5892
+ left: (1 - visible.widthFraction) / 2,
5893
+ right: (1 - visible.widthFraction) / 2
5894
+ } : {},
5895
+ ...visible.heightFraction < 1 ? {
5896
+ top: (1 - visible.heightFraction) / 2,
5897
+ bottom: (1 - visible.heightFraction) / 2
5898
+ } : {}
5899
+ } : {
5900
+ ...visible.widthFraction < 1 ? { right: 1 - visible.widthFraction } : {},
5901
+ ...visible.heightFraction < 1 ? { bottom: 1 - visible.heightFraction } : {}
5902
+ };
5903
+ return picture(box, props, resource, at, crop);
5904
+ }
5905
+ function picture(frame, props, resource, at, crop) {
5906
+ return {
5907
+ kind: "picture",
5908
+ frame,
5909
+ resourceId: resource.resourceId,
5910
+ ...props.alt ? { altText: props.alt } : {},
5911
+ ...crop && Object.keys(crop).length > 0 ? { crop } : {},
5912
+ name: `Image ${at}`
5913
+ };
5914
+ }
5915
+ function resolveImageBox(props, canvas, aspect, intrinsicInches) {
5916
+ const width = props.sizing?.w ?? props.w;
5917
+ const height = props.sizing?.h ?? props.h;
5918
+ const widthEmu = width !== void 0 ? resolveExtent(width, canvas.widthEmu) : void 0;
5919
+ const heightEmu = height !== void 0 ? resolveExtent(height, canvas.heightEmu) : void 0;
5920
+ const resolved = (() => {
5921
+ if (widthEmu !== void 0 && heightEmu !== void 0) {
5922
+ return { widthEmu, heightEmu };
5923
+ }
5924
+ if (widthEmu !== void 0) {
5925
+ return {
5926
+ widthEmu,
5927
+ heightEmu: aspect ? Math.round(widthEmu / aspect) : Math.round(widthEmu * (3 / 4))
5928
+ };
5929
+ }
5930
+ if (heightEmu !== void 0) {
5931
+ return {
5932
+ widthEmu: aspect ? Math.round(heightEmu * aspect) : Math.round(heightEmu * (4 / 3)),
5933
+ heightEmu
5934
+ };
5935
+ }
5936
+ if (intrinsicInches) {
5937
+ return {
5938
+ widthEmu: inchesToEmu(intrinsicInches.width),
5939
+ heightEmu: inchesToEmu(intrinsicInches.height)
5940
+ };
5941
+ }
5942
+ const fallbackWidth = Math.round(canvas.widthEmu * DEFAULT_WIDTH_FRACTION);
5943
+ return {
5944
+ widthEmu: fallbackWidth,
5945
+ heightEmu: Math.round(fallbackWidth * (3 / 4))
5946
+ };
5947
+ })();
5948
+ return {
5949
+ xEmu: props.x !== void 0 ? resolveOffset(props.x, canvas.widthEmu) : 0,
5950
+ yEmu: props.y !== void 0 ? resolveOffset(props.y, canvas.heightEmu) : 0,
5951
+ ...resolved,
5952
+ ...rotation(props.rotate)
5953
+ };
5954
+ }
5955
+ function fullBleed(canvas) {
5956
+ return {
5957
+ xEmu: 0,
5958
+ yEmu: 0,
5959
+ widthEmu: canvas.widthEmu,
5960
+ heightEmu: canvas.heightEmu
5961
+ };
5962
+ }
5963
+ function resolveFrame(props, canvas, options) {
5964
+ return {
5965
+ xEmu: props.x !== void 0 ? resolveOffset(props.x, canvas.widthEmu) : 0,
5966
+ yEmu: props.y !== void 0 ? resolveOffset(props.y, canvas.heightEmu) : 0,
5967
+ widthEmu: props.w !== void 0 ? resolveExtent(props.w, canvas.widthEmu) : Math.round(canvas.widthEmu * DEFAULT_WIDTH_FRACTION),
5968
+ heightEmu: props.h !== void 0 ? resolveExtent(props.h, canvas.heightEmu) : inchesToEmu(options.defaultHeightInches),
5969
+ ...rotation(props.rotate)
5970
+ };
5971
+ }
5972
+ function rotation(degrees) {
5973
+ if (degrees === void 0 || degrees % 360 === 0) return {};
5974
+ return { rotationDegrees: degrees };
5975
+ }
5976
+ function resolveOffset(value, axisEmu) {
5977
+ return resolveLength(value, axisEmu);
5978
+ }
5979
+ function resolveExtent(value, axisEmu) {
5980
+ return Math.max(0, resolveLength(value, axisEmu));
5981
+ }
5982
+ function resolveLength(value, axisEmu) {
5983
+ if (typeof value === "number") return inchesToEmu(value);
5984
+ const percent = Number.parseFloat(value);
5985
+ if (!Number.isFinite(percent)) return 0;
5986
+ return Math.round(percent / 100 * axisEmu);
5987
+ }
5988
+ function marginInsets(margin) {
5989
+ if (margin === void 0) return void 0;
5990
+ const [top, right, bottom, left] = Array.isArray(margin) ? margin : [margin, margin, margin, margin];
5991
+ return {
5992
+ top: pointsToEmu(top ?? 0),
5993
+ right: pointsToEmu(right ?? 0),
5994
+ bottom: pointsToEmu(bottom ?? 0),
5995
+ left: pointsToEmu(left ?? 0)
5996
+ };
5997
+ }
5998
+ function solidFill(color, transparency) {
5999
+ return {
6000
+ kind: "solid",
6001
+ color,
6002
+ ...transparency !== void 0 && transparency > 0 ? { transparencyPercent: transparency } : {}
6003
+ };
6004
+ }
6005
+ function resolveColors(entries, at, deps) {
6006
+ const resolved = /* @__PURE__ */ new Map();
6007
+ let failed = false;
6008
+ for (const [key, value] of entries) {
6009
+ if (value === void 0) continue;
6010
+ const color = deps.color(value);
6011
+ if (!color) {
6012
+ deps.reject(`${at}.${key} "${value}"`);
6013
+ failed = true;
6014
+ continue;
6015
+ }
6016
+ resolved.set(key, color);
6017
+ }
6018
+ return failed ? void 0 : resolved;
6019
+ }
5151
6020
 
5152
6021
  // src/styles/themeToStyles.ts
5153
6022
  init_themes();
@@ -6990,7 +7859,7 @@ function decoratedStyle(match, base) {
6990
7859
  function pushSegment(out, text, options, override = {}) {
6991
7860
  const bold = override.bold ?? options.base.bold;
6992
7861
  const italic = override.italic ?? options.base.italic;
6993
- const formatting = runFormatting(options, { bold, italic });
7862
+ const formatting = runFormatting2(options, { bold, italic });
6994
7863
  const lines = text.split("\n");
6995
7864
  for (const [lineIndex, line] of lines.entries()) {
6996
7865
  if (!line && lineIndex === 0) continue;
@@ -7068,7 +7937,7 @@ function pushNoProofWords(out, text, formatting, options) {
7068
7937
  out.push({ kind: "text", text: text.slice(lastIndex), formatting });
7069
7938
  }
7070
7939
  }
7071
- function runFormatting(options, state) {
7940
+ function runFormatting2(options, state) {
7072
7941
  const formatting = { ...options.base };
7073
7942
  if (state.bold !== void 0) formatting.bold = state.bold;
7074
7943
  if (state.italic !== void 0) formatting.italic = state.italic;
@@ -7440,7 +8309,11 @@ function compileComponent(component, scope) {
7440
8309
  case "columns":
7441
8310
  return compileColumns(component, scope);
7442
8311
  case "image":
7443
- return compileImage(component, scope);
8312
+ return compileImage2(component, scope);
8313
+ case "visual":
8314
+ return compileVisual(component, scope);
8315
+ case "chart":
8316
+ return compileChart(component, scope);
7444
8317
  case "table":
7445
8318
  return compileTable(component, scope);
7446
8319
  default:
@@ -7684,7 +8557,7 @@ function compileTextBox(component, scope) {
7684
8557
  const style = props.style;
7685
8558
  const padding = style?.padding;
7686
8559
  if (props.renderAs === "shape") {
7687
- const shape = compileShape(props, children, scope);
8560
+ const shape = compileShape2(props, children, scope);
7688
8561
  if (shape) return shape;
7689
8562
  }
7690
8563
  ctx.features.require("tables", path4);
@@ -7738,7 +8611,7 @@ function compileTextBox(component, scope) {
7738
8611
  ];
7739
8612
  }
7740
8613
  var EMU_PER_PIXEL = 9525;
7741
- function compileShape(props, children, scope) {
8614
+ function compileShape2(props, children, scope) {
7742
8615
  const { ctx, path: path4 } = scope;
7743
8616
  const width = shapeSize(props.width, "width", ctx);
7744
8617
  const height = shapeSize(props.height, "height", ctx);
@@ -8232,7 +9105,7 @@ function reportUnlowered(component, props, text, scope) {
8232
9105
  }
8233
9106
  function compileRuns(props, text, ctx, path4, mode = "inline") {
8234
9107
  const font = props.font ?? {};
8235
- const base = runFormatting2(font, props, ctx);
9108
+ const base = runFormatting3(font, props, ctx);
8236
9109
  if (base.language) ctx.features.require("proofing-language", path4);
8237
9110
  if (base.noProof) ctx.features.require("proofing-language", path4);
8238
9111
  const words = mergeNoProofWords(noProofWords(ctx.theme), props.noProofWords);
@@ -8502,7 +9375,7 @@ function isoDate(date) {
8502
9375
  function isoDateTime(date) {
8503
9376
  return date.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
8504
9377
  }
8505
- function runFormatting2(font, props, ctx) {
9378
+ function runFormatting3(font, props, ctx) {
8506
9379
  const hasWeightRequest = font.fontWeight != null || font.bold === true;
8507
9380
  const effectiveFamily = font.family ?? (hasWeightRequest ? resolveBodyFamily(ctx.theme) : void 0);
8508
9381
  const weighted = applyFontWeightAlias({
@@ -8665,8 +9538,107 @@ function headingLevel(level) {
8665
9538
  const value = typeof level === "number" ? level : 1;
8666
9539
  return value >= 1 && value <= 6 ? value : 1;
8667
9540
  }
9541
+ function compileVisual(component, scope) {
9542
+ const { ctx, path: path4 } = scope;
9543
+ const props = component.props ?? {};
9544
+ if (!isNativeVisualProps4(props)) {
9545
+ ctx.unsupported.push({
9546
+ name: "visual",
9547
+ path: path4,
9548
+ detail: "a raster visual reached the compiler; it should have been rasterized during desugaring"
9549
+ });
9550
+ return [];
9551
+ }
9552
+ const group = compileNativeVisualGroup(
9553
+ props,
9554
+ nativeVisualDeps(ctx, path4)
9555
+ );
9556
+ if (!group) return [];
9557
+ const size = visualPlacementPixels(props, group.canvas, {
9558
+ widthPx: Math.round(twipsToPixels(getAvailableWidthTwips(ctx.theme))),
9559
+ heightPx: Math.round(twipsToPixels(getAvailableHeightTwips(ctx.theme)))
9560
+ });
9561
+ const drawing = {
9562
+ kind: "drawingGroup",
9563
+ widthEmu: pixelsToEmu(size.width),
9564
+ heightEmu: pixelsToEmu(size.height),
9565
+ canvasWidthEmu: group.canvas.widthEmu,
9566
+ canvasHeightEmu: group.canvas.heightEmu,
9567
+ children: group.children,
9568
+ ...typeof props.alt === "string" && props.alt ? { altText: props.alt } : {},
9569
+ ...props.floating ? { floating: compileFloating(props.floating, ctx, path4) } : {}
9570
+ };
9571
+ ctx.features.require("drawing-groups", path4);
9572
+ if (props.floating) ctx.features.require("floating-images", path4);
9573
+ const spacing = {};
9574
+ if (props.spacing?.before !== void 0) {
9575
+ spacing.beforeTwips = pointsToTwips2(props.spacing.before);
9576
+ }
9577
+ if (props.spacing?.after !== void 0) {
9578
+ spacing.afterTwips = pointsToTwips2(props.spacing.after);
9579
+ }
9580
+ const blocks = [
9581
+ {
9582
+ kind: "paragraph",
9583
+ id: scope.id,
9584
+ path: path4,
9585
+ children: [drawing],
9586
+ formatting: {
9587
+ // A floating drawing is anchored, so aligning its paragraph would move
9588
+ // the anchor rather than the graphic — the same rule an image follows.
9589
+ ...props.floating ? {} : { alignment: compileAlignment(props.alignment) ?? "center" },
9590
+ ...Object.keys(spacing).length > 0 ? { spacing } : {},
9591
+ ...props.keepNext !== void 0 ? { keepNext: props.keepNext } : {},
9592
+ ...props.keepLines !== void 0 ? { keepLines: props.keepLines } : {}
9593
+ }
9594
+ }
9595
+ ];
9596
+ const caption = captionBlock(props.caption, scope, "visual");
9597
+ if (caption === void 0) return [];
9598
+ if (caption) blocks.push(caption);
9599
+ return blocks;
9600
+ }
9601
+ function nativeVisualDeps(ctx, path4) {
9602
+ return {
9603
+ color: (value) => {
9604
+ try {
9605
+ return irColor(resolveColor(value, ctx.theme));
9606
+ } catch {
9607
+ return void 0;
9608
+ }
9609
+ },
9610
+ picture: (source) => {
9611
+ const loaded = ctx.images.get(source);
9612
+ if (!loaded) return void 0;
9613
+ const mediaType = detectImageType(source, loaded.contentType);
9614
+ if (mediaType === "svg") ctx.features.require("svg-images", path4);
9615
+ return {
9616
+ resourceId: declareResource(loaded, mediaType, ctx),
9617
+ mediaType,
9618
+ ...loaded.intrinsic ? { intrinsic: loaded.intrinsic } : {}
9619
+ };
9620
+ },
9621
+ reject: (detail) => ctx.unsupported.push({ name: "visual", path: path4, detail }),
9622
+ warn: (message) => warnOnce(ctx, `visual:${message}`, message)
9623
+ };
9624
+ }
9625
+ function visualPlacementPixels(props, canvas, reference) {
9626
+ const canvasWidthPx = Math.round(emuToPixels(canvas.widthEmu));
9627
+ const canvasHeightPx = Math.round(emuToPixels(canvas.heightEmu));
9628
+ const targetWidth = props.width !== void 0 ? parseWidthValue(props.width, reference.widthPx) : void 0;
9629
+ const targetHeight = props.height !== void 0 ? parseDimensionValue(props.height, reference.heightPx) : void 0;
9630
+ if (targetWidth === void 0 && targetHeight === void 0) {
9631
+ return { width: canvasWidthPx, height: canvasHeightPx };
9632
+ }
9633
+ return calculateMissingDimension(
9634
+ canvasWidthPx,
9635
+ canvasHeightPx,
9636
+ targetWidth,
9637
+ targetHeight
9638
+ );
9639
+ }
8668
9640
  var UNLOWERED_IMAGE_PROPS = ["comment"];
8669
- function compileImage(component, scope) {
9641
+ function compileImage2(component, scope) {
8670
9642
  const { ctx, path: path4 } = scope;
8671
9643
  const props = component.props ?? {};
8672
9644
  for (const prop of UNLOWERED_IMAGE_PROPS) {
@@ -8727,32 +9699,142 @@ function compileImage(component, scope) {
8727
9699
  }
8728
9700
  }
8729
9701
  ];
8730
- if (props.caption) {
8731
- const caption = String(props.caption);
8732
- const syntax = containsUnsupportedSyntax(caption);
8733
- if (syntax) {
8734
- ctx.unsupported.push({ name: "image", path: path4, detail: syntax });
8735
- return [];
9702
+ const caption = captionBlock(props.caption, scope, "image");
9703
+ if (caption === void 0) return [];
9704
+ if (caption) blocks.push(caption);
9705
+ return blocks;
9706
+ }
9707
+ var DEFAULT_CHART_HEIGHT_INCHES = 3;
9708
+ function compileChart(component, scope) {
9709
+ const { ctx, path: path4 } = scope;
9710
+ const props = component.props ?? {};
9711
+ if (props.type === "bubble") {
9712
+ throw new Error(
9713
+ `Chart at ${path4} is a bubble chart, which no docx renderer draws; use the pptx \`chart\` component on the pptxgenjs renderer for one.`
9714
+ );
9715
+ }
9716
+ const rawSeries = Array.isArray(props.data) ? props.data : [];
9717
+ if (rawSeries.length === 0) {
9718
+ throw new Error(`Chart at ${path4} has no data series.`);
9719
+ }
9720
+ const series = rawSeries.map((entry, index) => {
9721
+ const raw = entry ?? {};
9722
+ const label = typeof raw.name === "string" ? raw.name : `series ${index}`;
9723
+ const labels = raw.labels;
9724
+ const values = raw.values;
9725
+ if (!Array.isArray(labels) || !Array.isArray(values)) {
9726
+ throw new Error(
9727
+ `Chart series "${label}" at ${path4} needs both "labels" and "values"; every series carries its own, not just the first.`
9728
+ );
9729
+ }
9730
+ if (labels.length !== values.length) {
9731
+ throw new Error(
9732
+ `Chart series "${label}" at ${path4} has ${labels.length} labels and ${values.length} values; they must be the same length.`
9733
+ );
9734
+ }
9735
+ return {
9736
+ ...typeof raw.name === "string" ? { name: raw.name } : {},
9737
+ labels: labels.map((value) => String(value)),
9738
+ values: values.map((value) => Number(value))
9739
+ };
9740
+ });
9741
+ const categories = series[0].labels;
9742
+ for (const [index, entry] of series.entries()) {
9743
+ if (index === 0) continue;
9744
+ if (entry.labels.length !== categories.length || entry.labels.some((label, i) => label !== categories[i])) {
9745
+ throw new Error(
9746
+ `Chart series "${entry.name ?? `series ${index}`}" at ${path4} 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.`
9747
+ );
8736
9748
  }
8737
- blocks.push({
8738
- kind: "paragraph",
8739
- id: `${scope.id}:caption`,
8740
- path: `${path4}.caption`,
8741
- styleId: "Normal",
8742
- // Captions default to left alignment, whatever the figure did.
8743
- formatting: { alignment: "left" },
8744
- // A caption reaches the parser only when it carries a decorator, which
8745
- // is why a link in an otherwise plain caption stays literal.
8746
- children: DECORATED.test(caption) ? parseInline(caption, {
8747
- base: {},
8748
- hyperlinks: true,
8749
- resolvePlaceholder: placeholderResolver(ctx),
8750
- resolveCrossReference: crossReferenceResolver(ctx, path4)
8751
- }) : parseLiteral(caption, { base: {} })
8752
- });
8753
9749
  }
9750
+ const colors = Array.isArray(props.chartColors) ? props.chartColors.map(
9751
+ (color) => resolveColor(String(color), ctx.theme)
9752
+ ) : DEFAULT_CHART_THEME_COLORS2.map((token) => {
9753
+ const value = ctx.theme?.colors?.[token];
9754
+ if (typeof value !== "string" || value.length === 0) return void 0;
9755
+ try {
9756
+ return resolveColor(token, ctx.theme);
9757
+ } catch {
9758
+ return void 0;
9759
+ }
9760
+ }).filter((color) => color !== void 0);
9761
+ const page = getPageSetup(ctx.theme);
9762
+ const contentWidthInches = (page.size.width - page.margin.left - page.margin.right) / 1440;
9763
+ const widthInches = typeof props.width === "number" ? props.width : contentWidthInches;
9764
+ const heightInches = typeof props.height === "number" ? props.height : DEFAULT_CHART_HEIGHT_INCHES;
9765
+ const chart = {
9766
+ kind: "chart",
9767
+ chartType: props.type,
9768
+ series,
9769
+ colors,
9770
+ widthEmu: inchesToEmu(widthInches),
9771
+ heightEmu: inchesToEmu(heightInches),
9772
+ ...typeof props.title === "string" ? { title: props.title } : {},
9773
+ ...props.showTitle !== void 0 ? { showTitle: props.showTitle } : {},
9774
+ ...props.showLegend !== void 0 ? { showLegend: props.showLegend } : {},
9775
+ ...props.legendPos ? { legendPosition: props.legendPos } : {},
9776
+ ...typeof props.catAxisTitle === "string" ? { categoryAxisTitle: props.catAxisTitle } : {},
9777
+ ...typeof props.valAxisTitle === "string" ? { valueAxisTitle: props.valAxisTitle } : {},
9778
+ ...typeof props.alt === "string" && props.alt ? { altText: props.alt } : {},
9779
+ ...props.floating ? { floating: compileFloating(props.floating, ctx, path4) } : {}
9780
+ };
9781
+ ctx.features.require("charts", path4);
9782
+ if (props.floating) ctx.features.require("floating-images", path4);
9783
+ const spacing = {};
9784
+ if (props.spacing?.before !== void 0) {
9785
+ spacing.beforeTwips = pointsToTwips2(props.spacing.before);
9786
+ }
9787
+ if (props.spacing?.after !== void 0) {
9788
+ spacing.afterTwips = pointsToTwips2(props.spacing.after);
9789
+ }
9790
+ const blocks = [
9791
+ {
9792
+ kind: "paragraph",
9793
+ id: scope.id,
9794
+ path: path4,
9795
+ children: [chart],
9796
+ formatting: {
9797
+ // An anchored chart moves with its anchor, so aligning the paragraph
9798
+ // would move the anchor rather than the chart — the same reasoning as
9799
+ // a floating image.
9800
+ ...props.floating ? {} : { alignment: compileAlignment(props.alignment) ?? "center" },
9801
+ ...Object.keys(spacing).length > 0 ? { spacing } : {},
9802
+ ...props.keepNext !== void 0 ? { keepNext: props.keepNext } : {},
9803
+ ...props.keepLines !== void 0 ? { keepLines: props.keepLines } : {}
9804
+ }
9805
+ }
9806
+ ];
9807
+ const caption = captionBlock(props.caption, scope, "chart");
9808
+ if (caption === void 0) return [];
9809
+ if (caption) blocks.push(caption);
8754
9810
  return blocks;
8755
9811
  }
9812
+ function captionBlock(value, scope, componentName) {
9813
+ if (!value) return null;
9814
+ const { ctx, path: path4 } = scope;
9815
+ const caption = String(value);
9816
+ const syntax = containsUnsupportedSyntax(caption);
9817
+ if (syntax) {
9818
+ ctx.unsupported.push({ name: componentName, path: path4, detail: syntax });
9819
+ return void 0;
9820
+ }
9821
+ return {
9822
+ kind: "paragraph",
9823
+ id: `${scope.id}:caption`,
9824
+ path: `${path4}.caption`,
9825
+ styleId: "Normal",
9826
+ // Captions default to left alignment, whatever the figure did.
9827
+ formatting: { alignment: "left" },
9828
+ // A caption reaches the parser only when it carries a decorator, which
9829
+ // is why a link in an otherwise plain caption stays literal.
9830
+ children: DECORATED.test(caption) ? parseInline(caption, {
9831
+ base: {},
9832
+ hyperlinks: true,
9833
+ resolvePlaceholder: placeholderResolver(ctx),
9834
+ resolveCrossReference: crossReferenceResolver(ctx, path4)
9835
+ }) : parseLiteral(caption, { base: {} })
9836
+ };
9837
+ }
8756
9838
  function relativeFrom(value, axis) {
8757
9839
  const allowed = axis === "horizontal" ? ["character", "column", "margin", "page"] : ["margin", "page", "paragraph", "line"];
8758
9840
  return typeof value === "string" && allowed.includes(value) ? value : void 0;
@@ -9176,6 +10258,9 @@ function cellChildren(cell, baseStyle, scope, rowRevision) {
9176
10258
  if (content.name === "image") {
9177
10259
  return wrap(cellImage(content, base, ctx));
9178
10260
  }
10261
+ if (content.name === "visual") {
10262
+ return wrap(cellVisual(content, scope));
10263
+ }
9179
10264
  if (content.name !== "paragraph") {
9180
10265
  return wrap([
9181
10266
  {
@@ -9215,6 +10300,40 @@ function cellChildren(cell, baseStyle, scope, rowRevision) {
9215
10300
  })
9216
10301
  );
9217
10302
  }
10303
+ function cellVisual(component, scope) {
10304
+ const { ctx, path: path4 } = scope;
10305
+ const props = component.props ?? {};
10306
+ if (!isNativeVisualProps4(props)) {
10307
+ ctx.unsupported.push({
10308
+ name: "visual",
10309
+ path: path4,
10310
+ detail: "a raster visual reached the compiler; it should have been rasterized during desugaring"
10311
+ });
10312
+ return [];
10313
+ }
10314
+ const group = compileNativeVisualGroup(
10315
+ props,
10316
+ nativeVisualDeps(ctx, path4)
10317
+ );
10318
+ if (!group) return [];
10319
+ const size = visualPlacementPixels(props, group.canvas, {
10320
+ widthPx: CELL_DRAWING_REFERENCE.width,
10321
+ heightPx: CELL_DRAWING_REFERENCE.height
10322
+ });
10323
+ ctx.features.require("drawing-groups", path4);
10324
+ return [
10325
+ {
10326
+ kind: "drawingGroup",
10327
+ widthEmu: pixelsToEmu(size.width),
10328
+ heightEmu: pixelsToEmu(size.height),
10329
+ canvasWidthEmu: group.canvas.widthEmu,
10330
+ canvasHeightEmu: group.canvas.heightEmu,
10331
+ children: group.children,
10332
+ ...typeof props.alt === "string" && props.alt ? { altText: props.alt } : {}
10333
+ }
10334
+ ];
10335
+ }
10336
+ var CELL_DRAWING_REFERENCE = { width: 300, height: 200 };
9218
10337
  function cellImage(component, base, ctx) {
9219
10338
  const props = component.props ?? {};
9220
10339
  const source = resolveImageSource(props);
@@ -10330,6 +11449,9 @@ import {
10330
11449
  clampVisualDpi as clampVisualDpi3,
10331
11450
  DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI3
10332
11451
  } from "@json-to-office/shared";
11452
+ import {
11453
+ isNativeVisualProps as isNativeVisualProps5
11454
+ } from "@json-to-office/shared-docx";
10333
11455
  var DEFAULT_CONCURRENCY = 4;
10334
11456
  async function flattenVisuals(doc, options) {
10335
11457
  let rasterize = options.rasterize;
@@ -10375,8 +11497,10 @@ async function flattenVisuals(doc, options) {
10375
11497
  doc,
10376
11498
  async (node) => (
10377
11499
  // A disabled visual is left as-is: it is filtered out at render anyway, so
10378
- // rasterizing it would be wasted work.
10379
- node.name === "visual" && node.enabled !== false ? rasterizeVisual(node, ctx) : void 0
11500
+ // rasterizing it would be wasted work. A native one is left as-is for a
11501
+ // stronger reason it is already service-free, and flattening exists to
11502
+ // remove the service dependency, not to trade editable objects for pixels.
11503
+ node.name === "visual" && node.enabled !== false && !isNativeVisualProps5(node.props) ? rasterizeVisual(node, ctx) : void 0
10380
11504
  )
10381
11505
  );
10382
11506
  }