@json-to-office/core-docx 6.0.0 → 6.2.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.
@@ -5870,7 +5870,7 @@ async function rasterizeVisualSlide(presentation, dpi, propsServerUrl, serviceCo
5870
5870
  serviceConfig?.serverUrl,
5871
5871
  DEFAULT_RASTERIZE_SERVER_URL
5872
5872
  );
5873
- const response = await postJsonToService({
5873
+ const body = await postJsonToService({
5874
5874
  url: serverUrl,
5875
5875
  path: "/rasterize",
5876
5876
  body: {
@@ -5882,11 +5882,12 @@ async function rasterizeVisualSlide(presentation, dpi, propsServerUrl, serviceCo
5882
5882
  headers: serviceConfig?.headers,
5883
5883
  serviceLabel: "PPTX rasterization service",
5884
5884
  onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Configure services.pptx with a \`render\` callback or a running \`serverUrl\`.
5885
- Cause: ${cause}`
5885
+ Cause: ${cause}`,
5886
+ decode: (response) => response.text()
5886
5887
  });
5887
5888
  let result;
5888
5889
  try {
5889
- result = await response.json();
5890
+ result = JSON.parse(body);
5890
5891
  } catch {
5891
5892
  throw new Error(
5892
5893
  "PPTX rasterization service returned a non-JSON response (expected { base64DataUri, width, height })."
@@ -6074,9 +6075,9 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
6074
6075
  DEFAULT_RASTERIZE_SERVER_URL
6075
6076
  );
6076
6077
  const postBatch = async (chunk, fonts) => {
6077
- let response;
6078
+ let body;
6078
6079
  try {
6079
- response = await postJsonToService2({
6080
+ body = await postJsonToService2({
6080
6081
  url: serverUrl,
6081
6082
  path: "/rasterize/batch",
6082
6083
  body: {
@@ -6089,14 +6090,21 @@ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
6089
6090
  },
6090
6091
  headers: serviceConfig?.headers,
6091
6092
  timeoutMs: BATCH_TIMEOUT_BASE_MS + BATCH_TIMEOUT_PER_SLIDE_MS * chunk.length,
6093
+ // The per-visual fallback below IS this call's retry, and a better
6094
+ // one: it degrades to smaller work instead of repeating the same
6095
+ // large request. Retrying here first would spend three full batch
6096
+ // timeouts — up to seventeen minutes on a 32-slide chunk — before
6097
+ // the fallback that was going to run anyway gets its turn.
6098
+ retries: 0,
6092
6099
  serviceLabel: "PPTX batch rasterization service",
6093
- onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Cause: ${cause}`
6100
+ onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Cause: ${cause}`,
6101
+ decode: (response) => response.text()
6094
6102
  });
6095
6103
  } catch (error) {
6096
6104
  return { applied: false, schemaRejected: isSchemaRejection(error) };
6097
6105
  }
6098
6106
  try {
6099
- if (applyBatchResponse(chunk, await response.json(), map)) {
6107
+ if (applyBatchResponse(chunk, JSON.parse(body), map)) {
6100
6108
  return { applied: true };
6101
6109
  }
6102
6110
  } catch {
@@ -6203,7 +6211,7 @@ function containsComponent(root, name) {
6203
6211
  import {
6204
6212
  clampVisualDpi as clampVisualDpi2,
6205
6213
  DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI2,
6206
- limitChartRequest
6214
+ recordChartCollected
6207
6215
  } from "@json-to-office/shared";
6208
6216
  import {
6209
6217
  isNativeVisualProps as isNativeVisualProps2
@@ -6222,9 +6230,8 @@ import {
6222
6230
  withChartTypography,
6223
6231
  resolveServiceUrl as resolveServiceUrl3,
6224
6232
  postJsonToService as postJsonToService3,
6225
- chartRequestKey,
6226
- dedupeChartRequest,
6227
- recordChartRetry
6233
+ recordChartRetry,
6234
+ sendChartRequest
6228
6235
  } from "@json-to-office/shared";
6229
6236
  var DEFAULT_EXPORT_SERVER_URL = "http://localhost:7801";
6230
6237
  function effectiveChartServerUrl(props, servicesConfig) {
@@ -6265,14 +6272,16 @@ async function generateChart(config, servicesConfig, warnings, cache) {
6265
6272
  // byte-identical to before for callers that omit it.
6266
6273
  ...config.resources ? { resources: config.resources } : {}
6267
6274
  };
6268
- return dedupeChartRequest(
6275
+ return sendChartRequest({
6269
6276
  cache,
6270
- chartRequestKey(serverUrl, requestBody),
6271
- () => postChart(serverUrl, requestBody, config, servicesConfig)
6272
- );
6277
+ serverUrl,
6278
+ concurrency: servicesConfig?.concurrency,
6279
+ requestBody,
6280
+ send: () => postChart(serverUrl, requestBody, config, servicesConfig)
6281
+ });
6273
6282
  }
6274
6283
  async function postChart(serverUrl, requestBody, config, servicesConfig) {
6275
- const response = await postJsonToService3({
6284
+ const base64Data = await postJsonToService3({
6276
6285
  url: serverUrl,
6277
6286
  path: "/export",
6278
6287
  body: requestBody,
@@ -6282,16 +6291,13 @@ async function postChart(serverUrl, requestBody, config, servicesConfig) {
6282
6291
  onRetry: recordChartRetry,
6283
6292
  serviceLabel: "Highcharts export server",
6284
6293
  onUnreachable: (url, cause) => `Highcharts Export Server is not running at ${url}. Start it with: npx highcharts-export-server --enableServer true
6285
- Cause: ${cause}`
6294
+ Cause: ${cause}`,
6295
+ decode: (response) => response.text()
6286
6296
  });
6287
- const base64Data = await response.text();
6288
- const base64DataUri = `data:image/png;base64,${base64Data}`;
6289
- const width = config.options.chart.width;
6290
- const height = config.options.chart.height;
6291
6297
  return {
6292
- base64DataUri,
6293
- width,
6294
- height
6298
+ base64DataUri: `data:image/png;base64,${base64Data}`,
6299
+ width: config.options.chart.width,
6300
+ height: config.options.chart.height
6295
6301
  };
6296
6302
  }
6297
6303
  function toChartColor(value, theme) {
@@ -6419,21 +6425,16 @@ async function desugarExternals(document, options) {
6419
6425
  });
6420
6426
  }
6421
6427
  if (node.name === "highcharts") {
6422
- const props = node.props;
6423
- const chartConfig = options.services?.highcharts;
6428
+ recordChartCollected();
6424
6429
  return withNodeIdentity(node, {
6425
6430
  name: "image",
6426
- props: await limitChartRequest(
6427
- effectiveChartServerUrl(props, chartConfig),
6428
- chartConfig?.concurrency,
6429
- () => renderChartToImageProps(
6430
- props,
6431
- options.theme,
6432
- chartConfig,
6433
- options.chartFonts,
6434
- options.warnings,
6435
- chartCache
6436
- )
6431
+ props: await renderChartToImageProps(
6432
+ node.props,
6433
+ options.theme,
6434
+ options.services?.highcharts,
6435
+ options.chartFonts,
6436
+ options.warnings,
6437
+ chartCache
6437
6438
  )
6438
6439
  });
6439
6440
  }
@@ -12969,6 +12970,7 @@ async function expandBlocksWithPlugins(document, theme, plugins, render, preserv
12969
12970
  init_styleHelpers();
12970
12971
  init_defaults();
12971
12972
  init_widthUtils();
12973
+ import probe2 from "probe-image-size";
12972
12974
 
12973
12975
  // src/quality/text-inventory.ts
12974
12976
  function tocDepth(depth) {
@@ -13219,6 +13221,92 @@ function collectDocxTextInventory(children, basePath = "/children") {
13219
13221
  return entries;
13220
13222
  }
13221
13223
 
13224
+ // src/quality/text-metrics.ts
13225
+ var DEFAULT_ADVANCE = 556;
13226
+ var ADVANCE = {
13227
+ " ": 278,
13228
+ A: 667,
13229
+ B: 667,
13230
+ C: 722,
13231
+ D: 722,
13232
+ E: 667,
13233
+ F: 611,
13234
+ G: 778,
13235
+ H: 722,
13236
+ I: 278,
13237
+ J: 500,
13238
+ K: 667,
13239
+ L: 556,
13240
+ M: 833,
13241
+ N: 722,
13242
+ O: 778,
13243
+ P: 667,
13244
+ Q: 778,
13245
+ R: 722,
13246
+ S: 667,
13247
+ T: 611,
13248
+ U: 722,
13249
+ V: 667,
13250
+ W: 944,
13251
+ X: 667,
13252
+ Y: 667,
13253
+ Z: 611,
13254
+ a: 556,
13255
+ b: 556,
13256
+ c: 500,
13257
+ d: 556,
13258
+ e: 556,
13259
+ f: 278,
13260
+ g: 556,
13261
+ h: 556,
13262
+ i: 222,
13263
+ j: 222,
13264
+ k: 500,
13265
+ l: 222,
13266
+ m: 833,
13267
+ n: 556,
13268
+ o: 556,
13269
+ p: 556,
13270
+ q: 556,
13271
+ r: 333,
13272
+ s: 500,
13273
+ t: 278,
13274
+ u: 556,
13275
+ v: 500,
13276
+ w: 722,
13277
+ x: 500,
13278
+ y: 500,
13279
+ z: 500,
13280
+ ".": 278,
13281
+ ",": 278,
13282
+ ":": 278,
13283
+ ";": 278,
13284
+ "!": 278,
13285
+ "?": 556,
13286
+ "'": 191,
13287
+ '"': 355,
13288
+ "-": 333,
13289
+ "\u2013": 556,
13290
+ "\u2014": 1e3,
13291
+ "&": 667,
13292
+ "%": 889,
13293
+ "(": 333,
13294
+ ")": 333,
13295
+ "/": 278,
13296
+ "@": 1015,
13297
+ "#": 556,
13298
+ "+": 584,
13299
+ "=": 584
13300
+ };
13301
+ function estimateTextWidthPt(text, fontSizePt, trackingPt = 0) {
13302
+ let units = 0;
13303
+ for (const character of text) {
13304
+ units += ADVANCE[character] ?? DEFAULT_ADVANCE;
13305
+ }
13306
+ const width = units / 1e3 * fontSizePt + text.length * trackingPt;
13307
+ return Math.max(0, width);
13308
+ }
13309
+
13222
13310
  // src/quality/facts.ts
13223
13311
  function asRecord2(value) {
13224
13312
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
@@ -14030,14 +14118,29 @@ function prepareDocxQualityDocument(document, options = {}) {
14030
14118
  }
14031
14119
  if (node.name === "image" || node.name === "visual") {
14032
14120
  for (const fact of svgTextFacts(props, path3)) addFact(fact);
14121
+ const drawn = drawnRatio(props);
14122
+ const natural = readableRatio(props);
14123
+ addFact({
14124
+ id: `docx:image:${path3}`,
14125
+ kind: "docx/image",
14126
+ path: path3,
14127
+ ...typeof props.alt === "string" && props.alt.trim() !== "" && { alt: props.alt },
14128
+ captioned: isCaptioned(facts, authoredPath(path3)),
14129
+ ...drawn !== void 0 && { drawnRatio: drawn },
14130
+ ...natural !== void 0 && { naturalRatio: natural }
14131
+ });
14033
14132
  }
14034
14133
  if (node.name === "heading") {
14035
14134
  const level = typeof props.level === "number" && Number.isFinite(props.level) ? props.level : 1;
14135
+ const styleKeepNext = asRecord2(
14136
+ typography.styles[`heading${level}`]
14137
+ )?.keepNext;
14036
14138
  addFact({
14037
14139
  id: `docx:heading:${path3}`,
14038
14140
  kind: "docx/heading",
14039
14141
  path: `${path3}/props/level`,
14040
14142
  level,
14143
+ keepNext: props.keepNext === true || props.keepNext === void 0 && styleKeepNext === true,
14041
14144
  ...previousHeadingLevel !== void 0 && {
14042
14145
  previousLevel: previousHeadingLevel
14043
14146
  }
@@ -14052,6 +14155,65 @@ function prepareDocxQualityDocument(document, options = {}) {
14052
14155
  const page = rec.name === "section" ? pageBox(resolved.theme, context.themeName, rec.props?.page) : basePage;
14053
14156
  walkActive(component, `/children/${index}`, page, visit);
14054
14157
  });
14158
+ const wordsOf = (text) => text.split(/\s+/).filter(Boolean).length;
14159
+ const bodyRoles = /* @__PURE__ */ new Set([
14160
+ "body",
14161
+ "list-item",
14162
+ "table-header",
14163
+ "table-cell",
14164
+ "statistic",
14165
+ "caption"
14166
+ ]);
14167
+ const exhibitKinds = /* @__PURE__ */ new Set(["docx/table", "docx/chart", "docx/image"]);
14168
+ resolved.children.forEach((component, index) => {
14169
+ const rec = component;
14170
+ if (rec.name !== "section") return;
14171
+ const prefix = `/children/${index}/`;
14172
+ const own = facts.filter((fact) => fact.path.startsWith(prefix));
14173
+ const texts = own.filter(
14174
+ (fact) => fact.kind === "docx/text"
14175
+ );
14176
+ addFact({
14177
+ id: `docx:section:${index}`,
14178
+ kind: "docx/section",
14179
+ path: `/children/${index}`,
14180
+ index,
14181
+ words: texts.filter((fact) => bodyRoles.has(fact.role)).reduce((total, fact) => total + wordsOf(fact.text), 0),
14182
+ exhibits: own.filter((fact) => exhibitKinds.has(fact.kind)).length,
14183
+ headings: texts.filter((fact) => fact.role === "heading").length
14184
+ });
14185
+ if (hasColumnsComponent(rec)) return;
14186
+ const page = pageBox(
14187
+ resolved.theme,
14188
+ context.themeName,
14189
+ rec.props?.page
14190
+ );
14191
+ const fontSizePt = effectiveFontSize({ name: "paragraph" }, {}, typography)?.fontSizePt ?? 11;
14192
+ const widthPt = page.availableWidthTwips / 20;
14193
+ const averageAdvancePt = estimateTextWidthPt(MEASURE_SAMPLE, fontSizePt) / MEASURE_SAMPLE.length;
14194
+ if (averageAdvancePt > 0)
14195
+ addFact({
14196
+ id: `docx:measure:${index}`,
14197
+ kind: "docx/measure",
14198
+ path: `/children/${index}`,
14199
+ index,
14200
+ charactersPerLine: Math.round(widthPt / averageAdvancePt),
14201
+ widthTwips: page.availableWidthTwips,
14202
+ fontSizePt
14203
+ });
14204
+ });
14205
+ const headingCount = facts.filter(
14206
+ (fact) => fact.kind === "docx/text" && fact.role === "heading"
14207
+ ).length;
14208
+ addFact({
14209
+ id: "docx:outline",
14210
+ kind: "docx/outline",
14211
+ path: "/props",
14212
+ headings: headingCount,
14213
+ contents: facts.some(
14214
+ (fact) => fact.kind === "docx/text" && fact.role === "toc-entry"
14215
+ )
14216
+ });
14055
14217
  return {
14056
14218
  format: "docx",
14057
14219
  model: {
@@ -14082,6 +14244,52 @@ function prepareDocxQualityDocument(document, options = {}) {
14082
14244
  }
14083
14245
  };
14084
14246
  }
14247
+ function hasColumnsComponent(node) {
14248
+ if (Array.isArray(node)) return node.some(hasColumnsComponent);
14249
+ const rec = asRecord2(node);
14250
+ if (!rec) return false;
14251
+ if (rec.name === "columns") return true;
14252
+ return Object.values(rec).some(hasColumnsComponent);
14253
+ }
14254
+ var CAPTION_LABEL = /^\s*(?:\*\*)?\s*(figure|exhibit|table|chart|image)\b/i;
14255
+ function isCaptioned(facts, image) {
14256
+ return facts.some((fact) => {
14257
+ if (fact.kind !== "docx/text") return false;
14258
+ const text = fact;
14259
+ const near = siblingOf(text.path) === siblingOf(image) || text.path.startsWith(`${image}/`) || image.startsWith(`${text.path}/`);
14260
+ return near && (text.role === "caption" || CAPTION_LABEL.test(text.text));
14261
+ });
14262
+ }
14263
+ function siblingOf(pointer) {
14264
+ return pointer.slice(0, pointer.lastIndexOf("/"));
14265
+ }
14266
+ var MEASURE_SAMPLE = "the quick brown fox jumps over the lazy dog and then settles under it ";
14267
+ function drawnRatio(props) {
14268
+ const width = finiteNumber(props.width);
14269
+ const height = finiteNumber(props.height);
14270
+ return width !== void 0 && height !== void 0 && height > 0 ? width / height : void 0;
14271
+ }
14272
+ function readableRatio(props) {
14273
+ if (typeof props.svg === "string") {
14274
+ const box = /viewBox\s*=\s*["']\s*[-\d.]+[ ,]+[-\d.]+[ ,]+([\d.]+)[ ,]+([\d.]+)/.exec(
14275
+ props.svg
14276
+ );
14277
+ const width = box ? Number(box[1]) : NaN;
14278
+ const height = box ? Number(box[2]) : NaN;
14279
+ return Number.isFinite(width) && Number.isFinite(height) && height > 0 ? width / height : void 0;
14280
+ }
14281
+ if (typeof props.base64 !== "string") return void 0;
14282
+ const comma = props.base64.indexOf(",");
14283
+ if (comma < 0) return void 0;
14284
+ try {
14285
+ const size = probe2.sync(
14286
+ Buffer.from(props.base64.slice(comma + 1), "base64")
14287
+ );
14288
+ return size && size.height > 0 ? size.width / size.height : void 0;
14289
+ } catch {
14290
+ return void 0;
14291
+ }
14292
+ }
14085
14293
  function slotIsFilled(value) {
14086
14294
  if (typeof value === "string") return value.trim() !== "";
14087
14295
  return value !== void 0 && value !== null && value !== false && (!Array.isArray(value) || value.length > 0);