@json-to-office/core-docx 0.15.0 → 0.17.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
@@ -1954,7 +1954,8 @@ import {
1954
1954
  isTableComponent,
1955
1955
  isListComponent,
1956
1956
  isTocComponent,
1957
- isHighchartsComponent
1957
+ isHighchartsComponent,
1958
+ isVisualComponent
1958
1959
  } from "@json-to-office/shared-docx";
1959
1960
 
1960
1961
  // src/core/generator.ts
@@ -2050,6 +2051,9 @@ function resolveListProps(props, theme) {
2050
2051
  function resolveHighchartsProps(props, _theme) {
2051
2052
  return props;
2052
2053
  }
2054
+ function resolveVisualProps(props, _theme) {
2055
+ return props;
2056
+ }
2053
2057
  function getCustomComponentDefaults(theme, componentName) {
2054
2058
  const defaults = getComponentDefaults(theme);
2055
2059
  return defaults?.[componentName] || {};
@@ -2079,7 +2083,8 @@ var RESOLVER_MAP = {
2079
2083
  section: resolveSectionProps,
2080
2084
  columns: resolveColumnsProps,
2081
2085
  list: resolveListProps,
2082
- highcharts: resolveHighchartsProps
2086
+ highcharts: resolveHighchartsProps,
2087
+ visual: resolveVisualProps
2083
2088
  };
2084
2089
  function resolveComponentDefaults(component, theme) {
2085
2090
  if (!component.props) return component;
@@ -4068,7 +4073,7 @@ async function clearComponentCache() {
4068
4073
  }
4069
4074
  }
4070
4075
  async function renderComponentWithCache(component, theme, themeName, context, bypassCache = false) {
4071
- const forceBypassForType = component.name === "toc" || component.name === "section" || componentHasRevision(component);
4076
+ const forceBypassForType = component.name === "toc" || component.name === "section" || component.name === "visual" || componentHasRevision(component);
4072
4077
  if (!componentCache) {
4073
4078
  initializeComponentCache();
4074
4079
  }
@@ -6350,21 +6355,61 @@ function hasNodeBuiltins() {
6350
6355
  }
6351
6356
  }
6352
6357
 
6358
+ // src/utils/serviceClient.ts
6359
+ var DEFAULT_TIMEOUT_MS = 3e4;
6360
+ function resolveServiceUrl(propsUrl, servicesUrl, defaultUrl) {
6361
+ const raw = (propsUrl || servicesUrl || defaultUrl).trim();
6362
+ const withScheme = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
6363
+ return withScheme.replace(/\/+$/, "");
6364
+ }
6365
+ async function postJsonToService(opts) {
6366
+ const resolvedHeaders = typeof opts.headers === "function" ? await opts.headers(opts.body) : opts.headers;
6367
+ const headers = {
6368
+ "Content-Type": "application/json",
6369
+ ...resolvedHeaders
6370
+ };
6371
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
6372
+ const controller = new AbortController();
6373
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
6374
+ let response;
6375
+ try {
6376
+ response = await fetch(`${opts.url}${opts.path}`, {
6377
+ method: "POST",
6378
+ headers,
6379
+ body: JSON.stringify(opts.body),
6380
+ signal: controller.signal
6381
+ });
6382
+ } catch (error) {
6383
+ if (error?.name === "AbortError") {
6384
+ throw new Error(
6385
+ `${opts.serviceLabel} timed out after ${timeoutMs}ms at ${opts.url}.`
6386
+ );
6387
+ }
6388
+ const cause = error instanceof Error ? error.message : String(error);
6389
+ throw new Error(opts.onUnreachable(opts.url, cause));
6390
+ } finally {
6391
+ clearTimeout(timer);
6392
+ }
6393
+ if (!response.ok) {
6394
+ throw new Error(
6395
+ `${opts.serviceLabel} returned ${response.status}: ${response.statusText}`
6396
+ );
6397
+ }
6398
+ return response;
6399
+ }
6400
+
6353
6401
  // src/components/highcharts.ts
6354
6402
  var DEFAULT_EXPORT_SERVER_URL = "http://localhost:7801";
6355
- function getExportServerUrl(propsUrl, servicesUrl) {
6356
- const raw = propsUrl || servicesUrl || DEFAULT_EXPORT_SERVER_URL;
6357
- return raw.startsWith("http") ? raw : `http://${raw}`;
6358
- }
6359
6403
  async function generateChart(config, servicesConfig) {
6360
6404
  if (!isNodeEnvironment()) {
6361
6405
  throw new Error(
6362
6406
  "Highcharts export server requires a Node.js environment. Chart generation is not available in browser environments."
6363
6407
  );
6364
6408
  }
6365
- const serverUrl = getExportServerUrl(
6409
+ const serverUrl = resolveServiceUrl(
6366
6410
  config.serverUrl,
6367
- servicesConfig?.serverUrl
6411
+ servicesConfig?.serverUrl,
6412
+ DEFAULT_EXPORT_SERVER_URL
6368
6413
  );
6369
6414
  const requestBody = {
6370
6415
  infile: config.options,
@@ -6372,26 +6417,15 @@ async function generateChart(config, servicesConfig) {
6372
6417
  b64: true,
6373
6418
  scale: config.scale
6374
6419
  };
6375
- const resolvedHeaders = typeof servicesConfig?.headers === "function" ? await servicesConfig.headers(requestBody) : servicesConfig?.headers;
6376
- const headers = {
6377
- "Content-Type": "application/json",
6378
- ...resolvedHeaders
6379
- };
6380
- const response = await fetch(`${serverUrl}/export`, {
6381
- method: "POST",
6382
- headers,
6383
- body: JSON.stringify(requestBody)
6384
- }).catch((error) => {
6385
- throw new Error(
6386
- `Highcharts Export Server is not running at ${serverUrl}. Start it with: npx highcharts-export-server --enableServer true
6387
- Cause: ${error instanceof Error ? error.message : String(error)}`
6388
- );
6420
+ const response = await postJsonToService({
6421
+ url: serverUrl,
6422
+ path: "/export",
6423
+ body: requestBody,
6424
+ headers: servicesConfig?.headers,
6425
+ serviceLabel: "Highcharts export server",
6426
+ onUnreachable: (url, cause) => `Highcharts Export Server is not running at ${url}. Start it with: npx highcharts-export-server --enableServer true
6427
+ Cause: ${cause}`
6389
6428
  });
6390
- if (!response.ok) {
6391
- throw new Error(
6392
- `Highcharts export server returned ${response.status}: ${response.statusText}`
6393
- );
6394
- }
6395
6429
  const base64Data = await response.text();
6396
6430
  const base64DataUri = `data:image/png;base64,${base64Data}`;
6397
6431
  const width = config.options.chart.width;
@@ -6425,6 +6459,116 @@ async function renderHighchartsComponent(component, theme, themeName, context) {
6425
6459
  return imageParagraphs;
6426
6460
  }
6427
6461
 
6462
+ // src/components/visual.ts
6463
+ import {
6464
+ clampVisualDpi,
6465
+ DEFAULT_VISUAL_DPI
6466
+ } from "@json-to-office/shared";
6467
+ var DEFAULT_RASTERIZE_SERVER_URL = "http://localhost:7802";
6468
+ var PIXELS_PER_INCH = 96;
6469
+ function buildVisualPresentation(props) {
6470
+ const { canvas, elements } = props;
6471
+ const presentationProps = {
6472
+ slideWidth: canvas.width,
6473
+ slideHeight: canvas.height
6474
+ };
6475
+ if (canvas.theme) presentationProps.theme = canvas.theme;
6476
+ const slideProps = {};
6477
+ if (canvas.background) slideProps.background = canvas.background;
6478
+ return {
6479
+ name: "pptx",
6480
+ props: presentationProps,
6481
+ children: [
6482
+ {
6483
+ name: "slide",
6484
+ props: slideProps,
6485
+ children: elements ?? []
6486
+ }
6487
+ ]
6488
+ };
6489
+ }
6490
+ function defaultVisualWidthPx(props) {
6491
+ return Math.round(props.canvas.width * PIXELS_PER_INCH);
6492
+ }
6493
+ function visualToImageOptions(props) {
6494
+ return {
6495
+ width: props.width ?? defaultVisualWidthPx(props),
6496
+ ...props.height !== void 0 && { height: props.height },
6497
+ alignment: props.alignment ?? "center",
6498
+ ...props.caption !== void 0 && { caption: props.caption },
6499
+ ...props.spacing !== void 0 && { spacing: props.spacing },
6500
+ ...props.floating !== void 0 && { floating: props.floating },
6501
+ ...props.keepNext !== void 0 && { keepNext: props.keepNext },
6502
+ ...props.keepLines !== void 0 && { keepLines: props.keepLines }
6503
+ };
6504
+ }
6505
+ function visualToImageProps(props, base64DataUri) {
6506
+ return {
6507
+ base64: base64DataUri,
6508
+ ...visualToImageOptions(props),
6509
+ ...props.alt !== void 0 && { alt: props.alt }
6510
+ };
6511
+ }
6512
+ async function rasterize(presentation, dpi, propsServerUrl, serviceConfig) {
6513
+ if (!isNodeEnvironment()) {
6514
+ throw new Error(
6515
+ "Visual rasterization requires a Node.js environment. It is not available in browser environments."
6516
+ );
6517
+ }
6518
+ if (serviceConfig?.render) {
6519
+ return serviceConfig.render({ presentation, dpi });
6520
+ }
6521
+ const serverUrl = resolveServiceUrl(
6522
+ propsServerUrl,
6523
+ serviceConfig?.serverUrl,
6524
+ DEFAULT_RASTERIZE_SERVER_URL
6525
+ );
6526
+ const response = await postJsonToService({
6527
+ url: serverUrl,
6528
+ path: "/rasterize",
6529
+ body: { presentation, dpi },
6530
+ headers: serviceConfig?.headers,
6531
+ serviceLabel: "PPTX rasterization service",
6532
+ onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Configure services.pptx with a \`render\` callback or a running \`serverUrl\`.
6533
+ Cause: ${cause}`
6534
+ });
6535
+ let result;
6536
+ try {
6537
+ result = await response.json();
6538
+ } catch {
6539
+ throw new Error(
6540
+ "PPTX rasterization service returned a non-JSON response (expected { base64DataUri, width, height })."
6541
+ );
6542
+ }
6543
+ if (!result?.base64DataUri) {
6544
+ throw new Error(
6545
+ "PPTX rasterization service returned a malformed response (missing base64DataUri)."
6546
+ );
6547
+ }
6548
+ return result;
6549
+ }
6550
+ async function renderVisualComponent(component, theme, themeName, context) {
6551
+ if (!isVisualComponent(component)) return [];
6552
+ const props = component.props;
6553
+ const serviceConfig = context?.services?.pptx;
6554
+ const presentation = buildVisualPresentation(props);
6555
+ const dpi = clampVisualDpi(
6556
+ props.dpi ?? serviceConfig?.dpi ?? DEFAULT_VISUAL_DPI
6557
+ );
6558
+ const result = await rasterize(
6559
+ presentation,
6560
+ dpi,
6561
+ props.serverUrl,
6562
+ serviceConfig
6563
+ );
6564
+ return await createImage(
6565
+ result.base64DataUri,
6566
+ theme,
6567
+ themeName,
6568
+ visualToImageOptions(props)
6569
+ );
6570
+ }
6571
+
6428
6572
  // src/components/text-space-after.ts
6429
6573
  import { Type } from "@sinclair/typebox";
6430
6574
 
@@ -6581,7 +6725,7 @@ async function renderDocument(structure, layout, options) {
6581
6725
  }
6582
6726
  });
6583
6727
  }
6584
- async function renderHeaderFooterComponents(components, theme, themeName, _context) {
6728
+ async function renderHeaderFooterComponents(components, theme, themeName, context) {
6585
6729
  if (!components || components.length === 0) {
6586
6730
  return [];
6587
6731
  }
@@ -6713,6 +6857,14 @@ async function renderHeaderFooterComponents(components, theme, themeName, _conte
6713
6857
  } else if (isTableComponent(component)) {
6714
6858
  const tables = await renderTableComponent(component, theme, themeName);
6715
6859
  elements.push(...tables);
6860
+ } else if (isVisualComponent(component)) {
6861
+ const visualEls = await renderVisualComponent(
6862
+ component,
6863
+ theme,
6864
+ themeName,
6865
+ context
6866
+ );
6867
+ elements.push(...visualEls);
6716
6868
  }
6717
6869
  }
6718
6870
  return elements;
@@ -6833,6 +6985,8 @@ async function renderComponent(component, theme, themeName, context) {
6833
6985
  themeName,
6834
6986
  context
6835
6987
  );
6988
+ } else if (isVisualComponent(component)) {
6989
+ return await renderVisualComponent(component, theme, themeName, context);
6836
6990
  } else if (isSectionComponent(component)) {
6837
6991
  return await renderSectionComponent(component, theme, themeName, context);
6838
6992
  }
@@ -7133,9 +7287,32 @@ async function generateDocumentWithCustomThemes(documentIn, customThemes, servic
7133
7287
  return renderedDocument;
7134
7288
  }
7135
7289
  async function generateDocumentFromJson(jsonConfig, options) {
7290
+ const validation = options?.validation;
7291
+ if (validation?.enabled !== false) {
7292
+ const result = validateJsonComponent(jsonConfig, {
7293
+ allowUnknownFields: validation?.allowUnknownFields
7294
+ });
7295
+ if (!result.valid) {
7296
+ throw new JsonValidationError(
7297
+ "Document validation failed",
7298
+ result.errors
7299
+ );
7300
+ }
7301
+ }
7136
7302
  let componentToConvert;
7137
7303
  if (typeof jsonConfig === "string") {
7138
- const parsed = parseJsonComponent(jsonConfig);
7304
+ let parsed;
7305
+ try {
7306
+ parsed = JSON.parse(jsonConfig);
7307
+ } catch (error) {
7308
+ throw new JsonParsingError("Invalid JSON syntax", [
7309
+ {
7310
+ path: "",
7311
+ message: error instanceof Error ? error.message : "Invalid JSON",
7312
+ code: "JSON_PARSE_ERROR"
7313
+ }
7314
+ ]);
7315
+ }
7139
7316
  if (!isReportComponent(parsed)) {
7140
7317
  throw new Error("Parsed JSON must be a docx component");
7141
7318
  }
@@ -7204,6 +7381,112 @@ var DocumentGenerator = {
7204
7381
  isReportComponentDefinition
7205
7382
  };
7206
7383
 
7384
+ // src/core/flattenVisuals.ts
7385
+ import {
7386
+ clampVisualDpi as clampVisualDpi2,
7387
+ DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI2
7388
+ } from "@json-to-office/shared";
7389
+ var DEFAULT_CONCURRENCY = 4;
7390
+ function createLimiter(max) {
7391
+ let active = 0;
7392
+ const queue = [];
7393
+ const release = () => {
7394
+ active--;
7395
+ queue.shift()?.();
7396
+ };
7397
+ return async function limit(fn) {
7398
+ if (active >= max)
7399
+ await new Promise((resolve2) => queue.push(resolve2));
7400
+ active++;
7401
+ try {
7402
+ return await fn();
7403
+ } finally {
7404
+ release();
7405
+ }
7406
+ };
7407
+ }
7408
+ async function flattenVisuals(doc, options) {
7409
+ const ctx = {
7410
+ rasterize: options.rasterize,
7411
+ dpi: options.dpi,
7412
+ limit: createLimiter(
7413
+ Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY)
7414
+ )
7415
+ };
7416
+ return await flattenNode(doc, ctx);
7417
+ }
7418
+ async function rasterizeVisual(obj, ctx) {
7419
+ const props = obj.props;
7420
+ const dpi = clampVisualDpi2(props.dpi ?? ctx.dpi ?? DEFAULT_VISUAL_DPI2);
7421
+ const result = await ctx.limit(
7422
+ () => ctx.rasterize({ presentation: buildVisualPresentation(props), dpi })
7423
+ );
7424
+ const image = {
7425
+ name: "image",
7426
+ props: visualToImageProps(props, result.base64DataUri)
7427
+ };
7428
+ if (obj.id !== void 0) image.id = obj.id;
7429
+ if (obj.enabled !== void 0) image.enabled = obj.enabled;
7430
+ return image;
7431
+ }
7432
+ async function flattenNode(node, ctx) {
7433
+ if (Array.isArray(node)) {
7434
+ return Promise.all(node.map((n) => flattenNode(n, ctx)));
7435
+ }
7436
+ if (!node || typeof node !== "object") return node;
7437
+ const obj = node;
7438
+ if (obj.name === "visual" && obj.props && obj.enabled !== false) {
7439
+ return rasterizeVisual(obj, ctx);
7440
+ }
7441
+ const next = { ...obj };
7442
+ if (Array.isArray(obj.children)) {
7443
+ next.children = await flattenNode(obj.children, ctx);
7444
+ }
7445
+ if (obj.props && typeof obj.props === "object") {
7446
+ const props = obj.props;
7447
+ let propsChanged = false;
7448
+ const nextProps = { ...props };
7449
+ for (const key of ["header", "footer"]) {
7450
+ if (Array.isArray(props[key])) {
7451
+ nextProps[key] = await flattenNode(props[key], ctx);
7452
+ propsChanged = true;
7453
+ }
7454
+ }
7455
+ if (Array.isArray(props.columns)) {
7456
+ nextProps.columns = await Promise.all(
7457
+ props.columns.map((col) => flattenColumn(col, ctx))
7458
+ );
7459
+ propsChanged = true;
7460
+ }
7461
+ if (propsChanged) next.props = nextProps;
7462
+ }
7463
+ return next;
7464
+ }
7465
+ async function flattenColumn(col, ctx) {
7466
+ if (!col || typeof col !== "object") return col;
7467
+ const column = col;
7468
+ const nextCol = { ...column };
7469
+ const header = column.header;
7470
+ if (header && typeof header === "object" && "content" in header) {
7471
+ nextCol.header = {
7472
+ ...header,
7473
+ content: await flattenNode(header.content, ctx)
7474
+ };
7475
+ }
7476
+ if (Array.isArray(column.cells)) {
7477
+ nextCol.cells = await Promise.all(
7478
+ column.cells.map(async (cell) => {
7479
+ if (cell && typeof cell === "object" && "content" in cell) {
7480
+ const c = cell;
7481
+ return { ...c, content: await flattenNode(c.content, ctx) };
7482
+ }
7483
+ return cell;
7484
+ })
7485
+ );
7486
+ }
7487
+ return nextCol;
7488
+ }
7489
+
7207
7490
  // src/utils/warningsDocument.ts
7208
7491
  function generateWarningsDocument(warnings) {
7209
7492
  if (!warnings || warnings.length === 0) {
@@ -7430,22 +7713,20 @@ import {
7430
7713
  ComponentValidationError as ComponentValidationError2,
7431
7714
  UnknownPreservedComponentError
7432
7715
  } from "@json-to-office/shared/plugin";
7433
- function validateComponentProps(schema, props, componentName) {
7716
+ function validateComponentProps(schema, props, componentName, opts) {
7434
7717
  return validateCustomComponentProps(schema.propsSchema, props, {
7435
- clean: true,
7436
- applyDefaults: true,
7718
+ clean: opts?.clean ?? true,
7719
+ applyDefaults: opts?.applyDefaults ?? true,
7437
7720
  componentName
7438
7721
  });
7439
7722
  }
7440
- function validateDocument(document, customComponents) {
7723
+ function validateDocument(document, customComponents, options) {
7724
+ const knownCustomNames = new Set(customComponents.map((c) => c.name));
7441
7725
  const documentResult = validateDocumentUnified(document, {
7442
- clean: true,
7443
- applyDefaults: true
7726
+ knownCustomNames,
7727
+ allowUnknownFields: options?.allowUnknownFields
7444
7728
  });
7445
- if (!documentResult.valid) {
7446
- return { ...documentResult, success: false };
7447
- }
7448
- const errors = [];
7729
+ const errors = [...documentResult.errors || []];
7449
7730
  function validateComponents(components, pathPrefix = "children") {
7450
7731
  components.forEach((componentData, index) => {
7451
7732
  const customComponent = customComponents.find(
@@ -7462,7 +7743,9 @@ function validateDocument(document, customComponents) {
7462
7743
  const validation = validateComponentProps(
7463
7744
  versionEntry,
7464
7745
  componentData.props,
7465
- customComponent.name
7746
+ customComponent.name,
7747
+ // Reject unknown custom props unless the caller allows them.
7748
+ { clean: options?.allowUnknownFields === true }
7466
7749
  );
7467
7750
  if (!validation.valid && validation.errors) {
7468
7751
  const indexedErrors = validation.errors.map(
@@ -7827,10 +8110,26 @@ function createBuilderImpl(state) {
7827
8110
  try {
7828
8111
  const preserveSet = resolvePreserveSet(options);
7829
8112
  const internalDocument = document;
7830
- validateDocument(
7831
- internalDocument,
7832
- state.components
7833
- );
8113
+ const vOpts = {
8114
+ ...state.validation,
8115
+ ...options?.validation
8116
+ };
8117
+ if (vOpts.enabled !== false) {
8118
+ const result = validateDocument(
8119
+ internalDocument,
8120
+ state.components,
8121
+ { allowUnknownFields: vOpts.allowUnknownFields }
8122
+ );
8123
+ if (!result.valid) {
8124
+ throw new ComponentValidationError2(
8125
+ (result.errors ?? []).map((e) => ({
8126
+ path: e.path ?? "",
8127
+ message: e.message
8128
+ })),
8129
+ internalDocument
8130
+ );
8131
+ }
8132
+ }
7834
8133
  const baseThemeName = internalDocument.props.theme || "minimal";
7835
8134
  const docTheme = resolveDocumentTheme(baseThemeName);
7836
8135
  const warnings = [];
@@ -7924,21 +8223,21 @@ function createBuilderImpl(state) {
7924
8223
  function validate(document) {
7925
8224
  try {
7926
8225
  const internalDocument = document;
7927
- validateDocument(
8226
+ const result = validateDocument(
7928
8227
  internalDocument,
7929
8228
  state.components
7930
8229
  );
7931
- return { valid: true };
7932
- } catch (error) {
7933
- if (error instanceof ComponentValidationError2) {
7934
- return {
7935
- valid: false,
7936
- errors: error.errors.map((e) => ({
7937
- path: e.path,
7938
- message: e.message
7939
- }))
7940
- };
8230
+ if (result.valid) {
8231
+ return { valid: true };
7941
8232
  }
8233
+ return {
8234
+ valid: false,
8235
+ errors: (result.errors ?? []).map((e) => ({
8236
+ path: e.path ?? "",
8237
+ message: e.message
8238
+ }))
8239
+ };
8240
+ } catch (error) {
7942
8241
  return {
7943
8242
  valid: false,
7944
8243
  errors: [
@@ -7988,7 +8287,8 @@ function createDocumentGenerator(options) {
7988
8287
  debug: options.debug ?? false,
7989
8288
  enableCache: options.enableCache ?? false,
7990
8289
  services: options.services,
7991
- fonts: options.fonts
8290
+ fonts: options.fonts,
8291
+ validation: options.validation
7992
8292
  };
7993
8293
  return createBuilderImpl(initialState);
7994
8294
  }
@@ -8023,6 +8323,7 @@ export {
8023
8323
  examples,
8024
8324
  exportPluginSchema,
8025
8325
  exportThemeToJson,
8326
+ flattenVisuals,
8026
8327
  formatDate,
8027
8328
  formatWarningsText,
8028
8329
  generateAndSave,
@@ -8057,6 +8358,7 @@ export {
8057
8358
  isSectionComponent,
8058
8359
  isStatisticComponent,
8059
8360
  isTableComponent,
8361
+ isVisualComponent,
8060
8362
  loadJsonExample,
8061
8363
  loadThemeFromFile,
8062
8364
  loadThemeFromJson,