@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.
@@ -1,6 +1,6 @@
1
1
  import { type ThemeConfig } from '../styles';
2
2
  import type { ServicesConfig, FontRuntimeOpts } from '@json-to-office/shared';
3
- import type { DocumentGeneratorBuilder } from './types';
3
+ import type { DocumentGeneratorBuilder, GenerationValidationOptions } from './types';
4
4
  /**
5
5
  * Options for creating a document generator
6
6
  */
@@ -17,6 +17,12 @@ export interface DocumentGeneratorOptions {
17
17
  services?: ServicesConfig;
18
18
  /** Font resolution options — extraEntries, Google Fonts config, onResolved hook. */
19
19
  fonts?: FontRuntimeOpts;
20
+ /**
21
+ * Default validation behavior for every generate/generateBuffer/generateFile
22
+ * call. Per-call `options.validation` overrides these. Validation is on by
23
+ * default; pass `{ enabled: false }` to opt out.
24
+ */
25
+ validation?: GenerationValidationOptions;
20
26
  }
21
27
  /**
22
28
  * Create a document generator with chainable component registration.
@@ -1 +1 @@
1
- {"version":3,"file":"createDocumentGenerator.d.ts","sourceRoot":"","sources":["../../src/plugin/createDocumentGenerator.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,WAAW,EAAwB,MAAM,WAAW,CAAC;AAEnE,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9E,OAAO,KAAK,EAEV,wBAAwB,EAOzB,MAAM,SAAS,CAAC;AAejB;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC3C,4CAA4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,oFAAoF;IACpF,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB;AAiiBD;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,wBAAwB,GAChC,wBAAwB,CAAC,SAAS,EAAE,CAAC,CAavC"}
1
+ {"version":3,"file":"createDocumentGenerator.d.ts","sourceRoot":"","sources":["../../src/plugin/createDocumentGenerator.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,WAAW,EAAwB,MAAM,WAAW,CAAC;AAEnE,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9E,OAAO,KAAK,EAEV,wBAAwB,EAOxB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAejB;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC3C,4CAA4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,oFAAoF;IACpF,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB;;;;OAIG;IACH,UAAU,CAAC,EAAE,2BAA2B,CAAC;CAC1C;AAojBD;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,wBAAwB,GAChC,wBAAwB,CAAC,SAAS,EAAE,CAAC,CAcvC"}
@@ -1904,22 +1904,20 @@ import {
1904
1904
  ComponentValidationError as ComponentValidationError2,
1905
1905
  UnknownPreservedComponentError
1906
1906
  } from "@json-to-office/shared/plugin";
1907
- function validateComponentProps(schema, props, componentName) {
1907
+ function validateComponentProps(schema, props, componentName, opts) {
1908
1908
  return validateCustomComponentProps(schema.propsSchema, props, {
1909
- clean: true,
1910
- applyDefaults: true,
1909
+ clean: opts?.clean ?? true,
1910
+ applyDefaults: opts?.applyDefaults ?? true,
1911
1911
  componentName
1912
1912
  });
1913
1913
  }
1914
- function validateDocument(document, customComponents) {
1914
+ function validateDocument(document, customComponents, options) {
1915
+ const knownCustomNames = new Set(customComponents.map((c) => c.name));
1915
1916
  const documentResult = validateDocumentUnified(document, {
1916
- clean: true,
1917
- applyDefaults: true
1917
+ knownCustomNames,
1918
+ allowUnknownFields: options?.allowUnknownFields
1918
1919
  });
1919
- if (!documentResult.valid) {
1920
- return { ...documentResult, success: false };
1921
- }
1922
- const errors = [];
1920
+ const errors = [...documentResult.errors || []];
1923
1921
  function validateComponents(components, pathPrefix = "children") {
1924
1922
  components.forEach((componentData, index) => {
1925
1923
  const customComponent = customComponents.find(
@@ -1936,7 +1934,9 @@ function validateDocument(document, customComponents) {
1936
1934
  const validation = validateComponentProps(
1937
1935
  versionEntry,
1938
1936
  componentData.props,
1939
- customComponent.name
1937
+ customComponent.name,
1938
+ // Reject unknown custom props unless the caller allows them.
1939
+ { clean: options?.allowUnknownFields === true }
1940
1940
  );
1941
1941
  if (!validation.valid && validation.errors) {
1942
1942
  const indexedErrors = validation.errors.map(
@@ -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
  }
@@ -7140,10 +7287,26 @@ function createBuilderImpl(state) {
7140
7287
  try {
7141
7288
  const preserveSet = resolvePreserveSet(options);
7142
7289
  const internalDocument = document;
7143
- validateDocument(
7144
- internalDocument,
7145
- state.components
7146
- );
7290
+ const vOpts = {
7291
+ ...state.validation,
7292
+ ...options?.validation
7293
+ };
7294
+ if (vOpts.enabled !== false) {
7295
+ const result = validateDocument(
7296
+ internalDocument,
7297
+ state.components,
7298
+ { allowUnknownFields: vOpts.allowUnknownFields }
7299
+ );
7300
+ if (!result.valid) {
7301
+ throw new ComponentValidationError2(
7302
+ (result.errors ?? []).map((e) => ({
7303
+ path: e.path ?? "",
7304
+ message: e.message
7305
+ })),
7306
+ internalDocument
7307
+ );
7308
+ }
7309
+ }
7147
7310
  const baseThemeName = internalDocument.props.theme || "minimal";
7148
7311
  const docTheme = resolveDocumentTheme(baseThemeName);
7149
7312
  const warnings = [];
@@ -7237,21 +7400,21 @@ function createBuilderImpl(state) {
7237
7400
  function validate(document) {
7238
7401
  try {
7239
7402
  const internalDocument = document;
7240
- validateDocument(
7403
+ const result = validateDocument(
7241
7404
  internalDocument,
7242
7405
  state.components
7243
7406
  );
7244
- return { valid: true };
7245
- } catch (error) {
7246
- if (error instanceof ComponentValidationError2) {
7247
- return {
7248
- valid: false,
7249
- errors: error.errors.map((e) => ({
7250
- path: e.path,
7251
- message: e.message
7252
- }))
7253
- };
7407
+ if (result.valid) {
7408
+ return { valid: true };
7254
7409
  }
7410
+ return {
7411
+ valid: false,
7412
+ errors: (result.errors ?? []).map((e) => ({
7413
+ path: e.path ?? "",
7414
+ message: e.message
7415
+ }))
7416
+ };
7417
+ } catch (error) {
7255
7418
  return {
7256
7419
  valid: false,
7257
7420
  errors: [
@@ -7301,7 +7464,8 @@ function createDocumentGenerator(options) {
7301
7464
  debug: options.debug ?? false,
7302
7465
  enableCache: options.enableCache ?? false,
7303
7466
  services: options.services,
7304
- fonts: options.fonts
7467
+ fonts: options.fonts,
7468
+ validation: options.validation
7305
7469
  };
7306
7470
  return createBuilderImpl(initialState);
7307
7471
  }