@json-to-office/core-docx 1.1.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.
package/dist/index.js CHANGED
@@ -2217,6 +2217,10 @@ function inlineChildren(children, resources = /* @__PURE__ */ new Map()) {
2217
2217
  throw new Error(
2218
2218
  "the docxjs renderer has no emitter for a drawing group; this document should have been refused by the capability check"
2219
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
+ );
2220
2224
  case "noteReference":
2221
2225
  out.push(
2222
2226
  child.noteKind === "endnote" ? new EndnoteReferenceRun(child.id) : new FootnoteReferenceRun(child.id)
@@ -2817,6 +2821,17 @@ var init_features = __esm({
2817
2821
  * pictures — a backend can plausibly have the first and not the second.
2818
2822
  */
2819
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",
2820
2835
  /** A table-of-contents field. */
2821
2836
  "toc",
2822
2837
  /** TOC entries baked in so an unrefreshed reader still shows them. */
@@ -3286,7 +3301,11 @@ var init_docxjs = __esm({
3286
3301
  // than a slice boundary: docx.js has no `wpg:wgp`. Leaving it out is what
3287
3302
  // turns a native `visual` sent to this backend into a named capability
3288
3303
  // error instead of a document with the graphic missing.
3289
- "drawing-groups"
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"
3290
3309
  ]);
3291
3310
  DOCXJS_CAPABILITIES = new Set(
3292
3311
  [...ALL_DOCX_FEATURES].filter((feature) => !NOT_YET_EMITTED.has(feature))
@@ -3294,11 +3313,105 @@ var init_docxjs = __esm({
3294
3313
  }
3295
3314
  });
3296
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
+
3297
3410
  // src/renderers/office-open/emit.ts
3298
3411
  import { assertNever as assertNever2 } from "@json-to-office/shared/rendering";
3299
3412
  function emptyContext() {
3300
3413
  let next = 1;
3301
- return { pictures: /* @__PURE__ */ new Map(), nextDrawingId: () => next++ };
3414
+ return { pictures: /* @__PURE__ */ new Map(), nextDrawingId: () => next++, charts: [] };
3302
3415
  }
3303
3416
  function simpleField(instruction, cachedText) {
3304
3417
  return {
@@ -3434,6 +3547,10 @@ function inlineChildren2(children, ctx = emptyContext()) {
3434
3547
  case "shape":
3435
3548
  out.push({ wpsShape: shapeOptions(child, ctx) });
3436
3549
  break;
3550
+ case "chart":
3551
+ ctx.charts?.push(child);
3552
+ out.push({ chart: chartOptions(child, ctx) });
3553
+ break;
3437
3554
  case "noteReference":
3438
3555
  out.push(
3439
3556
  child.noteKind === "endnote" ? { endnoteReference: child.id } : { footnoteReference: child.id }
@@ -3542,6 +3659,25 @@ function drawingGroupOptions(group, ctx) {
3542
3659
  ...group.floating ? { floating: floatingOptions2(group.floating) } : {}
3543
3660
  };
3544
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
+ }
3545
3681
  function groupChild(child, ctx) {
3546
3682
  return child.kind === "shape" ? groupShape(child, ctx) : groupPicture(child, ctx);
3547
3683
  }
@@ -4114,6 +4250,7 @@ __export(office_open_exports, {
4114
4250
  buildDocumentOptions: () => buildDocumentOptions,
4115
4251
  createOfficeOpenDocxRenderer: () => createOfficeOpenDocxRenderer
4116
4252
  });
4253
+ import AdmZip4 from "adm-zip";
4117
4254
  async function createOfficeOpenDocxRenderer() {
4118
4255
  const backend = await import(
4119
4256
  /* @vite-ignore */
@@ -4129,13 +4266,19 @@ async function createOfficeOpenDocxRenderer() {
4129
4266
  format: "docx",
4130
4267
  capabilities: OFFICE_OPEN_CAPABILITIES,
4131
4268
  async render(ir, options) {
4132
- const document = await buildDocumentOptions(ir);
4269
+ const charts = [];
4270
+ const document = await buildDocumentOptions(ir, charts);
4133
4271
  const bytes = await backend.generateDocument(document, {
4134
4272
  type: "uint8array"
4135
4273
  });
4136
- const raw = Buffer.from(
4274
+ let raw = Buffer.from(
4137
4275
  bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
4138
4276
  );
4277
+ if (charts.length > 0) {
4278
+ const zip = new AdmZip4(raw);
4279
+ spliceChartParts(zip, charts);
4280
+ raw = zip.toBuffer();
4281
+ }
4139
4282
  if (options?.deterministic === false) return new Uint8Array(raw);
4140
4283
  return new Uint8Array(
4141
4284
  canonicalizeDocxBuffer(
@@ -4149,11 +4292,12 @@ async function createOfficeOpenDocxRenderer() {
4149
4292
  }
4150
4293
  };
4151
4294
  }
4152
- async function buildDocumentOptions(ir) {
4295
+ async function buildDocumentOptions(ir, charts = []) {
4153
4296
  let nextDrawingId = 1;
4154
4297
  const ctx = {
4155
4298
  pictures: await prepareImages2(ir),
4156
- nextDrawingId: () => nextDrawingId++
4299
+ nextDrawingId: () => nextDrawingId++,
4300
+ charts
4157
4301
  };
4158
4302
  return {
4159
4303
  styles: emitStyles2(ir.styles),
@@ -4428,6 +4572,7 @@ var init_office_open = __esm({
4428
4572
  "src/renderers/office-open/index.ts"() {
4429
4573
  "use strict";
4430
4574
  init_features();
4575
+ init_chartParts();
4431
4576
  init_imageUtils();
4432
4577
  init_packageDocument();
4433
4578
  init_emit2();
@@ -5358,7 +5503,10 @@ init_colorUtils();
5358
5503
  import {
5359
5504
  FeatureRequirementCollector
5360
5505
  } from "@json-to-office/shared/rendering";
5361
- 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";
5362
5510
  import {
5363
5511
  isNativeVisualProps as isNativeVisualProps4
5364
5512
  } from "@json-to-office/shared-docx";
@@ -8164,6 +8312,8 @@ function compileComponent(component, scope) {
8164
8312
  return compileImage2(component, scope);
8165
8313
  case "visual":
8166
8314
  return compileVisual(component, scope);
8315
+ case "chart":
8316
+ return compileChart(component, scope);
8167
8317
  case "table":
8168
8318
  return compileTable(component, scope);
8169
8319
  default:
@@ -9554,6 +9704,111 @@ function compileImage2(component, scope) {
9554
9704
  if (caption) blocks.push(caption);
9555
9705
  return blocks;
9556
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
+ );
9748
+ }
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);
9810
+ return blocks;
9811
+ }
9557
9812
  function captionBlock(value, scope, componentName) {
9558
9813
  if (!value) return null;
9559
9814
  const { ctx, path: path4 } = scope;