@json-to-office/core-docx 0.14.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.
package/dist/index.js CHANGED
@@ -856,12 +856,20 @@ function getPageDimensions(size) {
856
856
  }
857
857
  return size;
858
858
  }
859
- var PAGE_SIZES, getTableStyle, getDocumentMargins, getPageSetup;
859
+ var resolveTableParagraphSpacing, PAGE_SIZES, getTableStyle, getDocumentMargins, getPageSetup;
860
860
  var init_layoutUtils = __esm({
861
861
  "src/styles/utils/layoutUtils.ts"() {
862
862
  "use strict";
863
863
  init_colorUtils();
864
864
  init_styleHelpers();
865
+ resolveTableParagraphSpacing = (style) => {
866
+ const lineSpacing = convertLineSpacing(style?.lineSpacing) ?? convertLineSpacing({ type: "single" });
867
+ return {
868
+ before: style?.spacing?.before !== void 0 ? pointsToTwips(style.spacing.before) : 0,
869
+ after: style?.spacing?.after !== void 0 ? pointsToTwips(style.spacing.after) : 0,
870
+ ...lineSpacing
871
+ };
872
+ };
865
873
  PAGE_SIZES = {
866
874
  A4: { width: 11906, height: 16838 },
867
875
  A3: { width: 16838, height: 23811 },
@@ -872,7 +880,10 @@ var init_layoutUtils = __esm({
872
880
  const themeConfig = resolveTheme(theme, themeName);
873
881
  if (themeConfig && themeConfig.styles?.normal) {
874
882
  const normalStyle = themeConfig.styles.normal;
883
+ const styles = themeConfig.styles;
875
884
  return {
885
+ cellParagraph: resolveTableParagraphSpacing(styles.tableCell),
886
+ headerParagraph: resolveTableParagraphSpacing(styles.tableHeader),
876
887
  tableHeader: {
877
888
  fill: resolveColor(
878
889
  themeConfig.colors?.primary || "#000000",
@@ -938,6 +949,8 @@ var init_layoutUtils = __esm({
938
949
  };
939
950
  }
940
951
  return {
952
+ cellParagraph: resolveTableParagraphSpacing(),
953
+ headerParagraph: resolveTableParagraphSpacing(),
941
954
  tableHeader: {
942
955
  fill: "000000",
943
956
  color: "FFFFFF",
@@ -1941,7 +1954,8 @@ import {
1941
1954
  isTableComponent,
1942
1955
  isListComponent,
1943
1956
  isTocComponent,
1944
- isHighchartsComponent
1957
+ isHighchartsComponent,
1958
+ isVisualComponent
1945
1959
  } from "@json-to-office/shared-docx";
1946
1960
 
1947
1961
  // src/core/generator.ts
@@ -2037,6 +2051,9 @@ function resolveListProps(props, theme) {
2037
2051
  function resolveHighchartsProps(props, _theme) {
2038
2052
  return props;
2039
2053
  }
2054
+ function resolveVisualProps(props, _theme) {
2055
+ return props;
2056
+ }
2040
2057
  function getCustomComponentDefaults(theme, componentName) {
2041
2058
  const defaults = getComponentDefaults(theme);
2042
2059
  return defaults?.[componentName] || {};
@@ -2066,7 +2083,8 @@ var RESOLVER_MAP = {
2066
2083
  section: resolveSectionProps,
2067
2084
  columns: resolveColumnsProps,
2068
2085
  list: resolveListProps,
2069
- highcharts: resolveHighchartsProps
2086
+ highcharts: resolveHighchartsProps,
2087
+ visual: resolveVisualProps
2070
2088
  };
2071
2089
  function resolveComponentDefaults(component, theme) {
2072
2090
  if (!component.props) return component;
@@ -4055,7 +4073,7 @@ async function clearComponentCache() {
4055
4073
  }
4056
4074
  }
4057
4075
  async function renderComponentWithCache(component, theme, themeName, context, bypassCache = false) {
4058
- const forceBypassForType = component.name === "toc" || component.name === "section" || componentHasRevision(component);
4076
+ const forceBypassForType = component.name === "toc" || component.name === "section" || component.name === "visual" || componentHasRevision(component);
4059
4077
  if (!componentCache) {
4060
4078
  initializeComponentCache();
4061
4079
  }
@@ -5119,6 +5137,7 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5119
5137
  children: [
5120
5138
  new Paragraph({
5121
5139
  ...tableConfig.keepInOnePage && { keepNext: true },
5140
+ spacing: tableStyle.headerParagraph,
5122
5141
  alignment: getAlignment(horizontalAlignment),
5123
5142
  children: cellChildren
5124
5143
  })
@@ -5213,6 +5232,7 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5213
5232
  children: [
5214
5233
  new Paragraph({
5215
5234
  ...tableConfig.keepInOnePage && !isLastRow || isLastRow && tableConfig.keepNext ? { keepNext: true } : {},
5235
+ spacing: tableStyle.cellParagraph,
5216
5236
  alignment: AlignmentType2.LEFT,
5217
5237
  children: []
5218
5238
  })
@@ -5288,6 +5308,7 @@ async function createTable(columns, tableConfig, theme, themeName, _options = {}
5288
5308
  children: [
5289
5309
  new Paragraph({
5290
5310
  ...tableConfig.keepInOnePage && !isLastRow || isLastRow && tableConfig.keepNext ? { keepNext: true } : {},
5311
+ spacing: tableStyle.cellParagraph,
5291
5312
  alignment: getAlignment(horizontalAlignment),
5292
5313
  children: cellChildren
5293
5314
  })
@@ -6334,21 +6355,61 @@ function hasNodeBuiltins() {
6334
6355
  }
6335
6356
  }
6336
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
+
6337
6401
  // src/components/highcharts.ts
6338
6402
  var DEFAULT_EXPORT_SERVER_URL = "http://localhost:7801";
6339
- function getExportServerUrl(propsUrl, servicesUrl) {
6340
- const raw = propsUrl || servicesUrl || DEFAULT_EXPORT_SERVER_URL;
6341
- return raw.startsWith("http") ? raw : `http://${raw}`;
6342
- }
6343
6403
  async function generateChart(config, servicesConfig) {
6344
6404
  if (!isNodeEnvironment()) {
6345
6405
  throw new Error(
6346
6406
  "Highcharts export server requires a Node.js environment. Chart generation is not available in browser environments."
6347
6407
  );
6348
6408
  }
6349
- const serverUrl = getExportServerUrl(
6409
+ const serverUrl = resolveServiceUrl(
6350
6410
  config.serverUrl,
6351
- servicesConfig?.serverUrl
6411
+ servicesConfig?.serverUrl,
6412
+ DEFAULT_EXPORT_SERVER_URL
6352
6413
  );
6353
6414
  const requestBody = {
6354
6415
  infile: config.options,
@@ -6356,26 +6417,15 @@ async function generateChart(config, servicesConfig) {
6356
6417
  b64: true,
6357
6418
  scale: config.scale
6358
6419
  };
6359
- const resolvedHeaders = typeof servicesConfig?.headers === "function" ? await servicesConfig.headers(requestBody) : servicesConfig?.headers;
6360
- const headers = {
6361
- "Content-Type": "application/json",
6362
- ...resolvedHeaders
6363
- };
6364
- const response = await fetch(`${serverUrl}/export`, {
6365
- method: "POST",
6366
- headers,
6367
- body: JSON.stringify(requestBody)
6368
- }).catch((error) => {
6369
- throw new Error(
6370
- `Highcharts Export Server is not running at ${serverUrl}. Start it with: npx highcharts-export-server --enableServer true
6371
- Cause: ${error instanceof Error ? error.message : String(error)}`
6372
- );
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}`
6373
6428
  });
6374
- if (!response.ok) {
6375
- throw new Error(
6376
- `Highcharts export server returned ${response.status}: ${response.statusText}`
6377
- );
6378
- }
6379
6429
  const base64Data = await response.text();
6380
6430
  const base64DataUri = `data:image/png;base64,${base64Data}`;
6381
6431
  const width = config.options.chart.width;
@@ -6409,6 +6459,116 @@ async function renderHighchartsComponent(component, theme, themeName, context) {
6409
6459
  return imageParagraphs;
6410
6460
  }
6411
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
+
6412
6572
  // src/components/text-space-after.ts
6413
6573
  import { Type } from "@sinclair/typebox";
6414
6574
 
@@ -6565,7 +6725,7 @@ async function renderDocument(structure, layout, options) {
6565
6725
  }
6566
6726
  });
6567
6727
  }
6568
- async function renderHeaderFooterComponents(components, theme, themeName, _context) {
6728
+ async function renderHeaderFooterComponents(components, theme, themeName, context) {
6569
6729
  if (!components || components.length === 0) {
6570
6730
  return [];
6571
6731
  }
@@ -6697,6 +6857,14 @@ async function renderHeaderFooterComponents(components, theme, themeName, _conte
6697
6857
  } else if (isTableComponent(component)) {
6698
6858
  const tables = await renderTableComponent(component, theme, themeName);
6699
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);
6700
6868
  }
6701
6869
  }
6702
6870
  return elements;
@@ -6817,6 +6985,8 @@ async function renderComponent(component, theme, themeName, context) {
6817
6985
  themeName,
6818
6986
  context
6819
6987
  );
6988
+ } else if (isVisualComponent(component)) {
6989
+ return await renderVisualComponent(component, theme, themeName, context);
6820
6990
  } else if (isSectionComponent(component)) {
6821
6991
  return await renderSectionComponent(component, theme, themeName, context);
6822
6992
  }
@@ -7188,6 +7358,112 @@ var DocumentGenerator = {
7188
7358
  isReportComponentDefinition
7189
7359
  };
7190
7360
 
7361
+ // src/core/flattenVisuals.ts
7362
+ import {
7363
+ clampVisualDpi as clampVisualDpi2,
7364
+ DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI2
7365
+ } from "@json-to-office/shared";
7366
+ var DEFAULT_CONCURRENCY = 4;
7367
+ function createLimiter(max) {
7368
+ let active = 0;
7369
+ const queue = [];
7370
+ const release = () => {
7371
+ active--;
7372
+ queue.shift()?.();
7373
+ };
7374
+ return async function limit(fn) {
7375
+ if (active >= max)
7376
+ await new Promise((resolve2) => queue.push(resolve2));
7377
+ active++;
7378
+ try {
7379
+ return await fn();
7380
+ } finally {
7381
+ release();
7382
+ }
7383
+ };
7384
+ }
7385
+ async function flattenVisuals(doc, options) {
7386
+ const ctx = {
7387
+ rasterize: options.rasterize,
7388
+ dpi: options.dpi,
7389
+ limit: createLimiter(
7390
+ Math.max(1, options.concurrency ?? DEFAULT_CONCURRENCY)
7391
+ )
7392
+ };
7393
+ return await flattenNode(doc, ctx);
7394
+ }
7395
+ async function rasterizeVisual(obj, ctx) {
7396
+ const props = obj.props;
7397
+ const dpi = clampVisualDpi2(props.dpi ?? ctx.dpi ?? DEFAULT_VISUAL_DPI2);
7398
+ const result = await ctx.limit(
7399
+ () => ctx.rasterize({ presentation: buildVisualPresentation(props), dpi })
7400
+ );
7401
+ const image = {
7402
+ name: "image",
7403
+ props: visualToImageProps(props, result.base64DataUri)
7404
+ };
7405
+ if (obj.id !== void 0) image.id = obj.id;
7406
+ if (obj.enabled !== void 0) image.enabled = obj.enabled;
7407
+ return image;
7408
+ }
7409
+ async function flattenNode(node, ctx) {
7410
+ if (Array.isArray(node)) {
7411
+ return Promise.all(node.map((n) => flattenNode(n, ctx)));
7412
+ }
7413
+ if (!node || typeof node !== "object") return node;
7414
+ const obj = node;
7415
+ if (obj.name === "visual" && obj.props && obj.enabled !== false) {
7416
+ return rasterizeVisual(obj, ctx);
7417
+ }
7418
+ const next = { ...obj };
7419
+ if (Array.isArray(obj.children)) {
7420
+ next.children = await flattenNode(obj.children, ctx);
7421
+ }
7422
+ if (obj.props && typeof obj.props === "object") {
7423
+ const props = obj.props;
7424
+ let propsChanged = false;
7425
+ const nextProps = { ...props };
7426
+ for (const key of ["header", "footer"]) {
7427
+ if (Array.isArray(props[key])) {
7428
+ nextProps[key] = await flattenNode(props[key], ctx);
7429
+ propsChanged = true;
7430
+ }
7431
+ }
7432
+ if (Array.isArray(props.columns)) {
7433
+ nextProps.columns = await Promise.all(
7434
+ props.columns.map((col) => flattenColumn(col, ctx))
7435
+ );
7436
+ propsChanged = true;
7437
+ }
7438
+ if (propsChanged) next.props = nextProps;
7439
+ }
7440
+ return next;
7441
+ }
7442
+ async function flattenColumn(col, ctx) {
7443
+ if (!col || typeof col !== "object") return col;
7444
+ const column = col;
7445
+ const nextCol = { ...column };
7446
+ const header = column.header;
7447
+ if (header && typeof header === "object" && "content" in header) {
7448
+ nextCol.header = {
7449
+ ...header,
7450
+ content: await flattenNode(header.content, ctx)
7451
+ };
7452
+ }
7453
+ if (Array.isArray(column.cells)) {
7454
+ nextCol.cells = await Promise.all(
7455
+ column.cells.map(async (cell) => {
7456
+ if (cell && typeof cell === "object" && "content" in cell) {
7457
+ const c = cell;
7458
+ return { ...c, content: await flattenNode(c.content, ctx) };
7459
+ }
7460
+ return cell;
7461
+ })
7462
+ );
7463
+ }
7464
+ return nextCol;
7465
+ }
7466
+
7191
7467
  // src/utils/warningsDocument.ts
7192
7468
  function generateWarningsDocument(warnings) {
7193
7469
  if (!warnings || warnings.length === 0) {
@@ -8007,6 +8283,7 @@ export {
8007
8283
  examples,
8008
8284
  exportPluginSchema,
8009
8285
  exportThemeToJson,
8286
+ flattenVisuals,
8010
8287
  formatDate,
8011
8288
  formatWarningsText,
8012
8289
  generateAndSave,
@@ -8041,6 +8318,7 @@ export {
8041
8318
  isSectionComponent,
8042
8319
  isStatisticComponent,
8043
8320
  isTableComponent,
8321
+ isVisualComponent,
8044
8322
  loadJsonExample,
8045
8323
  loadThemeFromFile,
8046
8324
  loadThemeFromJson,