@json-to-office/core-docx 0.15.0 → 0.16.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.
@@ -2038,7 +2038,8 @@ import {
2038
2038
  isTableComponent,
2039
2039
  isListComponent,
2040
2040
  isTocComponent,
2041
- isHighchartsComponent
2041
+ isHighchartsComponent,
2042
+ isVisualComponent
2042
2043
  } from "@json-to-office/shared-docx";
2043
2044
 
2044
2045
  // src/utils/formatters.ts
@@ -2131,6 +2132,9 @@ function resolveListProps(props, theme) {
2131
2132
  function resolveHighchartsProps(props, _theme) {
2132
2133
  return props;
2133
2134
  }
2135
+ function resolveVisualProps(props, _theme) {
2136
+ return props;
2137
+ }
2134
2138
  function getCustomComponentDefaults(theme, componentName) {
2135
2139
  const defaults = getComponentDefaults(theme);
2136
2140
  return defaults?.[componentName] || {};
@@ -2160,7 +2164,8 @@ var RESOLVER_MAP = {
2160
2164
  section: resolveSectionProps,
2161
2165
  columns: resolveColumnsProps,
2162
2166
  list: resolveListProps,
2163
- highcharts: resolveHighchartsProps
2167
+ highcharts: resolveHighchartsProps,
2168
+ visual: resolveVisualProps
2164
2169
  };
2165
2170
  function resolveComponentDefaults(component, theme) {
2166
2171
  if (!component.props) return component;
@@ -4141,7 +4146,7 @@ function initializeComponentCache(cache) {
4141
4146
  }
4142
4147
  }
4143
4148
  async function renderComponentWithCache(component, theme, themeName, context, bypassCache = false) {
4144
- const forceBypassForType = component.name === "toc" || component.name === "section" || componentHasRevision(component);
4149
+ const forceBypassForType = component.name === "toc" || component.name === "section" || component.name === "visual" || componentHasRevision(component);
4145
4150
  if (!componentCache) {
4146
4151
  initializeComponentCache();
4147
4152
  }
@@ -6393,21 +6398,61 @@ function isNodeEnvironment() {
6393
6398
  return typeof process !== "undefined" && process.versions != null && process.versions.node != null && typeof process.versions.node === "string";
6394
6399
  }
6395
6400
 
6401
+ // src/utils/serviceClient.ts
6402
+ var DEFAULT_TIMEOUT_MS = 3e4;
6403
+ function resolveServiceUrl(propsUrl, servicesUrl, defaultUrl) {
6404
+ const raw = (propsUrl || servicesUrl || defaultUrl).trim();
6405
+ const withScheme = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
6406
+ return withScheme.replace(/\/+$/, "");
6407
+ }
6408
+ async function postJsonToService(opts) {
6409
+ const resolvedHeaders = typeof opts.headers === "function" ? await opts.headers(opts.body) : opts.headers;
6410
+ const headers = {
6411
+ "Content-Type": "application/json",
6412
+ ...resolvedHeaders
6413
+ };
6414
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
6415
+ const controller = new AbortController();
6416
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
6417
+ let response;
6418
+ try {
6419
+ response = await fetch(`${opts.url}${opts.path}`, {
6420
+ method: "POST",
6421
+ headers,
6422
+ body: JSON.stringify(opts.body),
6423
+ signal: controller.signal
6424
+ });
6425
+ } catch (error) {
6426
+ if (error?.name === "AbortError") {
6427
+ throw new Error(
6428
+ `${opts.serviceLabel} timed out after ${timeoutMs}ms at ${opts.url}.`
6429
+ );
6430
+ }
6431
+ const cause = error instanceof Error ? error.message : String(error);
6432
+ throw new Error(opts.onUnreachable(opts.url, cause));
6433
+ } finally {
6434
+ clearTimeout(timer);
6435
+ }
6436
+ if (!response.ok) {
6437
+ throw new Error(
6438
+ `${opts.serviceLabel} returned ${response.status}: ${response.statusText}`
6439
+ );
6440
+ }
6441
+ return response;
6442
+ }
6443
+
6396
6444
  // src/components/highcharts.ts
6397
6445
  var DEFAULT_EXPORT_SERVER_URL = "http://localhost:7801";
6398
- function getExportServerUrl(propsUrl, servicesUrl) {
6399
- const raw = propsUrl || servicesUrl || DEFAULT_EXPORT_SERVER_URL;
6400
- return raw.startsWith("http") ? raw : `http://${raw}`;
6401
- }
6402
6446
  async function generateChart(config, servicesConfig) {
6403
6447
  if (!isNodeEnvironment()) {
6404
6448
  throw new Error(
6405
6449
  "Highcharts export server requires a Node.js environment. Chart generation is not available in browser environments."
6406
6450
  );
6407
6451
  }
6408
- const serverUrl = getExportServerUrl(
6452
+ const serverUrl = resolveServiceUrl(
6409
6453
  config.serverUrl,
6410
- servicesConfig?.serverUrl
6454
+ servicesConfig?.serverUrl,
6455
+ DEFAULT_EXPORT_SERVER_URL
6411
6456
  );
6412
6457
  const requestBody = {
6413
6458
  infile: config.options,
@@ -6415,26 +6460,15 @@ async function generateChart(config, servicesConfig) {
6415
6460
  b64: true,
6416
6461
  scale: config.scale
6417
6462
  };
6418
- const resolvedHeaders = typeof servicesConfig?.headers === "function" ? await servicesConfig.headers(requestBody) : servicesConfig?.headers;
6419
- const headers = {
6420
- "Content-Type": "application/json",
6421
- ...resolvedHeaders
6422
- };
6423
- const response = await fetch(`${serverUrl}/export`, {
6424
- method: "POST",
6425
- headers,
6426
- body: JSON.stringify(requestBody)
6427
- }).catch((error) => {
6428
- throw new Error(
6429
- `Highcharts Export Server is not running at ${serverUrl}. Start it with: npx highcharts-export-server --enableServer true
6430
- Cause: ${error instanceof Error ? error.message : String(error)}`
6431
- );
6463
+ const response = await postJsonToService({
6464
+ url: serverUrl,
6465
+ path: "/export",
6466
+ body: requestBody,
6467
+ headers: servicesConfig?.headers,
6468
+ serviceLabel: "Highcharts export server",
6469
+ onUnreachable: (url, cause) => `Highcharts Export Server is not running at ${url}. Start it with: npx highcharts-export-server --enableServer true
6470
+ Cause: ${cause}`
6432
6471
  });
6433
- if (!response.ok) {
6434
- throw new Error(
6435
- `Highcharts export server returned ${response.status}: ${response.statusText}`
6436
- );
6437
- }
6438
6472
  const base64Data = await response.text();
6439
6473
  const base64DataUri = `data:image/png;base64,${base64Data}`;
6440
6474
  const width = config.options.chart.width;
@@ -6468,6 +6502,109 @@ async function renderHighchartsComponent(component, theme, themeName, context) {
6468
6502
  return imageParagraphs;
6469
6503
  }
6470
6504
 
6505
+ // src/components/visual.ts
6506
+ import {
6507
+ clampVisualDpi,
6508
+ DEFAULT_VISUAL_DPI
6509
+ } from "@json-to-office/shared";
6510
+ var DEFAULT_RASTERIZE_SERVER_URL = "http://localhost:7802";
6511
+ var PIXELS_PER_INCH = 96;
6512
+ function buildVisualPresentation(props) {
6513
+ const { canvas, elements } = props;
6514
+ const presentationProps = {
6515
+ slideWidth: canvas.width,
6516
+ slideHeight: canvas.height
6517
+ };
6518
+ if (canvas.theme) presentationProps.theme = canvas.theme;
6519
+ const slideProps = {};
6520
+ if (canvas.background) slideProps.background = canvas.background;
6521
+ return {
6522
+ name: "pptx",
6523
+ props: presentationProps,
6524
+ children: [
6525
+ {
6526
+ name: "slide",
6527
+ props: slideProps,
6528
+ children: elements ?? []
6529
+ }
6530
+ ]
6531
+ };
6532
+ }
6533
+ function defaultVisualWidthPx(props) {
6534
+ return Math.round(props.canvas.width * PIXELS_PER_INCH);
6535
+ }
6536
+ function visualToImageOptions(props) {
6537
+ return {
6538
+ width: props.width ?? defaultVisualWidthPx(props),
6539
+ ...props.height !== void 0 && { height: props.height },
6540
+ alignment: props.alignment ?? "center",
6541
+ ...props.caption !== void 0 && { caption: props.caption },
6542
+ ...props.spacing !== void 0 && { spacing: props.spacing },
6543
+ ...props.floating !== void 0 && { floating: props.floating },
6544
+ ...props.keepNext !== void 0 && { keepNext: props.keepNext },
6545
+ ...props.keepLines !== void 0 && { keepLines: props.keepLines }
6546
+ };
6547
+ }
6548
+ async function rasterize(presentation, dpi, propsServerUrl, serviceConfig) {
6549
+ if (!isNodeEnvironment()) {
6550
+ throw new Error(
6551
+ "Visual rasterization requires a Node.js environment. It is not available in browser environments."
6552
+ );
6553
+ }
6554
+ if (serviceConfig?.render) {
6555
+ return serviceConfig.render({ presentation, dpi });
6556
+ }
6557
+ const serverUrl = resolveServiceUrl(
6558
+ propsServerUrl,
6559
+ serviceConfig?.serverUrl,
6560
+ DEFAULT_RASTERIZE_SERVER_URL
6561
+ );
6562
+ const response = await postJsonToService({
6563
+ url: serverUrl,
6564
+ path: "/rasterize",
6565
+ body: { presentation, dpi },
6566
+ headers: serviceConfig?.headers,
6567
+ serviceLabel: "PPTX rasterization service",
6568
+ onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Configure services.pptx with a \`render\` callback or a running \`serverUrl\`.
6569
+ Cause: ${cause}`
6570
+ });
6571
+ let result;
6572
+ try {
6573
+ result = await response.json();
6574
+ } catch {
6575
+ throw new Error(
6576
+ "PPTX rasterization service returned a non-JSON response (expected { base64DataUri, width, height })."
6577
+ );
6578
+ }
6579
+ if (!result?.base64DataUri) {
6580
+ throw new Error(
6581
+ "PPTX rasterization service returned a malformed response (missing base64DataUri)."
6582
+ );
6583
+ }
6584
+ return result;
6585
+ }
6586
+ async function renderVisualComponent(component, theme, themeName, context) {
6587
+ if (!isVisualComponent(component)) return [];
6588
+ const props = component.props;
6589
+ const serviceConfig = context?.services?.pptx;
6590
+ const presentation = buildVisualPresentation(props);
6591
+ const dpi = clampVisualDpi(
6592
+ props.dpi ?? serviceConfig?.dpi ?? DEFAULT_VISUAL_DPI
6593
+ );
6594
+ const result = await rasterize(
6595
+ presentation,
6596
+ dpi,
6597
+ props.serverUrl,
6598
+ serviceConfig
6599
+ );
6600
+ return await createImage(
6601
+ result.base64DataUri,
6602
+ theme,
6603
+ themeName,
6604
+ visualToImageOptions(props)
6605
+ );
6606
+ }
6607
+
6471
6608
  // src/components/text-space-after.ts
6472
6609
  import { Type as Type2 } from "@sinclair/typebox";
6473
6610
 
@@ -6624,7 +6761,7 @@ async function renderDocument(structure, layout, options) {
6624
6761
  }
6625
6762
  });
6626
6763
  }
6627
- async function renderHeaderFooterComponents(components, theme, themeName, _context) {
6764
+ async function renderHeaderFooterComponents(components, theme, themeName, context) {
6628
6765
  if (!components || components.length === 0) {
6629
6766
  return [];
6630
6767
  }
@@ -6756,6 +6893,14 @@ async function renderHeaderFooterComponents(components, theme, themeName, _conte
6756
6893
  } else if (isTableComponent(component)) {
6757
6894
  const tables = await renderTableComponent(component, theme, themeName);
6758
6895
  elements.push(...tables);
6896
+ } else if (isVisualComponent(component)) {
6897
+ const visualEls = await renderVisualComponent(
6898
+ component,
6899
+ theme,
6900
+ themeName,
6901
+ context
6902
+ );
6903
+ elements.push(...visualEls);
6759
6904
  }
6760
6905
  }
6761
6906
  return elements;
@@ -6876,6 +7021,8 @@ async function renderComponent(component, theme, themeName, context) {
6876
7021
  themeName,
6877
7022
  context
6878
7023
  );
7024
+ } else if (isVisualComponent(component)) {
7025
+ return await renderVisualComponent(component, theme, themeName, context);
6879
7026
  } else if (isSectionComponent(component)) {
6880
7027
  return await renderSectionComponent(component, theme, themeName, context);
6881
7028
  }