@json-to-office/core-pptx 1.3.0 → 1.5.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
@@ -39,6 +39,7 @@ var init_warn = __esm({
39
39
  UNKNOWN_COLOR: "UNKNOWN_COLOR",
40
40
  GRID_POSITION_CLAMPED: "GRID_POSITION_CLAMPED",
41
41
  TEXT_NO_CONTENT: "TEXT_NO_CONTENT",
42
+ TEXT_OVERLAP_UNPOSITIONED: "TEXT_OVERLAP_UNPOSITIONED",
42
43
  UNKNOWN_PATTERN_PRESET: "UNKNOWN_PATTERN_PRESET",
43
44
  ADVANCED_FILL_FALLBACK: "ADVANCED_FILL_FALLBACK",
44
45
  IMAGE_ZERO_BOX: "IMAGE_ZERO_BOX",
@@ -122,147 +123,6 @@ var init_units = __esm({
122
123
  }
123
124
  });
124
125
 
125
- // src/ir/features.ts
126
- var PPTX_FEATURES, ALL_PPTX_FEATURES;
127
- var init_features = __esm({
128
- "src/ir/features.ts"() {
129
- "use strict";
130
- PPTX_FEATURES = [
131
- /** Slide masters compiled from authored templates. */
132
- "masters",
133
- /** Named placeholder regions declared by a master. */
134
- "placeholders",
135
- /** Multiple independently-formatted runs inside one text body. */
136
- "rich-text",
137
- /**
138
- * Bullet glyphs outside PptxGenJS's single-BMP-character API: astral
139
- * characters and multi-code-point graphemes.
140
- */
141
- "complex-bullet-glyphs",
142
- /** Text bodies at all (a single run, uniform formatting). */
143
- "text",
144
- /** Preset-geometry shapes. */
145
- "shapes",
146
- /** Raster and vector images. */
147
- "images",
148
- /** SVG images specifically — many backends need a raster fallback. */
149
- "svg",
150
- /**
151
- * Drawing only part of a picture: `cover`, `crop`, or a `contain` fit the
152
- * compiler could not resolve into the frame.
153
- *
154
- * A backend without it draws the whole picture into the frame, which is a
155
- * different image — the wrong region at the wrong scale — with nothing to
156
- * show that anything was lost.
157
- */
158
- "image-crop",
159
- /** Masking a picture to rounded corners or a circle. */
160
- "image-rounding",
161
- "tables",
162
- /** Merged table cells (colSpan / rowSpan). */
163
- "table-merged-cells",
164
- /**
165
- * Cell insets on a table.
166
- *
167
- * Separate from the inset on a text body: a reader takes a cell's padding
168
- * from `a:tcPr`, and a backend that can only write text-body insets puts the
169
- * numbers somewhere nothing reads them.
170
- */
171
- "table-insets",
172
- /**
173
- * Rounded table corners.
174
- *
175
- * A separate ability because OOXML has no such property: a backend realises
176
- * it by drawing shapes behind the table, or it cannot realise it at all.
177
- */
178
- "table-rounded-corners",
179
- /**
180
- * Flowing an over-long table onto further slides, header row included.
181
- *
182
- * Pagination is the backend's, not the IR's — the IR describes one table and
183
- * says it may split, so a backend that cannot split has to refuse rather than
184
- * emit a table running off the slide.
185
- */
186
- "table-auto-page",
187
- /** Native OOXML charts with an embedded workbook. */
188
- "charts",
189
- /*
190
- * Chart styling, split finer than `charts` itself.
191
- *
192
- * `charts` says a backend can draw a chart from data. It says nothing about
193
- * whether that backend honours the options the author styled it with, and one
194
- * coarse feature let a renderer accept a chart, draw it, and quietly ignore
195
- * half of what was asked for — an ignored axis maximum draws a different
196
- * chart from the authored one and the file says nothing about it.
197
- *
198
- * Each name below is required by the compiler *only when the matching prop
199
- * was authored*, at that prop's own path, so a default never demands a
200
- * capability and a refusal always names the line that caused it.
201
- */
202
- /** `barGapWidthPct`, `barOverlapPct`. */
203
- "chart-bar-style",
204
- /** `firstSliceAng`, `holeSize`. */
205
- "chart-pie-style",
206
- /** `lineSmooth`, `lineDataSymbol`, `lineSize`, `lineDataSymbolSize`. */
207
- "chart-line-style",
208
- /** `radarStyle` — anything but the `standard` a backend may hardcode. */
209
- "chart-radar-style",
210
- /** `showValue`, `showPercent`, `showLabel`, `showSerName`, `dataLabelPosition`. */
211
- "chart-data-labels",
212
- /** `dataBorder`: an outline on bars, slices and areas. */
213
- "chart-data-border",
214
- /** Value-axis bounds and number format: min, max, major unit, format code. */
215
- "chart-axis-scale",
216
- /** Hiding an axis or its line. */
217
- "chart-axis-visibility",
218
- /** Axis label rotation and grid lines. */
219
- "chart-axis-style",
220
- /** Font family, size, weight and colour on any chart text. */
221
- "chart-text-style",
222
- "solid-fills",
223
- "gradient-fills",
224
- "pattern-fills",
225
- "image-fills",
226
- "lines",
227
- "shadows",
228
- /** Slide backgrounds (solid or image). */
229
- "backgrounds",
230
- "speaker-notes",
231
- "hidden-slides",
232
- "transitions",
233
- /** Hyperlinks to an external URL. */
234
- "external-links",
235
- /** Hyperlinks targeting another slide in the same deck. */
236
- "internal-links",
237
- /** A link covering a text body — realisable on the runs inside it. */
238
- "text-hyperlinks",
239
- /** A link on a shape or image as a whole, which runs cannot stand in for. */
240
- "element-hyperlinks",
241
- /** Rotation on a shape, text box or table. */
242
- "rotation",
243
- /**
244
- * Any transform on a picture — rotation or flip.
245
- *
246
- * One feature rather than three: a backend that cannot rotate a picture
247
- * generally cannot flip one either, because the option carrying both is
248
- * simply absent from its picture type.
249
- */
250
- "image-transform",
251
- "flip-horizontal",
252
- "flip-vertical",
253
- /** Grouped elements sharing a transform. */
254
- "groups",
255
- /** Per-run proofing language. */
256
- "proofing-language",
257
- /** Right-to-left reading order. */
258
- "rtl"
259
- ];
260
- ALL_PPTX_FEATURES = new Set(
261
- PPTX_FEATURES
262
- );
263
- }
264
- });
265
-
266
126
  // src/renderers/pptxgenjs/fills.ts
267
127
  function colorXml(color2) {
268
128
  const alpha = color2.transparency !== void 0 ? `<a:alpha val="${Math.round((100 - color2.transparency) * PCT_UNIT)}"/>` : "";
@@ -1363,16 +1223,55 @@ var PPTXGENJS_RENDERER_ID, PPTXGENJS_CAPABILITIES;
1363
1223
  var init_pptxgenjs = __esm({
1364
1224
  "src/renderers/pptxgenjs/index.ts"() {
1365
1225
  "use strict";
1366
- init_features();
1367
1226
  init_units();
1368
1227
  init_emit();
1369
1228
  init_packaging();
1370
1229
  PPTXGENJS_RENDERER_ID = "pptxgenjs";
1371
- PPTXGENJS_CAPABILITIES = new Set(
1372
- [...ALL_PPTX_FEATURES].filter(
1373
- (feature) => feature !== "image-fills" && feature !== "transitions" && feature !== "groups" && feature !== "complex-bullet-glyphs"
1374
- )
1375
- );
1230
+ PPTXGENJS_CAPABILITIES = /* @__PURE__ */ new Set([
1231
+ "masters",
1232
+ "placeholders",
1233
+ "rich-text",
1234
+ "text",
1235
+ "shapes",
1236
+ "images",
1237
+ "svg",
1238
+ "image-crop",
1239
+ "image-rounding",
1240
+ "tables",
1241
+ "table-merged-cells",
1242
+ "table-insets",
1243
+ "table-rounded-corners",
1244
+ "table-auto-page",
1245
+ "charts",
1246
+ "chart-bar-style",
1247
+ "chart-pie-style",
1248
+ "chart-line-style",
1249
+ "chart-radar-style",
1250
+ "chart-data-labels",
1251
+ "chart-data-border",
1252
+ "chart-axis-scale",
1253
+ "chart-axis-visibility",
1254
+ "chart-axis-style",
1255
+ "chart-text-style",
1256
+ "solid-fills",
1257
+ "gradient-fills",
1258
+ "pattern-fills",
1259
+ "lines",
1260
+ "shadows",
1261
+ "backgrounds",
1262
+ "speaker-notes",
1263
+ "hidden-slides",
1264
+ "external-links",
1265
+ "internal-links",
1266
+ "text-hyperlinks",
1267
+ "element-hyperlinks",
1268
+ "rotation",
1269
+ "image-transform",
1270
+ "flip-horizontal",
1271
+ "flip-vertical",
1272
+ "proofing-language",
1273
+ "rtl"
1274
+ ]);
1376
1275
  }
1377
1276
  });
1378
1277
 
@@ -2150,33 +2049,52 @@ async function loadResourceBytes(resources) {
2150
2049
  );
2151
2050
  return new Map(entries);
2152
2051
  }
2153
- var OFFICE_OPEN_PPTX_RENDERER_ID, OFFICE_OPEN_PPTX, UNSUPPORTED, OFFICE_OPEN_CAPABILITIES, SCHEME_SLOTS;
2052
+ var OFFICE_OPEN_PPTX_RENDERER_ID, OFFICE_OPEN_PPTX, OFFICE_OPEN_CAPABILITIES, SCHEME_SLOTS;
2154
2053
  var init_office_open = __esm({
2155
2054
  "src/renderers/office-open/index.ts"() {
2156
2055
  "use strict";
2157
2056
  init_finalizePackage();
2158
2057
  init_chartParts();
2159
- init_features();
2160
2058
  init_emit2();
2161
2059
  OFFICE_OPEN_PPTX_RENDERER_ID = "office-open";
2162
2060
  OFFICE_OPEN_PPTX = "@office-open/pptx";
2163
- UNSUPPORTED = /* @__PURE__ */ new Set([
2164
- "svg",
2165
- "image-transform",
2166
- "image-crop",
2167
- "image-rounding",
2168
- "flip-vertical",
2169
- "element-hyperlinks",
2170
- "masters",
2171
- "placeholders",
2172
- "table-merged-cells",
2173
- "table-rounded-corners",
2174
- "table-auto-page",
2175
- "table-insets"
2061
+ OFFICE_OPEN_CAPABILITIES = /* @__PURE__ */ new Set([
2062
+ "rich-text",
2063
+ "complex-bullet-glyphs",
2064
+ "text",
2065
+ "shapes",
2066
+ "images",
2067
+ "tables",
2068
+ "charts",
2069
+ "chart-bar-style",
2070
+ "chart-pie-style",
2071
+ "chart-line-style",
2072
+ "chart-radar-style",
2073
+ "chart-data-labels",
2074
+ "chart-data-border",
2075
+ "chart-axis-scale",
2076
+ "chart-axis-visibility",
2077
+ "chart-axis-style",
2078
+ "chart-text-style",
2079
+ "solid-fills",
2080
+ "gradient-fills",
2081
+ "pattern-fills",
2082
+ "image-fills",
2083
+ "lines",
2084
+ "shadows",
2085
+ "backgrounds",
2086
+ "speaker-notes",
2087
+ "hidden-slides",
2088
+ "transitions",
2089
+ "external-links",
2090
+ "internal-links",
2091
+ "text-hyperlinks",
2092
+ "rotation",
2093
+ "flip-horizontal",
2094
+ "groups",
2095
+ "proofing-language",
2096
+ "rtl"
2176
2097
  ]);
2177
- OFFICE_OPEN_CAPABILITIES = new Set(
2178
- [...ALL_PPTX_FEATURES].filter((feature) => !UNSUPPORTED.has(feature))
2179
- );
2180
2098
  SCHEME_SLOTS = [
2181
2099
  ["dark1", "text"],
2182
2100
  ["light1", "background"],
@@ -2317,99 +2235,6 @@ function applyFontWeight(params) {
2317
2235
  return { fontFace: synth.family, bold: synth.bold, italic: synth.italic };
2318
2236
  }
2319
2237
 
2320
- // src/utils/componentDefaults.ts
2321
- import { mergeWithDefaults } from "@json-to-office/shared";
2322
- function getComponentDefaults(theme) {
2323
- return theme.componentDefaults || {};
2324
- }
2325
- function getTextDefaults(theme) {
2326
- return getComponentDefaults(theme).text || {};
2327
- }
2328
- function getImageDefaults(theme) {
2329
- return getComponentDefaults(theme).image || {};
2330
- }
2331
- function getShapeDefaults(theme) {
2332
- return getComponentDefaults(theme).shape || {};
2333
- }
2334
- function getTableDefaults(theme) {
2335
- return getComponentDefaults(theme).table || {};
2336
- }
2337
- function getHighchartsDefaults(theme) {
2338
- return getComponentDefaults(theme).highcharts || {};
2339
- }
2340
- function getChartDefaults(theme) {
2341
- return getComponentDefaults(theme).chart || {};
2342
- }
2343
- function getCustomComponentDefaults(theme, componentName) {
2344
- const defaults = getComponentDefaults(theme);
2345
- return defaults?.[componentName] || {};
2346
- }
2347
- function resolveTextProps(props, theme) {
2348
- return mergeWithDefaults(props, getTextDefaults(theme));
2349
- }
2350
- function resolveImageProps(props, theme) {
2351
- return mergeWithDefaults(props, getImageDefaults(theme));
2352
- }
2353
- function resolveShapeProps(props, theme) {
2354
- return mergeWithDefaults(props, getShapeDefaults(theme));
2355
- }
2356
- function resolveTableProps(props, theme) {
2357
- return mergeWithDefaults(props, getTableDefaults(theme));
2358
- }
2359
- function resolveHighchartsProps(props, theme) {
2360
- return mergeWithDefaults(props, getHighchartsDefaults(theme));
2361
- }
2362
- function resolveChartProps(props, theme) {
2363
- return mergeWithDefaults(props, getChartDefaults(theme));
2364
- }
2365
- function resolveCustomComponentProps(props, theme, componentName) {
2366
- const defaults = getCustomComponentDefaults(theme, componentName);
2367
- return mergeWithDefaults(props, defaults);
2368
- }
2369
- var TYPE_GETTERS = {
2370
- text: getTextDefaults,
2371
- image: getImageDefaults,
2372
- shape: getShapeDefaults,
2373
- table: getTableDefaults,
2374
- highcharts: getHighchartsDefaults,
2375
- chart: getChartDefaults
2376
- };
2377
- function getDefaultsForType(componentName, theme) {
2378
- const getter = TYPE_GETTERS[componentName];
2379
- return getter ? getter(theme) : getCustomComponentDefaults(theme, componentName);
2380
- }
2381
-
2382
- // src/utils/resolveComponentTree.ts
2383
- var RESOLVER_MAP = {
2384
- text: resolveTextProps,
2385
- image: resolveImageProps,
2386
- shape: resolveShapeProps,
2387
- table: resolveTableProps,
2388
- highcharts: resolveHighchartsProps,
2389
- chart: resolveChartProps
2390
- };
2391
- function resolveComponentDefaults(component, theme) {
2392
- const resolver = RESOLVER_MAP[component.name];
2393
- const resolvedProps = resolver ? resolver(component.props, theme) : resolveCustomComponentProps(
2394
- component.props,
2395
- theme,
2396
- component.name
2397
- );
2398
- return { ...component, props: resolvedProps };
2399
- }
2400
- function resolveComponentTree(components, theme) {
2401
- return components.map((component) => {
2402
- const resolved = resolveComponentDefaults(component, theme);
2403
- if (resolved.children && resolved.children.length > 0) {
2404
- return {
2405
- ...resolved,
2406
- children: resolveComponentTree(resolved.children, theme)
2407
- };
2408
- }
2409
- return resolved;
2410
- });
2411
- }
2412
-
2413
2238
  // src/utils/imageSource.ts
2414
2239
  import path from "path";
2415
2240
  var hasValue = (v) => typeof v === "string" && v.trim().length > 0;
@@ -2482,7 +2307,8 @@ var DEFAULT_GRID_CONFIG = {
2482
2307
  };
2483
2308
  function resolveMargin(margin) {
2484
2309
  if (margin == null) return DEFAULT_GRID_CONFIG.margin;
2485
- if (typeof margin === "number") return { top: margin, right: margin, bottom: margin, left: margin };
2310
+ if (typeof margin === "number")
2311
+ return { top: margin, right: margin, bottom: margin, left: margin };
2486
2312
  return margin;
2487
2313
  }
2488
2314
  function resolveGutter(gutter) {
@@ -2546,36 +2372,210 @@ function resolveGridPosition(gridPos, gridConfig, slideWidth, slideHeight, warni
2546
2372
  function resolveComponentGridPosition(component, gridConfig, slideWidth, slideHeight, warnings) {
2547
2373
  const gridPos = component.props.grid;
2548
2374
  if (!gridPos) return component;
2549
- const resolved = resolveGridPosition(gridPos, gridConfig, slideWidth, slideHeight, warnings);
2375
+ const resolved = resolveGridPosition(
2376
+ gridPos,
2377
+ gridConfig,
2378
+ slideWidth,
2379
+ slideHeight,
2380
+ warnings
2381
+ );
2550
2382
  const { grid: _grid, ...restProps } = component.props;
2551
2383
  const newProps = { ...restProps };
2552
2384
  const hasPercentX = typeof newProps.x === "string" || typeof newProps.w === "string";
2553
2385
  const hasPercentY = typeof newProps.y === "string" || typeof newProps.h === "string";
2554
2386
  const toPercX = (v) => `${+(v / slideWidth * 100).toFixed(2)}%`;
2555
2387
  const toPercY = (v) => `${+(v / slideHeight * 100).toFixed(2)}%`;
2556
- if (newProps.x == null) newProps.x = hasPercentX ? toPercX(resolved.x) : resolved.x;
2557
- if (newProps.y == null) newProps.y = hasPercentY ? toPercY(resolved.y) : resolved.y;
2558
- if (newProps.w == null) newProps.w = hasPercentX ? toPercX(resolved.w) : resolved.w;
2559
- if (newProps.h == null) newProps.h = hasPercentY ? toPercY(resolved.h) : resolved.h;
2388
+ if (newProps.x == null)
2389
+ newProps.x = hasPercentX ? toPercX(resolved.x) : resolved.x;
2390
+ if (newProps.y == null)
2391
+ newProps.y = hasPercentY ? toPercY(resolved.y) : resolved.y;
2392
+ if (newProps.w == null)
2393
+ newProps.w = hasPercentX ? toPercX(resolved.w) : resolved.w;
2394
+ if (newProps.h == null)
2395
+ newProps.h = hasPercentY ? toPercY(resolved.h) : resolved.h;
2560
2396
  return { ...component, props: newProps };
2561
2397
  }
2562
2398
 
2563
2399
  // src/ir/compiler.ts
2564
2400
  init_warn();
2401
+
2402
+ // src/core/placeholders.ts
2565
2403
  import { mergeWithDefaults as mergeWithDefaults2 } from "@json-to-office/shared";
2566
2404
 
2567
- // src/ir/resources.ts
2568
- init_units();
2569
- var ResourceTable = class {
2570
- byKey = /* @__PURE__ */ new Map();
2571
- order = [];
2572
- /**
2573
- * Add a resource (or return the existing one with matching identity) and
2574
- * return its id.
2575
- */
2576
- intern(input, intrinsic) {
2577
- const origin = toOrigin(input);
2578
- const key = identity(origin);
2405
+ // src/utils/componentDefaults.ts
2406
+ import { mergeWithDefaults } from "@json-to-office/shared";
2407
+ function getComponentDefaults(theme) {
2408
+ return theme.componentDefaults || {};
2409
+ }
2410
+ function getTextDefaults(theme) {
2411
+ return getComponentDefaults(theme).text || {};
2412
+ }
2413
+ function getImageDefaults(theme) {
2414
+ return getComponentDefaults(theme).image || {};
2415
+ }
2416
+ function getShapeDefaults(theme) {
2417
+ return getComponentDefaults(theme).shape || {};
2418
+ }
2419
+ function getTableDefaults(theme) {
2420
+ return getComponentDefaults(theme).table || {};
2421
+ }
2422
+ function getHighchartsDefaults(theme) {
2423
+ return getComponentDefaults(theme).highcharts || {};
2424
+ }
2425
+ function getChartDefaults(theme) {
2426
+ return getComponentDefaults(theme).chart || {};
2427
+ }
2428
+ function getCustomComponentDefaults(theme, componentName) {
2429
+ const defaults = getComponentDefaults(theme);
2430
+ return defaults?.[componentName] || {};
2431
+ }
2432
+ function resolveTextProps(props, theme) {
2433
+ return mergeWithDefaults(props, getTextDefaults(theme));
2434
+ }
2435
+ function resolveImageProps(props, theme) {
2436
+ return mergeWithDefaults(props, getImageDefaults(theme));
2437
+ }
2438
+ function resolveShapeProps(props, theme) {
2439
+ return mergeWithDefaults(props, getShapeDefaults(theme));
2440
+ }
2441
+ function resolveTableProps(props, theme) {
2442
+ return mergeWithDefaults(props, getTableDefaults(theme));
2443
+ }
2444
+ function resolveHighchartsProps(props, theme) {
2445
+ return mergeWithDefaults(props, getHighchartsDefaults(theme));
2446
+ }
2447
+ function resolveChartProps(props, theme) {
2448
+ return mergeWithDefaults(props, getChartDefaults(theme));
2449
+ }
2450
+ function resolveCustomComponentProps(props, theme, componentName) {
2451
+ const defaults = getCustomComponentDefaults(theme, componentName);
2452
+ return mergeWithDefaults(props, defaults);
2453
+ }
2454
+ var TYPE_GETTERS = {
2455
+ text: getTextDefaults,
2456
+ image: getImageDefaults,
2457
+ shape: getShapeDefaults,
2458
+ table: getTableDefaults,
2459
+ highcharts: getHighchartsDefaults,
2460
+ chart: getChartDefaults
2461
+ };
2462
+ function getDefaultsForType(componentName, theme) {
2463
+ const getter = TYPE_GETTERS[componentName];
2464
+ return getter ? getter(theme) : getCustomComponentDefaults(theme, componentName);
2465
+ }
2466
+
2467
+ // src/utils/resolveComponentTree.ts
2468
+ var RESOLVER_MAP = {
2469
+ text: resolveTextProps,
2470
+ image: resolveImageProps,
2471
+ shape: resolveShapeProps,
2472
+ table: resolveTableProps,
2473
+ highcharts: resolveHighchartsProps,
2474
+ chart: resolveChartProps
2475
+ };
2476
+ function resolveComponentDefaults(component, theme) {
2477
+ const resolver = RESOLVER_MAP[component.name];
2478
+ const resolvedProps = resolver ? resolver(component.props, theme) : resolveCustomComponentProps(
2479
+ component.props,
2480
+ theme,
2481
+ component.name
2482
+ );
2483
+ return { ...component, props: resolvedProps };
2484
+ }
2485
+ function resolveComponentTree(components, theme) {
2486
+ return components.map((component) => {
2487
+ const resolved = resolveComponentDefaults(component, theme);
2488
+ if (resolved.children && resolved.children.length > 0) {
2489
+ return {
2490
+ ...resolved,
2491
+ children: resolveComponentTree(resolved.children, theme)
2492
+ };
2493
+ }
2494
+ return resolved;
2495
+ });
2496
+ }
2497
+
2498
+ // src/core/placeholders.ts
2499
+ init_warn();
2500
+ function resolvePlaceholderComponents(slide, template, effectiveGrid, options) {
2501
+ if (!slide.placeholders) return [];
2502
+ const out = [];
2503
+ if (!template) {
2504
+ for (const [name, component] of Object.entries(slide.placeholders)) {
2505
+ const defaulted = resolveComponentDefaults(component, options.theme);
2506
+ const positioned = defaulted.props.x != null || defaulted.props.y != null || defaulted.props.grid;
2507
+ if (!positioned) {
2508
+ warn(
2509
+ options.warnings,
2510
+ W.PLACEHOLDER_NO_POSITION,
2511
+ `Placeholder "${name}" has no template and no explicit position \u2014 skipped`,
2512
+ { slide: options.slideIndex }
2513
+ );
2514
+ continue;
2515
+ }
2516
+ out.push({
2517
+ name,
2518
+ component: resolveComponentGridPosition(
2519
+ defaulted,
2520
+ effectiveGrid,
2521
+ options.slideWidth,
2522
+ options.slideHeight,
2523
+ options.warnings
2524
+ )
2525
+ });
2526
+ }
2527
+ return out;
2528
+ }
2529
+ const declared = new Map(
2530
+ (template.placeholders ?? []).map((placeholder) => [
2531
+ placeholder.name,
2532
+ placeholder
2533
+ ])
2534
+ );
2535
+ for (const [name, component] of Object.entries(slide.placeholders)) {
2536
+ const definition = declared.get(name);
2537
+ if (!definition) {
2538
+ warn(
2539
+ options.warnings,
2540
+ W.UNKNOWN_PLACEHOLDER,
2541
+ `Unknown placeholder "${name}" in template "${slide.template}". Available: ${[...declared.keys()].join(", ")}`,
2542
+ { slide: options.slideIndex }
2543
+ );
2544
+ continue;
2545
+ }
2546
+ const gridResolved = resolveComponentGridPosition(
2547
+ component,
2548
+ effectiveGrid,
2549
+ options.slideWidth,
2550
+ options.slideHeight,
2551
+ options.warnings
2552
+ );
2553
+ const typeDefaults = getDefaultsForType(component.name, options.theme);
2554
+ const positionDefaults = {};
2555
+ if (definition.x != null) positionDefaults.x = definition.x;
2556
+ if (definition.y != null) positionDefaults.y = definition.y;
2557
+ if (definition.w != null) positionDefaults.w = definition.w;
2558
+ if (definition.h != null) positionDefaults.h = definition.h;
2559
+ let props = mergeWithDefaults2(positionDefaults, typeDefaults);
2560
+ props = mergeWithDefaults2(definition.defaults?.props ?? {}, props);
2561
+ props = mergeWithDefaults2(gridResolved.props, props);
2562
+ out.push({ name, component: { ...gridResolved, props } });
2563
+ }
2564
+ return out;
2565
+ }
2566
+
2567
+ // src/ir/resources.ts
2568
+ init_units();
2569
+ var ResourceTable = class {
2570
+ byKey = /* @__PURE__ */ new Map();
2571
+ order = [];
2572
+ /**
2573
+ * Add a resource (or return the existing one with matching identity) and
2574
+ * return its id.
2575
+ */
2576
+ intern(input, intrinsic) {
2577
+ const origin = toOrigin(input);
2578
+ const key = identity(origin);
2579
2579
  const existing = this.byKey.get(key);
2580
2580
  if (existing) {
2581
2581
  if (!existing.intrinsic && intrinsic) existing.intrinsic = intrinsic;
@@ -2673,7 +2673,8 @@ function compilePresentation(processed, warnings = []) {
2673
2673
  extent: {
2674
2674
  widthEmu: inchesToEmu(processed.slideWidth),
2675
2675
  heightEmu: inchesToEmu(processed.slideHeight)
2676
- }
2676
+ },
2677
+ positionlessText: []
2677
2678
  };
2678
2679
  const masters = (processed.templates ?? []).map(
2679
2680
  (template, index) => compileMaster(template, index, processed, ctx)
@@ -2782,6 +2783,7 @@ function compileSlide(slide, slideIndex, processed, ctx) {
2782
2783
  if (element) elements.push(element);
2783
2784
  nextIndex += 1;
2784
2785
  };
2786
+ ctx.positionlessText = [];
2785
2787
  const gradientSource = slide.background?.gradient ?? (slide.background ? void 0 : template?.background?.gradient);
2786
2788
  let background2;
2787
2789
  if (gradientSource) {
@@ -2839,12 +2841,17 @@ function compileSlide(slide, slideIndex, processed, ctx) {
2839
2841
  })
2840
2842
  );
2841
2843
  }
2842
- for (const component of compilePlaceholderComponents(
2844
+ for (const { component } of resolvePlaceholderComponents(
2843
2845
  slide,
2844
2846
  template,
2845
2847
  effectiveGrid,
2846
- slideIndex,
2847
- ctx
2848
+ {
2849
+ theme: ctx.theme,
2850
+ slideWidth: ctx.slideWidthInches,
2851
+ slideHeight: ctx.slideHeightInches,
2852
+ slideIndex,
2853
+ warnings: ctx.warnings
2854
+ }
2848
2855
  )) {
2849
2856
  push(
2850
2857
  compileComponent(component, {
@@ -2855,6 +2862,7 @@ function compileSlide(slide, slideIndex, processed, ctx) {
2855
2862
  })
2856
2863
  );
2857
2864
  }
2865
+ warnOverlappingText(slideIndex, ctx);
2858
2866
  if (slide.notes) ctx.features.require("speaker-notes", `${path4}.notes`);
2859
2867
  if (slide.hidden) ctx.features.require("hidden-slides", `${path4}.hidden`);
2860
2868
  if (background2) ctx.features.require("backgrounds", `${path4}.background`);
@@ -2871,6 +2879,23 @@ function compileSlide(slide, slideIndex, processed, ctx) {
2871
2879
  ...transition ? { transition } : {}
2872
2880
  };
2873
2881
  }
2882
+ function warnOverlappingText(slideIndex, ctx) {
2883
+ const boxes = ctx.positionlessText;
2884
+ for (let i = 0; i < boxes.length; i += 1) {
2885
+ for (let j = i + 1; j < boxes.length; j += 1) {
2886
+ const a = boxes[i];
2887
+ const b = boxes[j];
2888
+ const overlaps = a.transform.xEmu < b.transform.xEmu + b.transform.widthEmu && b.transform.xEmu < a.transform.xEmu + a.transform.widthEmu && a.transform.yEmu < b.transform.yEmu + b.transform.heightEmu && b.transform.yEmu < a.transform.yEmu + a.transform.heightEmu;
2889
+ if (!overlaps) continue;
2890
+ warn(
2891
+ ctx.warnings,
2892
+ W.TEXT_OVERLAP_UNPOSITIONED,
2893
+ `Text at ${a.path} and ${b.path} give no x/y and overlap on this slide. Give at least one of them explicit coordinates, or a named style whose default band differs.`,
2894
+ { slide: slideIndex, component: "text" }
2895
+ );
2896
+ }
2897
+ }
2898
+ }
2874
2899
  function compileTransition(transition) {
2875
2900
  const type = transition?.type;
2876
2901
  if (!type || type === "none") return void 0;
@@ -2887,71 +2912,6 @@ function slideContextFor(slideIndex, processed) {
2887
2912
  language: processed.language
2888
2913
  };
2889
2914
  }
2890
- function compilePlaceholderComponents(slide, template, effectiveGrid, slideIndex, ctx) {
2891
- if (!slide.placeholders) return [];
2892
- const out = [];
2893
- if (!template) {
2894
- for (const [name, component] of Object.entries(slide.placeholders)) {
2895
- const defaulted = resolveComponentDefaults(component, ctx.theme);
2896
- const positioned = defaulted.props.x != null || defaulted.props.y != null || defaulted.props.grid;
2897
- if (!positioned) {
2898
- warn(
2899
- ctx.warnings,
2900
- W.PLACEHOLDER_NO_POSITION,
2901
- `Placeholder "${name}" has no template and no explicit position \u2014 skipped`,
2902
- { slide: slideIndex }
2903
- );
2904
- continue;
2905
- }
2906
- out.push(
2907
- resolveComponentGridPosition(
2908
- defaulted,
2909
- effectiveGrid,
2910
- ctx.slideWidthInches,
2911
- ctx.slideHeightInches,
2912
- ctx.warnings
2913
- )
2914
- );
2915
- }
2916
- return out;
2917
- }
2918
- const declared = new Map(
2919
- (template.placeholders ?? []).map((placeholder) => [
2920
- placeholder.name,
2921
- placeholder
2922
- ])
2923
- );
2924
- for (const [name, component] of Object.entries(slide.placeholders)) {
2925
- const definition = declared.get(name);
2926
- if (!definition) {
2927
- warn(
2928
- ctx.warnings,
2929
- W.UNKNOWN_PLACEHOLDER,
2930
- `Unknown placeholder "${name}" in template "${slide.template}". Available: ${[...declared.keys()].join(", ")}`,
2931
- { slide: slideIndex }
2932
- );
2933
- continue;
2934
- }
2935
- const gridResolved = resolveComponentGridPosition(
2936
- component,
2937
- effectiveGrid,
2938
- ctx.slideWidthInches,
2939
- ctx.slideHeightInches,
2940
- ctx.warnings
2941
- );
2942
- const typeDefaults = getDefaultsForType(component.name, ctx.theme);
2943
- const positionDefaults = {};
2944
- if (definition.x != null) positionDefaults.x = definition.x;
2945
- if (definition.y != null) positionDefaults.y = definition.y;
2946
- if (definition.w != null) positionDefaults.w = definition.w;
2947
- if (definition.h != null) positionDefaults.h = definition.h;
2948
- let props = mergeWithDefaults2(positionDefaults, typeDefaults);
2949
- props = mergeWithDefaults2(definition.defaults?.props ?? {}, props);
2950
- props = mergeWithDefaults2(gridResolved.props, props);
2951
- out.push({ ...gridResolved, props });
2952
- }
2953
- return out;
2954
- }
2955
2915
  function compileComponent(component, scope) {
2956
2916
  if (component.enabled === false) return void 0;
2957
2917
  switch (component.name) {
@@ -3005,6 +2965,9 @@ function compileText(component, scope) {
3005
2965
  const heightFontSize = props.fontSize ?? ctx.theme.defaults.fontSize ?? 18;
3006
2966
  const transform = textTransform(props, heightFontSize, lineCount, ctx);
3007
2967
  const autoFit = props.h === void 0;
2968
+ if (props.x === void 0 && props.y === void 0) {
2969
+ ctx.positionlessText.push({ path: path4, transform });
2970
+ }
3008
2971
  const hyperlink2 = compileHyperlink(props.hyperlink, "text", ctx, path4);
3009
2972
  const runs = runProps ? runProps.map(
3010
2973
  (run) => compileRichRun(run, cascade, void 0, scope.slideCtx, ctx)
@@ -3210,12 +3173,26 @@ function requireBulletFeatures(bullet, path4, ctx) {
3210
3173
  ctx.features.require("complex-bullet-glyphs", `${path4}.bullet.style`);
3211
3174
  }
3212
3175
  }
3176
+ var STYLE_BANDS = {
3177
+ // A title slide's title and its subtitle, as two bands in the middle third.
3178
+ title: { x: 0.08, y: 0.34, w: 0.84 },
3179
+ subtitle: { x: 0.08, y: 0.56, w: 0.84 },
3180
+ // Content-slide headings sit in the top margin, all three at the same place:
3181
+ // the level changes the type, not where the slide starts.
3182
+ heading1: { x: 0.06, y: 0.06, w: 0.88 },
3183
+ heading2: { x: 0.06, y: 0.06, w: 0.88 },
3184
+ heading3: { x: 0.06, y: 0.06, w: 0.88 },
3185
+ // Below a heading, in the content-safe area.
3186
+ body: { x: 0.06, y: 0.26, w: 0.88 },
3187
+ caption: { x: 0.06, y: 0.88, w: 0.88 }
3188
+ };
3213
3189
  function textTransform(props, fontSize, lineCount, ctx) {
3214
3190
  const heightEmu = props.h !== void 0 ? resolveDimensionEmu(props.h, "Y", ctx.extent) : inchesToEmu(Math.max(0.5, fontSize / 72 * 1.6 * lineCount));
3191
+ const band = typeof props.style === "string" ? STYLE_BANDS[props.style] : void 0;
3215
3192
  return {
3216
- xEmu: props.x !== void 0 ? resolveDimensionEmu(props.x, "X", ctx.extent) : 0,
3217
- yEmu: props.y !== void 0 ? resolveDimensionEmu(props.y, "Y", ctx.extent) : 0,
3218
- widthEmu: props.w !== void 0 ? resolveDimensionEmu(props.w, "X", ctx.extent) : defaultWidthEmu(ctx.extent),
3193
+ xEmu: props.x !== void 0 ? resolveDimensionEmu(props.x, "X", ctx.extent) : Math.round((band?.x ?? 0) * ctx.extent.widthEmu),
3194
+ yEmu: props.y !== void 0 ? resolveDimensionEmu(props.y, "Y", ctx.extent) : Math.round((band?.y ?? 0) * ctx.extent.heightEmu),
3195
+ widthEmu: props.w !== void 0 ? resolveDimensionEmu(props.w, "X", ctx.extent) : band ? Math.round(band.w * ctx.extent.widthEmu) : defaultWidthEmu(ctx.extent),
3219
3196
  heightEmu,
3220
3197
  ...rotationProperty(props.rotate)
3221
3198
  };
@@ -3437,6 +3414,11 @@ function compileTable(component, scope) {
3437
3414
  const rows = authoredRows.map((row) => ({
3438
3415
  cells: row.map((cell) => compileTableCell(cell, props, ctx))
3439
3416
  }));
3417
+ if (props.headerRow && rows.length > 0) {
3418
+ rows[0] = {
3419
+ cells: rows[0].cells.map((cell) => headerCell(cell, props, ctx))
3420
+ };
3421
+ }
3440
3422
  const border = compileTableBorder(props.border, ctx);
3441
3423
  const cornerRadius = typeof props.borderRadius === "number" && props.borderRadius > 0 ? props.borderRadius : void 0;
3442
3424
  ctx.features.require("tables", path4);
@@ -3501,6 +3483,19 @@ function toEmuList(value, axis, ctx) {
3501
3483
  const values = Array.isArray(value) ? value : [value];
3502
3484
  return values.map((v) => resolveDimensionEmu(v, axis, ctx.extent));
3503
3485
  }
3486
+ function headerCell(cell, tableProps, ctx) {
3487
+ const formatting = {
3488
+ ...tableProps.fontWeight == null ? { bold: true } : {},
3489
+ ...tableProps.color ? {} : { color: irColor(resolveColor("text", ctx.theme, ctx.warnings)) },
3490
+ ...cell.formatting
3491
+ };
3492
+ const fill2 = cell.fill ?? (tableProps.fill ? void 0 : irColor(resolveColor("background2", ctx.theme, ctx.warnings)));
3493
+ return {
3494
+ ...cell,
3495
+ formatting,
3496
+ ...fill2 ? { fill: fill2 } : {}
3497
+ };
3498
+ }
3504
3499
  function compileTableDefaults(props, ctx) {
3505
3500
  const family = props.fontFace ?? ctx.theme.fonts.body;
3506
3501
  let fontFamily = family;
@@ -4045,7 +4040,9 @@ function optionalTransform(x, y, w, h, ctx) {
4045
4040
  }
4046
4041
 
4047
4042
  // src/renderers/registry.ts
4048
- import { RendererRegistry } from "@json-to-office/shared/rendering";
4043
+ import {
4044
+ RendererRegistry
4045
+ } from "@json-to-office/shared/rendering";
4049
4046
 
4050
4047
  // src/renderers/types.ts
4051
4048
  import {
@@ -4071,6 +4068,9 @@ function resolvePptxRenderer(id) {
4071
4068
  function pptxRendererIds() {
4072
4069
  return registry.ids();
4073
4070
  }
4071
+ function pptxRendererStatuses() {
4072
+ return registry.statuses();
4073
+ }
4074
4074
  function isPptxRendererId(value) {
4075
4075
  return registry.has(value);
4076
4076
  }
@@ -4146,134 +4146,6 @@ async function resolveDocumentFonts(document, theme, warnings, fonts) {
4146
4146
  return resolved;
4147
4147
  }
4148
4148
 
4149
- // src/core/generationContext.ts
4150
- import { applyExportMode } from "@json-to-office/shared";
4151
-
4152
- // src/themes/defaults.ts
4153
- var DEFAULT_STYLES = {
4154
- title: { fontSize: 36, bold: true, fontColor: "text", align: "center" },
4155
- subtitle: { fontSize: 20, italic: true, fontColor: "text2", align: "center" },
4156
- heading1: { fontSize: 28, bold: true, fontColor: "primary" },
4157
- heading2: { fontSize: 22, bold: true, fontColor: "primary" },
4158
- heading3: { fontSize: 18, bold: true, fontColor: "text" },
4159
- body: { fontSize: 14 },
4160
- caption: { fontSize: 10, italic: true, fontColor: "text2" }
4161
- };
4162
- var DEFAULT_PPTX_THEME = {
4163
- name: "default",
4164
- colors: {
4165
- primary: "#4472C4",
4166
- secondary: "#ED7D31",
4167
- accent: "#70AD47",
4168
- background: "#FFFFFF",
4169
- text: "#333333",
4170
- text2: "#44546A",
4171
- background2: "#E7E6E6",
4172
- accent4: "#FFC000",
4173
- accent5: "#5B9BD5",
4174
- accent6: "#70AD47"
4175
- },
4176
- fonts: {
4177
- heading: "Arial",
4178
- body: "Arial"
4179
- },
4180
- defaults: {
4181
- fontSize: 18,
4182
- fontColor: "#333333"
4183
- },
4184
- styles: DEFAULT_STYLES
4185
- };
4186
- var PPTX_THEMES = {
4187
- default: DEFAULT_PPTX_THEME,
4188
- dark: {
4189
- name: "dark",
4190
- colors: {
4191
- primary: "#5B9BD5",
4192
- secondary: "#FF6F61",
4193
- accent: "#6BCB77",
4194
- background: "#2D2D2D",
4195
- text: "#FFFFFF",
4196
- text2: "#CCCCCC",
4197
- background2: "#3D3D3D",
4198
- accent4: "#FFB347",
4199
- accent5: "#77DD77",
4200
- accent6: "#AEC6CF"
4201
- },
4202
- fonts: {
4203
- heading: "Arial",
4204
- body: "Arial"
4205
- },
4206
- defaults: {
4207
- fontSize: 18,
4208
- fontColor: "#FFFFFF"
4209
- },
4210
- styles: DEFAULT_STYLES
4211
- },
4212
- minimal: {
4213
- name: "minimal",
4214
- colors: {
4215
- primary: "#000000",
4216
- secondary: "#666666",
4217
- accent: "#999999",
4218
- background: "#FFFFFF",
4219
- text: "#000000",
4220
- text2: "#444444",
4221
- background2: "#F5F5F5",
4222
- accent4: "#BBBBBB",
4223
- accent5: "#DDDDDD",
4224
- accent6: "#888888"
4225
- },
4226
- fonts: {
4227
- heading: "Helvetica",
4228
- body: "Helvetica"
4229
- },
4230
- defaults: {
4231
- fontSize: 18,
4232
- fontColor: "#000000"
4233
- },
4234
- styles: DEFAULT_STYLES
4235
- }
4236
- };
4237
- function getPptxTheme(name) {
4238
- return PPTX_THEMES[name] || DEFAULT_PPTX_THEME;
4239
- }
4240
- function hasPptxTheme(name) {
4241
- return Object.prototype.hasOwnProperty.call(PPTX_THEMES, name);
4242
- }
4243
- var pptxThemes = PPTX_THEMES;
4244
-
4245
- // src/core/generationContext.ts
4246
- function resolveThemeContext(documentIn, options = {}) {
4247
- const { customThemes, fonts, warnings, defaultThemeName, resolveNamedTheme } = options;
4248
- if (documentIn.props === null) {
4249
- throw new Error(
4250
- "Document `props` is null. Omit it, or provide an object \u2014 a null props cannot carry a theme."
4251
- );
4252
- }
4253
- let document = documentIn.props === void 0 ? { ...documentIn, props: {} } : documentIn;
4254
- let inlineTheme;
4255
- if (typeof document.props.theme === "object" && document.props.theme !== null) {
4256
- inlineTheme = document.props.theme;
4257
- }
4258
- const authoredThemeName = typeof document.props.theme === "string" ? document.props.theme : void 0;
4259
- const baseThemeName = inlineTheme ? inlineTheme.name || "inline-theme" : authoredThemeName ?? defaultThemeName ?? "default";
4260
- let theme = inlineTheme ?? (resolveNamedTheme ? resolveNamedTheme(baseThemeName, authoredThemeName !== void 0) : customThemes?.[baseThemeName] ?? getPptxTheme(baseThemeName));
4261
- const mode = applyExportMode({ doc: document, theme, fonts });
4262
- document = mode.doc;
4263
- theme = mode.theme;
4264
- for (const w of mode.warnings) {
4265
- warnings?.push({
4266
- code: w.code,
4267
- message: w.message,
4268
- component: "fontRegistry"
4269
- });
4270
- }
4271
- return {
4272
- document,
4273
- theme
4274
- };
4275
- }
4276
-
4277
4149
  // src/core/generationOptions.ts
4278
4150
  import {
4279
4151
  collectImageSourceConflicts,
@@ -4633,6 +4505,144 @@ async function probeIntrinsicSize(source, warnings) {
4633
4505
  }
4634
4506
  }
4635
4507
 
4508
+ // src/quality/facts.ts
4509
+ import { DEFAULT_PPTX_RENDERER_ID as DEFAULT_PPTX_RENDERER_ID2 } from "@json-to-office/shared-pptx";
4510
+
4511
+ // src/core/generationContext.ts
4512
+ import { applyExportMode } from "@json-to-office/shared";
4513
+
4514
+ // src/themes/defaults.ts
4515
+ var DEFAULT_TABLE = {
4516
+ border: { type: "solid", pt: 1, color: "background2" },
4517
+ headerRow: true
4518
+ };
4519
+ var DEFAULT_STYLES = {
4520
+ title: { fontSize: 36, bold: true, fontColor: "text", align: "center" },
4521
+ subtitle: { fontSize: 20, italic: true, fontColor: "text2", align: "center" },
4522
+ heading1: { fontSize: 28, bold: true, fontColor: "primary" },
4523
+ heading2: { fontSize: 22, bold: true, fontColor: "primary" },
4524
+ heading3: { fontSize: 18, bold: true, fontColor: "text" },
4525
+ body: { fontSize: 14 },
4526
+ caption: { fontSize: 10, italic: true, fontColor: "text2" }
4527
+ };
4528
+ var DEFAULT_PPTX_THEME = {
4529
+ name: "default",
4530
+ colors: {
4531
+ primary: "#4472C4",
4532
+ secondary: "#ED7D31",
4533
+ accent: "#70AD47",
4534
+ background: "#FFFFFF",
4535
+ text: "#333333",
4536
+ text2: "#44546A",
4537
+ background2: "#E7E6E6",
4538
+ accent4: "#FFC000",
4539
+ accent5: "#5B9BD5",
4540
+ accent6: "#70AD47"
4541
+ },
4542
+ fonts: {
4543
+ heading: "Arial",
4544
+ body: "Arial"
4545
+ },
4546
+ defaults: {
4547
+ fontSize: 18,
4548
+ fontColor: "#333333"
4549
+ },
4550
+ styles: DEFAULT_STYLES,
4551
+ componentDefaults: { table: DEFAULT_TABLE }
4552
+ };
4553
+ var PPTX_THEMES = {
4554
+ default: DEFAULT_PPTX_THEME,
4555
+ dark: {
4556
+ name: "dark",
4557
+ colors: {
4558
+ primary: "#5B9BD5",
4559
+ secondary: "#FF6F61",
4560
+ accent: "#6BCB77",
4561
+ background: "#2D2D2D",
4562
+ text: "#FFFFFF",
4563
+ text2: "#CCCCCC",
4564
+ background2: "#3D3D3D",
4565
+ accent4: "#FFB347",
4566
+ accent5: "#77DD77",
4567
+ accent6: "#AEC6CF"
4568
+ },
4569
+ fonts: {
4570
+ heading: "Arial",
4571
+ body: "Arial"
4572
+ },
4573
+ defaults: {
4574
+ fontSize: 18,
4575
+ fontColor: "#FFFFFF"
4576
+ },
4577
+ styles: DEFAULT_STYLES,
4578
+ componentDefaults: { table: DEFAULT_TABLE }
4579
+ },
4580
+ minimal: {
4581
+ name: "minimal",
4582
+ colors: {
4583
+ primary: "#000000",
4584
+ secondary: "#666666",
4585
+ accent: "#999999",
4586
+ background: "#FFFFFF",
4587
+ text: "#000000",
4588
+ text2: "#444444",
4589
+ background2: "#F5F5F5",
4590
+ accent4: "#BBBBBB",
4591
+ accent5: "#DDDDDD",
4592
+ accent6: "#888888"
4593
+ },
4594
+ fonts: {
4595
+ heading: "Helvetica",
4596
+ body: "Helvetica"
4597
+ },
4598
+ defaults: {
4599
+ fontSize: 18,
4600
+ fontColor: "#000000"
4601
+ },
4602
+ styles: DEFAULT_STYLES,
4603
+ componentDefaults: { table: DEFAULT_TABLE }
4604
+ }
4605
+ };
4606
+ function getPptxTheme(name) {
4607
+ return PPTX_THEMES[name] || DEFAULT_PPTX_THEME;
4608
+ }
4609
+ function hasPptxTheme(name) {
4610
+ return Object.prototype.hasOwnProperty.call(PPTX_THEMES, name);
4611
+ }
4612
+ var pptxThemes = PPTX_THEMES;
4613
+
4614
+ // src/core/generationContext.ts
4615
+ function resolveThemeContext(documentIn, options = {}) {
4616
+ const { customThemes, fonts, warnings, defaultThemeName, resolveNamedTheme } = options;
4617
+ if (documentIn.props === null) {
4618
+ throw new Error(
4619
+ "Document `props` is null. Omit it, or provide an object \u2014 a null props cannot carry a theme."
4620
+ );
4621
+ }
4622
+ let document = documentIn.props === void 0 ? { ...documentIn, props: {} } : documentIn;
4623
+ let inlineTheme;
4624
+ if (typeof document.props.theme === "object" && document.props.theme !== null) {
4625
+ inlineTheme = document.props.theme;
4626
+ }
4627
+ const authoredThemeName = typeof document.props.theme === "string" ? document.props.theme : void 0;
4628
+ const baseThemeName = inlineTheme ? inlineTheme.name || "inline-theme" : authoredThemeName ?? defaultThemeName ?? "default";
4629
+ let theme = inlineTheme ?? (resolveNamedTheme ? resolveNamedTheme(baseThemeName, authoredThemeName !== void 0) : customThemes?.[baseThemeName] ?? getPptxTheme(baseThemeName));
4630
+ const mode = applyExportMode({ doc: document, theme, fonts });
4631
+ document = mode.doc;
4632
+ theme = mode.theme;
4633
+ for (const w of mode.warnings) {
4634
+ warnings?.push({
4635
+ code: w.code,
4636
+ message: w.message,
4637
+ component: "fontRegistry"
4638
+ });
4639
+ }
4640
+ return {
4641
+ document,
4642
+ theme
4643
+ };
4644
+ }
4645
+
4636
4646
  // src/core/structure.ts
4637
4647
  import { mergeWithDefaults as mergeWithDefaults3 } from "@json-to-office/shared";
4638
4648
  function isSlideEnabled(child) {
@@ -4755,6 +4765,458 @@ function processPresentation(document, options) {
4755
4765
  };
4756
4766
  }
4757
4767
 
4768
+ // src/quality/facts.ts
4769
+ function asRecord(value) {
4770
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
4771
+ }
4772
+ function asNumber(value) {
4773
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4774
+ }
4775
+ function dimToPt(value, axisIn) {
4776
+ if (typeof value === "number" && Number.isFinite(value)) return value * 72;
4777
+ if (typeof value === "string") {
4778
+ const trimmed = value.trim();
4779
+ if (trimmed.endsWith("%")) {
4780
+ const pct = Number(trimmed.slice(0, -1));
4781
+ return Number.isFinite(pct) ? pct / 100 * axisIn * 72 : void 0;
4782
+ }
4783
+ const inches = Number(trimmed);
4784
+ return Number.isFinite(inches) ? inches * 72 : void 0;
4785
+ }
4786
+ return void 0;
4787
+ }
4788
+ function resolveBox(props, grid, slideWidthIn, slideHeightIn) {
4789
+ let xPt = dimToPt(props.x, slideWidthIn);
4790
+ let yPt = dimToPt(props.y, slideHeightIn);
4791
+ let widthPt = dimToPt(props.w, slideWidthIn);
4792
+ let heightPt = dimToPt(props.h, slideHeightIn);
4793
+ const gridPos = asRecord(props.grid);
4794
+ if (gridPos !== void 0 && asNumber(gridPos.column) !== void 0 && asNumber(gridPos.row) !== void 0 && (xPt === void 0 || yPt === void 0 || widthPt === void 0 || heightPt === void 0)) {
4795
+ const resolved = resolveGridPosition(
4796
+ gridPos,
4797
+ grid,
4798
+ slideWidthIn,
4799
+ slideHeightIn
4800
+ );
4801
+ xPt ??= resolved.x * 72;
4802
+ yPt ??= resolved.y * 72;
4803
+ widthPt ??= resolved.w * 72;
4804
+ heightPt ??= resolved.h * 72;
4805
+ }
4806
+ return { xPt, yPt, widthPt, heightPt };
4807
+ }
4808
+ function isCompleteBox(box) {
4809
+ return box.xPt !== void 0 && box.yPt !== void 0 && box.widthPt !== void 0 && box.heightPt !== void 0 && box.widthPt > 0 && box.heightPt > 0;
4810
+ }
4811
+ function containsCentre(surface, text) {
4812
+ const cx = text.xPt + text.widthPt / 2;
4813
+ const cy = text.yPt + text.heightPt / 2;
4814
+ return cx >= surface.xPt && cx <= surface.xPt + surface.widthPt && cy >= surface.yPt && cy <= surface.yPt + surface.heightPt;
4815
+ }
4816
+ function themeContext(theme) {
4817
+ return {
4818
+ styles: theme.styles ?? {},
4819
+ defaultFontSize: asNumber(theme.defaults?.fontSize) ?? 18
4820
+ };
4821
+ }
4822
+ function fillColorHexes(fill2, theme) {
4823
+ const found = [];
4824
+ const visit = (node) => {
4825
+ if (Array.isArray(node)) {
4826
+ node.forEach(visit);
4827
+ return;
4828
+ }
4829
+ const rec = asRecord(node);
4830
+ if (!rec) return;
4831
+ if (typeof rec.color === "string") {
4832
+ const hex = resolveColor(rec.color, theme);
4833
+ if (hex) found.push(hex.toUpperCase());
4834
+ }
4835
+ for (const value of Object.values(rec)) {
4836
+ if (typeof value === "object" && value !== null) visit(value);
4837
+ }
4838
+ };
4839
+ visit(fill2);
4840
+ return [...new Set(found)];
4841
+ }
4842
+ var FOCUS_CORNERS = {
4843
+ topLeft: [0, 0],
4844
+ topRight: [1, 0],
4845
+ bottomLeft: [0, 1],
4846
+ bottomRight: [1, 1]
4847
+ };
4848
+ function lerpHex(a, b, t) {
4849
+ const pa = parseInt(a, 16);
4850
+ const pb = parseInt(b, 16);
4851
+ const mix = (shift) => Math.round(
4852
+ (pa >> shift & 255) + ((pb >> shift & 255) - (pa >> shift & 255)) * t
4853
+ );
4854
+ return [mix(16), mix(8), mix(0)].map((c) => c.toString(16).padStart(2, "0")).join("").toUpperCase();
4855
+ }
4856
+ function gradientPosition(gradient, fx, fy, widthUnits, heightUnits) {
4857
+ if (gradient.type === "radial") {
4858
+ const focus = FOCUS_CORNERS[typeof gradient.focus === "string" ? gradient.focus : "topLeft"] ?? FOCUS_CORNERS.topLeft;
4859
+ const radius = Math.hypot(widthUnits, heightUnits) / 2;
4860
+ if (radius === 0) return 0;
4861
+ const distance = Math.hypot(
4862
+ (fx - focus[0]) * widthUnits,
4863
+ (fy - focus[1]) * heightUnits
4864
+ );
4865
+ return Math.min(1, distance / radius);
4866
+ }
4867
+ const angle = (asNumber(gradient.angle) ?? 0) * Math.PI / 180;
4868
+ const dx = Math.cos(angle);
4869
+ const dy = Math.sin(angle);
4870
+ const min = Math.min(0, dx) + Math.min(0, dy);
4871
+ const max = Math.max(0, dx) + Math.max(0, dy);
4872
+ if (max === min) return 0;
4873
+ return Math.min(1, Math.max(0, (fx * dx + fy * dy - min) / (max - min)));
4874
+ }
4875
+ function paintedColorHexes(fill2, theme, fx, fy, widthUnits, heightUnits) {
4876
+ const rec = asRecord(fill2);
4877
+ const gradient = asRecord(rec?.gradient);
4878
+ const stops = Array.isArray(gradient?.stops) ? gradient.stops : void 0;
4879
+ if (gradient && stops && stops.length > 0) {
4880
+ const parsed = stops.flatMap((stop) => {
4881
+ const entry = asRecord(stop);
4882
+ const color2 = typeof entry?.color === "string" ? entry.color : void 0;
4883
+ if (!color2) return [];
4884
+ const hex = resolveColor(color2, theme);
4885
+ if (!hex) return [];
4886
+ return [{ pos: asNumber(entry?.pos) ?? 0, hex: hex.toUpperCase() }];
4887
+ }).sort((a, b) => a.pos - b.pos);
4888
+ if (parsed.length === 0) return [];
4889
+ const target = gradientPosition(gradient, fx, fy, widthUnits, heightUnits) * 100;
4890
+ if (target <= parsed[0].pos) return [parsed[0].hex];
4891
+ const last = parsed[parsed.length - 1];
4892
+ if (target >= last.pos) return [last.hex];
4893
+ for (let i = 1; i < parsed.length; i += 1) {
4894
+ const previous = parsed[i - 1];
4895
+ const next = parsed[i];
4896
+ if (target <= next.pos) {
4897
+ const span = next.pos - previous.pos;
4898
+ const t = span === 0 ? 0 : (target - previous.pos) / span;
4899
+ return [lerpHex(previous.hex, next.hex, t)];
4900
+ }
4901
+ }
4902
+ return [last.hex];
4903
+ }
4904
+ return fillColorHexes(fill2, theme);
4905
+ }
4906
+ function defaultLineHeightPt(fontSize) {
4907
+ if (fontSize >= 60) return fontSize * 1.05;
4908
+ if (fontSize >= 28) return fontSize * 1.15;
4909
+ return fontSize * 1.25;
4910
+ }
4911
+ function resolveTypography(props, ctx) {
4912
+ const styleName = typeof props.style === "string" ? props.style : void 0;
4913
+ const style = styleName !== void 0 ? ctx.styles[styleName] : void 0;
4914
+ const fontSize = asNumber(props.fontSize) ?? style?.fontSize ?? ctx.defaultFontSize;
4915
+ const multiple = asNumber(props.lineSpacingMultiple);
4916
+ const lineSpacing = multiple !== void 0 ? fontSize * multiple : asNumber(props.lineSpacing) ?? style?.lineSpacing ?? defaultLineHeightPt(fontSize);
4917
+ const weight = asNumber(props.fontWeight) ?? style?.fontWeight;
4918
+ const bold = weight !== void 0 ? weight >= 600 : typeof props.bold === "boolean" ? props.bold : style?.bold ?? false;
4919
+ return {
4920
+ fontSize,
4921
+ lineSpacing,
4922
+ paraSpaceBefore: asNumber(props.paraSpaceBefore) ?? 0,
4923
+ paraSpaceAfter: asNumber(props.paraSpaceAfter) ?? style?.paraSpaceAfter ?? 0,
4924
+ bold,
4925
+ ...styleName && { styleName }
4926
+ };
4927
+ }
4928
+ function collectSlideNodes(component, path4, text, surfaces, counter) {
4929
+ const rec = asRecord(component);
4930
+ if (!rec || rec.enabled === false) return;
4931
+ const props = asRecord(rec.props) ?? {};
4932
+ const order = counter.next++;
4933
+ if (rec.name === "image" || rec.name === "visual" || rec.name === "chart") {
4934
+ surfaces.push({ order, props, isImage: true });
4935
+ } else if (rec.name === "shape" && props.fill !== void 0) {
4936
+ surfaces.push({ order, props, isImage: false });
4937
+ }
4938
+ const content = typeof props.text === "string" ? props.text : void 0;
4939
+ if (content !== void 0 && content.trim() !== "" && props.runs === void 0) {
4940
+ if (rec.name === "text" || rec.name === "shape") {
4941
+ text.push({ props, path: path4, text: content, order });
4942
+ }
4943
+ }
4944
+ const children = Array.isArray(rec.children) ? rec.children : [];
4945
+ children.forEach(
4946
+ (child, index) => collectSlideNodes(
4947
+ child,
4948
+ `${path4}/children/${index}`,
4949
+ text,
4950
+ surfaces,
4951
+ counter
4952
+ )
4953
+ );
4954
+ }
4955
+ function pointerSegment(value) {
4956
+ return value.replace(/~/g, "~0").replace(/\//g, "~1");
4957
+ }
4958
+ function addSlideFacts(roots, slidePath, renderedIndex, grid, slideWidthIn, slideHeightIn, ctx, theme, slideBackground, analyzedTextPaths, addFact) {
4959
+ const nodes = [];
4960
+ const surfaces = [];
4961
+ const counter = { next: 0 };
4962
+ for (const root of roots) {
4963
+ collectSlideNodes(root.component, root.path, nodes, surfaces, counter);
4964
+ }
4965
+ const surfaceBoxes = surfaces.flatMap((surface) => {
4966
+ const box = resolveBox(surface.props, grid, slideWidthIn, slideHeightIn);
4967
+ return isCompleteBox(box) ? [{ ...surface, box }] : [];
4968
+ });
4969
+ let bodyWords = 0;
4970
+ nodes.forEach((node, nodeIndex) => {
4971
+ const typography = resolveTypography(node.props, ctx);
4972
+ if (typography.styleName !== "title" && typography.styleName !== "subtitle") {
4973
+ bodyWords += node.text.split(/\s+/).filter(Boolean).length;
4974
+ }
4975
+ if (analyzedTextPaths.has(node.path)) return;
4976
+ analyzedTextPaths.add(node.path);
4977
+ const gridPos = asRecord(node.props.grid);
4978
+ const nodeBox = resolveBox(node.props, grid, slideWidthIn, slideHeightIn);
4979
+ const {
4980
+ xPt: boxXPt,
4981
+ yPt: boxYPt,
4982
+ widthPt: boxWidthPt,
4983
+ heightPt: boxHeightPt
4984
+ } = nodeBox;
4985
+ const sampleFractions = (box, originX, originY, widthPt, heightPt) => {
4986
+ if (!isCompleteBox(box) || widthPt <= 0 || heightPt <= 0) {
4987
+ return [[0.5, 0.5]];
4988
+ }
4989
+ const x0 = (box.xPt - originX) / widthPt;
4990
+ const x1 = (box.xPt + box.widthPt - originX) / widthPt;
4991
+ const y0 = (box.yPt - originY) / heightPt;
4992
+ const y1 = (box.yPt + box.heightPt - originY) / heightPt;
4993
+ return [
4994
+ [(x0 + x1) / 2, (y0 + y1) / 2],
4995
+ [x0, y0],
4996
+ [x1, y0],
4997
+ [x0, y1],
4998
+ [x1, y1]
4999
+ ];
5000
+ };
5001
+ const slideSamples = sampleFractions(
5002
+ nodeBox,
5003
+ 0,
5004
+ 0,
5005
+ slideWidthIn * 72,
5006
+ slideHeightIn * 72
5007
+ );
5008
+ const ownFill = [
5009
+ ...new Set(
5010
+ sampleFractions(
5011
+ nodeBox,
5012
+ boxXPt ?? 0,
5013
+ boxYPt ?? 0,
5014
+ boxWidthPt ?? 0,
5015
+ boxHeightPt ?? 0
5016
+ ).flatMap(
5017
+ ([fx, fy]) => paintedColorHexes(
5018
+ node.props.fill,
5019
+ theme,
5020
+ fx,
5021
+ fy,
5022
+ (boxWidthPt ?? 0) / 72,
5023
+ (boxHeightPt ?? 0) / 72
5024
+ )
5025
+ )
5026
+ )
5027
+ ];
5028
+ let backgroundHexes = [
5029
+ ...new Set(slideSamples.flatMap(([fx, fy]) => slideBackground(fx, fy)))
5030
+ ];
5031
+ let backgroundUnknown = false;
5032
+ if (ownFill.length > 0) {
5033
+ backgroundHexes = ownFill;
5034
+ } else if (isCompleteBox(nodeBox)) {
5035
+ const covering = surfaceBoxes.filter(
5036
+ (surface) => surface.order < node.order && containsCentre(surface.box, nodeBox)
5037
+ ).pop();
5038
+ if (covering?.isImage) {
5039
+ backgroundUnknown = true;
5040
+ } else if (covering) {
5041
+ const fill2 = [
5042
+ ...new Set(
5043
+ sampleFractions(
5044
+ nodeBox,
5045
+ covering.box.xPt,
5046
+ covering.box.yPt,
5047
+ covering.box.widthPt,
5048
+ covering.box.heightPt
5049
+ ).flatMap(
5050
+ ([fx, fy]) => paintedColorHexes(
5051
+ covering.props.fill,
5052
+ theme,
5053
+ fx,
5054
+ fy,
5055
+ covering.box.widthPt / 72,
5056
+ covering.box.heightPt / 72
5057
+ )
5058
+ )
5059
+ )
5060
+ ];
5061
+ if (fill2.length > 0) backgroundHexes = fill2;
5062
+ else backgroundUnknown = true;
5063
+ }
5064
+ }
5065
+ const colorHex = typeof node.props.color === "string" ? resolveColor(node.props.color, theme)?.toUpperCase() : void 0;
5066
+ addFact({
5067
+ id: `pptx:text:${renderedIndex}:${nodeIndex}:${node.path}`,
5068
+ kind: "pptx/text",
5069
+ path: node.path,
5070
+ slidePath,
5071
+ text: node.text,
5072
+ fontSizePt: typography.fontSize,
5073
+ lineSpacingPt: typography.lineSpacing,
5074
+ paraSpaceBeforePt: typography.paraSpaceBefore,
5075
+ paraSpaceAfterPt: typography.paraSpaceAfter,
5076
+ ...typography.styleName && { styleName: typography.styleName },
5077
+ ...boxXPt !== void 0 && { boxXPt },
5078
+ ...boxYPt !== void 0 && { boxYPt },
5079
+ ...boxWidthPt !== void 0 && boxWidthPt > 0 && { boxWidthPt },
5080
+ ...boxHeightPt !== void 0 && boxHeightPt > 0 && { boxHeightPt },
5081
+ verticalAlign: node.props.valign === "middle" || node.props.valign === "bottom" ? node.props.valign : "top",
5082
+ rotationDeg: asNumber(node.props.rotate) ?? 0,
5083
+ bold: typography.bold,
5084
+ autoFit: node.props.h === void 0 && gridPos === void 0,
5085
+ ...colorHex !== void 0 && { colorHex },
5086
+ ...!backgroundUnknown && backgroundHexes.length > 0 && { backgroundHexes }
5087
+ });
5088
+ });
5089
+ addFact({
5090
+ id: `pptx:slide:${renderedIndex}:${slidePath}`,
5091
+ kind: "pptx/slide",
5092
+ path: slidePath,
5093
+ bodyWords
5094
+ });
5095
+ }
5096
+ function preparePptxQualityDocument(document, options = {}) {
5097
+ const facts = [];
5098
+ const provenance = {};
5099
+ const addFact = (fact) => {
5100
+ facts.push(fact);
5101
+ provenance[fact.id] = {
5102
+ path: fact.path,
5103
+ ...fact.relatedPaths && { relatedPaths: fact.relatedPaths }
5104
+ };
5105
+ };
5106
+ const props = asRecord(document.props) ?? {};
5107
+ addFact({
5108
+ id: "pptx:canvas",
5109
+ kind: "pptx/canvas",
5110
+ path: "/props",
5111
+ ...asNumber(props.slideWidth) !== void 0 && {
5112
+ widthIn: asNumber(props.slideWidth)
5113
+ },
5114
+ ...asNumber(props.slideHeight) !== void 0 && {
5115
+ heightIn: asNumber(props.slideHeight)
5116
+ }
5117
+ });
5118
+ const warnings = options.warnings ?? [];
5119
+ const context = resolveThemeContext(document, {
5120
+ customThemes: options.customThemes,
5121
+ fonts: options.fonts,
5122
+ warnings
5123
+ });
5124
+ const processed = processPresentation(context.document, {
5125
+ theme: context.theme,
5126
+ customThemes: options.customThemes,
5127
+ services: options.services
5128
+ });
5129
+ const ctx = themeContext(processed.theme);
5130
+ const authoredChildren = Array.isArray(document.children) ? document.children : [];
5131
+ const slideIndexes = authoredChildren.flatMap((child, index) => {
5132
+ const slide = asRecord(child);
5133
+ return slide?.name === "slide" && slide.enabled !== false ? [index] : [];
5134
+ });
5135
+ const templateIndexes = /* @__PURE__ */ new Map();
5136
+ const templates = new Map(
5137
+ (processed.templates ?? []).map((template, index) => {
5138
+ templateIndexes.set(template.name, index);
5139
+ return [template.name, template];
5140
+ })
5141
+ );
5142
+ const analyzedTextPaths = /* @__PURE__ */ new Set();
5143
+ processed.slides.forEach((slide, renderedIndex) => {
5144
+ const authoredIndex = slideIndexes[renderedIndex];
5145
+ if (authoredIndex === void 0) return;
5146
+ const slidePath = `/children/${authoredIndex}`;
5147
+ const authoredSlide = asRecord(authoredChildren[authoredIndex]);
5148
+ const authoredComponents = Array.isArray(authoredSlide?.children) ? authoredSlide.children : [];
5149
+ const template = slide.template ? templates.get(slide.template) : void 0;
5150
+ const effectiveGrid = mergeGridConfigs(processed.grid, template?.grid);
5151
+ const roots = [];
5152
+ const templateIndex = template ? templateIndexes.get(template.name) : void 0;
5153
+ if (template && templateIndex !== void 0) {
5154
+ template.objects?.forEach((component, index) => {
5155
+ roots.push({
5156
+ component,
5157
+ path: `/props/templates/${templateIndex}/objects/${index}`
5158
+ });
5159
+ });
5160
+ }
5161
+ slide.components.forEach((component, index) => {
5162
+ if (authoredComponents[index] === void 0) return;
5163
+ roots.push({
5164
+ component,
5165
+ path: `${slidePath}/children/${index}`
5166
+ });
5167
+ });
5168
+ for (const resolved of resolvePlaceholderComponents(
5169
+ slide,
5170
+ template,
5171
+ effectiveGrid,
5172
+ {
5173
+ theme: processed.theme,
5174
+ slideWidth: processed.slideWidth,
5175
+ slideHeight: processed.slideHeight,
5176
+ slideIndex: renderedIndex,
5177
+ warnings
5178
+ }
5179
+ )) {
5180
+ roots.push({
5181
+ component: resolved.component,
5182
+ path: `${slidePath}/props/placeholders/${pointerSegment(resolved.name)}`
5183
+ });
5184
+ }
5185
+ addSlideFacts(
5186
+ roots,
5187
+ slidePath,
5188
+ renderedIndex,
5189
+ effectiveGrid,
5190
+ processed.slideWidth,
5191
+ processed.slideHeight,
5192
+ ctx,
5193
+ processed.theme,
5194
+ (fx, fy) => paintedColorHexes(
5195
+ slide.background ?? template?.background ?? props.background,
5196
+ processed.theme,
5197
+ fx,
5198
+ fy,
5199
+ processed.slideWidth,
5200
+ processed.slideHeight
5201
+ ),
5202
+ analyzedTextPaths,
5203
+ addFact
5204
+ );
5205
+ });
5206
+ return {
5207
+ format: "pptx",
5208
+ model: {
5209
+ authored: document,
5210
+ document: context.document,
5211
+ theme: context.theme,
5212
+ processed
5213
+ },
5214
+ facts,
5215
+ provenance,
5216
+ renderer: options.renderer ?? DEFAULT_PPTX_RENDERER_ID2
5217
+ };
5218
+ }
5219
+
4758
5220
  // src/core/generateFromIr.ts
4759
5221
  var UncompiledComponentError = class extends Error {
4760
5222
  code = "UNCOMPILED_COMPONENT";
@@ -4777,29 +5239,24 @@ async function generateBufferViaIr(jsonConfig, options) {
4777
5239
  selectedRenderer && PPTX_RENDERER_IDS.includes(selectedRenderer) ? { ...component, renderer: selectedRenderer } : component,
4778
5240
  options?.validation
4779
5241
  );
5242
+ assertNoContentConflicts(component);
4780
5243
  const warnings = [];
4781
- const context = resolveThemeContext(component, {
5244
+ const prepared = options?.prepared ?? preparePptxQualityDocument(component, {
4782
5245
  customThemes: options?.customThemes,
4783
5246
  fonts: options?.fonts,
4784
- warnings
5247
+ warnings,
5248
+ services: options?.services,
5249
+ renderer: selectedRenderer
4785
5250
  });
4786
- assertNoContentConflicts(context.document);
4787
5251
  await resolveDocumentFonts(
4788
- context.document,
4789
- context.theme,
5252
+ prepared.model.document,
5253
+ prepared.model.theme,
4790
5254
  warnings,
4791
5255
  options?.fonts
4792
5256
  );
4793
5257
  const buffer = await runWithBaseDir(
4794
5258
  options?.baseDir,
4795
- () => renderProcessedViaIr(
4796
- processPresentation(context.document, {
4797
- ...effectiveOptions,
4798
- theme: context.theme
4799
- }),
4800
- warnings,
4801
- effectiveOptions
4802
- )
5259
+ () => renderProcessedViaIr(prepared.model.processed, warnings, effectiveOptions)
4803
5260
  );
4804
5261
  return { buffer, warnings };
4805
5262
  }
@@ -4868,6 +5325,465 @@ var PresentationGenerator = {
4868
5325
  init_finalizePackage();
4869
5326
  init_warn();
4870
5327
 
5328
+ // src/quality/preflight.ts
5329
+ import {
5330
+ assertValidQualityPolicy,
5331
+ assertValidQualityProfile
5332
+ } from "@json-to-office/quality";
5333
+
5334
+ // src/quality/rules.ts
5335
+ import {
5336
+ mergeQualityProfiles,
5337
+ QUALITY_CODES,
5338
+ QualityEngine,
5339
+ resolveRuleConfiguration
5340
+ } from "@json-to-office/quality";
5341
+ var RENDERER_DEFAULT_WIDTH_IN = 10;
5342
+ var RENDERER_DEFAULT_HEIGHT_IN = 7.5;
5343
+ var DEFAULT_CHAR_WIDTH_FACTOR = 0.46;
5344
+ var DEFAULT_SAFETY_BUFFER_PT = 8;
5345
+ var DEFAULT_MIN_READABLE_FONT_PT = 7;
5346
+ var DEFAULT_MAX_BODY_WORDS_PER_SLIDE = 130;
5347
+ var KNOWN_CANVASES = [
5348
+ { w: 13.333, h: 7.5, label: "16:9 standard" },
5349
+ { w: 10, h: 5.625, label: "16:9 small" },
5350
+ { w: 7.5, h: 7.5, label: "1:1 carousel" },
5351
+ { w: 7.5, h: 9.375, label: "4:5 vertical" },
5352
+ { w: 4.5, h: 8, label: "9:16 story" },
5353
+ { w: 10, h: 7.5, label: "4:3 legacy", legacy: true }
5354
+ ];
5355
+ function numberParameter(parameters, name, fallback) {
5356
+ const value = parameters[name];
5357
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
5358
+ }
5359
+ function textFacts(facts) {
5360
+ return facts.filter(
5361
+ (fact) => fact.kind === "pptx/text"
5362
+ );
5363
+ }
5364
+ function estimateTextHeightPt(fact, charWidthFactor) {
5365
+ if (fact.boxWidthPt === void 0 || fact.boxHeightPt === void 0) {
5366
+ return void 0;
5367
+ }
5368
+ const paragraphs = fact.text.split("\n");
5369
+ const charsPerLine = Math.max(
5370
+ 1,
5371
+ Math.floor(fact.boxWidthPt / (fact.fontSizePt * charWidthFactor))
5372
+ );
5373
+ let lines = 0;
5374
+ for (const paragraph of paragraphs) {
5375
+ const measured = paragraph.trimEnd();
5376
+ lines += measured === "" ? 1 : Math.max(1, Math.ceil(measured.length / charsPerLine));
5377
+ }
5378
+ let heightPt = fact.fontSizePt + Math.max(0, lines - 1) * fact.lineSpacingPt;
5379
+ if (paragraphs.length > 1) {
5380
+ heightPt += (paragraphs.length - 1) * (fact.paraSpaceBeforePt + fact.paraSpaceAfterPt);
5381
+ }
5382
+ return { heightPt, lines };
5383
+ }
5384
+ var pptxCanvasRule = {
5385
+ id: "pptx/canvas",
5386
+ code: QUALITY_CODES.CANVAS_UNSPECIFIED,
5387
+ category: "composition",
5388
+ defaultSeverity: "info",
5389
+ defaultCertainty: "deterministic",
5390
+ formats: ["pptx"],
5391
+ evaluate: ({ facts }) => {
5392
+ const canvas = facts.find(
5393
+ (fact) => fact.kind === "pptx/canvas"
5394
+ );
5395
+ if (!canvas) return [];
5396
+ const { widthIn: width, heightIn: height } = canvas;
5397
+ if (width === void 0 || height === void 0) {
5398
+ const missing = [
5399
+ width === void 0 ? "props.slideWidth" : void 0,
5400
+ height === void 0 ? "props.slideHeight" : void 0
5401
+ ].filter((entry) => entry !== void 0);
5402
+ const state = missing.length === 2 ? "No slide canvas declared" : `Incomplete slide canvas (${missing[0]} missing)`;
5403
+ return [
5404
+ {
5405
+ code: QUALITY_CODES.CANVAS_UNSPECIFIED,
5406
+ severity: "warning",
5407
+ category: "integrity",
5408
+ message: `${state}: the renderer falls back to 4:3 (${RENDERER_DEFAULT_WIDTH_IN}\xD7${RENDERER_DEFAULT_HEIGHT_IN}"), and 16:9 content on that canvas leaves a dead strip at the bottom.`,
5409
+ path: canvas.path,
5410
+ suggestion: "Declare props.slideWidth and props.slideHeight \u2014 13.333 \xD7 7.5 for a standard 16:9 deck.",
5411
+ context: {
5412
+ missing,
5413
+ rendererDefault: {
5414
+ slideWidth: RENDERER_DEFAULT_WIDTH_IN,
5415
+ slideHeight: RENDERER_DEFAULT_HEIGHT_IN
5416
+ }
5417
+ }
5418
+ }
5419
+ ];
5420
+ }
5421
+ const match = KNOWN_CANVASES.find(
5422
+ (known) => Math.abs(known.w - width) < 0.01 && Math.abs(known.h - height) < 0.01
5423
+ );
5424
+ if (match?.legacy) {
5425
+ return [
5426
+ {
5427
+ code: QUALITY_CODES.CANVAS_LEGACY,
5428
+ message: `Canvas is 4:3 legacy (${match.w}\xD7${match.h}") \u2014 modern screens are 16:9.`,
5429
+ path: canvas.path,
5430
+ suggestion: "If 4:3 is not deliberate, use slideWidth 13.333 and slideHeight 7.5."
5431
+ }
5432
+ ];
5433
+ }
5434
+ if (match) return [];
5435
+ return [
5436
+ {
5437
+ code: QUALITY_CODES.CANVAS_NONSTANDARD,
5438
+ message: `Canvas ${width}\xD7${height}" matches no common preset (16:9, 1:1, 4:5, 9:16).`,
5439
+ path: canvas.path,
5440
+ suggestion: "Confirm the size is deliberate; a mistyped canvas distorts every slide.",
5441
+ context: {
5442
+ knownCanvases: KNOWN_CANVASES.map(({ w, h, label }) => ({
5443
+ slideWidth: w,
5444
+ slideHeight: h,
5445
+ label
5446
+ }))
5447
+ }
5448
+ }
5449
+ ];
5450
+ }
5451
+ };
5452
+ var pptxMinimumFontRule = {
5453
+ id: "pptx/minimum-font-size",
5454
+ code: QUALITY_CODES.FONT_SIZE_MIN,
5455
+ category: "legibility",
5456
+ defaultSeverity: "warning",
5457
+ defaultCertainty: "measured",
5458
+ formats: ["pptx"],
5459
+ defaultParameters: { minimumFontPt: DEFAULT_MIN_READABLE_FONT_PT },
5460
+ evaluate: ({ facts, configuration }) => {
5461
+ const minimum = numberParameter(
5462
+ configuration.parameters,
5463
+ "minimumFontPt",
5464
+ DEFAULT_MIN_READABLE_FONT_PT
5465
+ );
5466
+ return textFacts(facts).filter((fact) => fact.fontSizePt < minimum).map((fact) => ({
5467
+ message: `Effective font size is ${fact.fontSizePt}pt \u2014 unreadable on a projected slide.`,
5468
+ path: `${fact.path}/props`,
5469
+ suggestion: `Use at least ${minimum}pt; captions rarely work below 10pt.`,
5470
+ context: { fontSize: fact.fontSizePt, threshold: minimum },
5471
+ evidence: { actual: fact.fontSizePt, expected: minimum, unit: "pt" },
5472
+ // `add` replaces an existing member, so this lifts an explicit
5473
+ // fontSize and overrides an inherited style value alike.
5474
+ fixes: [
5475
+ {
5476
+ op: "add",
5477
+ path: `${fact.path}/props/fontSize`,
5478
+ value: minimum
5479
+ }
5480
+ ]
5481
+ }));
5482
+ }
5483
+ };
5484
+ function fittingFontSizePt(fact, charWidthFactor, minimumFontPt) {
5485
+ if (fact.autoFit === true) return void 0;
5486
+ const minimumWholeSize = Math.ceil(minimumFontPt);
5487
+ for (let size = Math.floor(fact.fontSizePt) - 1; size >= minimumWholeSize; size--) {
5488
+ const estimate = estimateTextHeightPt(
5489
+ { ...fact, fontSizePt: size },
5490
+ charWidthFactor
5491
+ );
5492
+ if (estimate !== void 0 && fact.boxHeightPt !== void 0 && estimate.heightPt <= fact.boxHeightPt) {
5493
+ return size;
5494
+ }
5495
+ }
5496
+ return void 0;
5497
+ }
5498
+ var pptxTextFitRule = {
5499
+ id: "pptx/text-fit",
5500
+ code: QUALITY_CODES.TEXT_TIGHT,
5501
+ category: "integrity",
5502
+ defaultSeverity: "info",
5503
+ defaultCertainty: "estimated",
5504
+ formats: ["pptx"],
5505
+ defaultParameters: {
5506
+ characterWidthFactor: DEFAULT_CHAR_WIDTH_FACTOR,
5507
+ safetyBufferPt: DEFAULT_SAFETY_BUFFER_PT
5508
+ },
5509
+ evaluate: ({ facts, configuration, profile, policy }) => {
5510
+ const factor = numberParameter(
5511
+ configuration.parameters,
5512
+ "characterWidthFactor",
5513
+ DEFAULT_CHAR_WIDTH_FACTOR
5514
+ );
5515
+ const safetyBufferPt = numberParameter(
5516
+ configuration.parameters,
5517
+ "safetyBufferPt",
5518
+ DEFAULT_SAFETY_BUFFER_PT
5519
+ );
5520
+ const minimumFontConfiguration = resolveRuleConfiguration(
5521
+ pptxMinimumFontRule,
5522
+ profile,
5523
+ policy
5524
+ );
5525
+ const minimumFontPt = minimumFontConfiguration.enabled ? numberParameter(
5526
+ minimumFontConfiguration.parameters,
5527
+ "minimumFontPt",
5528
+ DEFAULT_MIN_READABLE_FONT_PT
5529
+ ) : DEFAULT_MIN_READABLE_FONT_PT;
5530
+ const findings = [];
5531
+ for (const fact of textFacts(facts)) {
5532
+ const estimate = estimateTextHeightPt(fact, factor);
5533
+ if (!estimate || fact.boxHeightPt === void 0) continue;
5534
+ const marginPt = fact.boxHeightPt - estimate.heightPt;
5535
+ const measured = {
5536
+ estimatedTextPt: Math.round(estimate.heightPt * 10) / 10,
5537
+ availablePt: Math.round(fact.boxHeightPt * 10) / 10,
5538
+ marginPt: Math.round(marginPt * 10) / 10,
5539
+ estimatedLines: estimate.lines,
5540
+ fontSize: fact.fontSizePt,
5541
+ boxWidthPt: Math.round(fact.boxWidthPt * 10) / 10
5542
+ };
5543
+ if (marginPt < -fact.lineSpacingPt) {
5544
+ const fittingSize = fittingFontSizePt(fact, factor, minimumFontPt);
5545
+ findings.push({
5546
+ code: QUALITY_CODES.TEXT_OVERFLOW,
5547
+ severity: "warning",
5548
+ message: `Text is estimated at ${measured.estimatedTextPt}pt tall (${estimate.lines} line${estimate.lines === 1 ? "" : "s"} of ${fact.fontSizePt}pt) in a ${measured.availablePt}pt box \u2014 it will overflow.`,
5549
+ path: fact.path,
5550
+ suggestion: "Shorten the text, reduce fontSize, or enlarge the box (h / rowSpan).",
5551
+ context: measured,
5552
+ evidence: {
5553
+ actual: measured.estimatedTextPt,
5554
+ expected: measured.availablePt,
5555
+ unit: "pt"
5556
+ },
5557
+ // A ready-made patch only when a readable size fits; shortening
5558
+ // the text or growing the box stays the author's call.
5559
+ ...fittingSize !== void 0 && {
5560
+ fixes: [
5561
+ {
5562
+ op: "add",
5563
+ path: `${fact.path}/props/fontSize`,
5564
+ value: fittingSize
5565
+ }
5566
+ ]
5567
+ }
5568
+ });
5569
+ continue;
5570
+ }
5571
+ if (marginPt >= safetyBufferPt) continue;
5572
+ findings.push({
5573
+ code: QUALITY_CODES.TEXT_TIGHT,
5574
+ message: marginPt < 0 ? `Text is estimated to exceed its ${measured.availablePt}pt box by ${-measured.marginPt}pt \u2014 within one line-height, so likely a harmless spill into the gap below.` : `Text fits its box with only ${measured.marginPt}pt to spare \u2014 renderer rounding can push it over.`,
5575
+ path: fact.path,
5576
+ suggestion: `Leave at least ${safetyBufferPt}pt of vertical margin.`,
5577
+ context: measured,
5578
+ evidence: {
5579
+ actual: measured.marginPt,
5580
+ expected: safetyBufferPt,
5581
+ unit: "pt margin"
5582
+ }
5583
+ });
5584
+ }
5585
+ return findings;
5586
+ }
5587
+ };
5588
+ var pptxSlideDensityRule = {
5589
+ id: "pptx/slide-density",
5590
+ code: QUALITY_CODES.SLIDE_DENSITY,
5591
+ category: "information-design",
5592
+ defaultSeverity: "warning",
5593
+ defaultCertainty: "estimated",
5594
+ formats: ["pptx"],
5595
+ defaultParameters: { maximumBodyWords: DEFAULT_MAX_BODY_WORDS_PER_SLIDE },
5596
+ evaluate: ({ facts, configuration }) => {
5597
+ const threshold = numberParameter(
5598
+ configuration.parameters,
5599
+ "maximumBodyWords",
5600
+ DEFAULT_MAX_BODY_WORDS_PER_SLIDE
5601
+ );
5602
+ return facts.filter((fact) => fact.kind === "pptx/slide").filter((fact) => fact.bodyWords > threshold).map((fact) => ({
5603
+ message: `${fact.bodyWords} words of body text on one slide \u2014 an audience reads a slide, it does not study one.`,
5604
+ path: fact.path,
5605
+ suggestion: "One idea per slide: split the content across more slides.",
5606
+ context: { bodyWords: fact.bodyWords, threshold },
5607
+ evidence: {
5608
+ actual: fact.bodyWords,
5609
+ expected: threshold,
5610
+ unit: "words"
5611
+ }
5612
+ }));
5613
+ }
5614
+ };
5615
+ var AA_NORMAL_RATIO = 4.5;
5616
+ var AA_LARGE_RATIO = 3;
5617
+ var LARGE_TEXT_PT = 18;
5618
+ var LARGE_BOLD_TEXT_PT = 14;
5619
+ function channelLuminance(channel) {
5620
+ const c = channel / 255;
5621
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
5622
+ }
5623
+ function relativeLuminance(hex) {
5624
+ const match = /^#?([0-9a-f]{6})$/i.exec(hex);
5625
+ if (!match) return void 0;
5626
+ const value = parseInt(match[1], 16);
5627
+ return 0.2126 * channelLuminance(value >> 16 & 255) + 0.7152 * channelLuminance(value >> 8 & 255) + 0.0722 * channelLuminance(value & 255);
5628
+ }
5629
+ function contrastRatio(a, b) {
5630
+ const la = relativeLuminance(a);
5631
+ const lb = relativeLuminance(b);
5632
+ if (la === void 0 || lb === void 0) return void 0;
5633
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
5634
+ }
5635
+ var pptxTextContrastRule = {
5636
+ id: "pptx/text-contrast",
5637
+ code: QUALITY_CODES.TEXT_CONTRAST,
5638
+ category: "accessibility",
5639
+ defaultSeverity: "warning",
5640
+ defaultCertainty: "deterministic",
5641
+ formats: ["pptx"],
5642
+ defaultParameters: {
5643
+ normalRatio: AA_NORMAL_RATIO,
5644
+ largeRatio: AA_LARGE_RATIO,
5645
+ largeTextPt: LARGE_TEXT_PT,
5646
+ largeBoldTextPt: LARGE_BOLD_TEXT_PT
5647
+ },
5648
+ evaluate: ({ facts, configuration }) => {
5649
+ const normalRatio = numberParameter(
5650
+ configuration.parameters,
5651
+ "normalRatio",
5652
+ AA_NORMAL_RATIO
5653
+ );
5654
+ const largeRatio = numberParameter(
5655
+ configuration.parameters,
5656
+ "largeRatio",
5657
+ AA_LARGE_RATIO
5658
+ );
5659
+ const largeTextPt = numberParameter(
5660
+ configuration.parameters,
5661
+ "largeTextPt",
5662
+ LARGE_TEXT_PT
5663
+ );
5664
+ const largeBoldTextPt = numberParameter(
5665
+ configuration.parameters,
5666
+ "largeBoldTextPt",
5667
+ LARGE_BOLD_TEXT_PT
5668
+ );
5669
+ return textFacts(facts).flatMap((fact) => {
5670
+ const { colorHex, backgroundHexes } = fact;
5671
+ if (!colorHex || !backgroundHexes?.length) return [];
5672
+ let worst;
5673
+ for (const background2 of backgroundHexes) {
5674
+ const ratio = contrastRatio(colorHex, background2);
5675
+ if (ratio === void 0) continue;
5676
+ if (!worst || ratio < worst.ratio) worst = { ratio, background: background2 };
5677
+ }
5678
+ if (!worst) return [];
5679
+ const isLarge = fact.fontSizePt >= largeTextPt || fact.bold && fact.fontSizePt >= largeBoldTextPt;
5680
+ const required = isLarge ? largeRatio : normalRatio;
5681
+ if (worst.ratio >= required) return [];
5682
+ const rounded = Math.round(worst.ratio * 100) / 100;
5683
+ return [
5684
+ {
5685
+ message: `Text at #${colorHex} on #${worst.background} has ${rounded}:1 contrast \u2014 below the ${required}:1 needed at ${fact.fontSizePt}pt${fact.bold ? " bold" : ""}.`,
5686
+ path: fact.path,
5687
+ suggestion: "Darken the text, lighten it further, or change the surface behind it.",
5688
+ context: {
5689
+ colorHex,
5690
+ backgroundHex: worst.background,
5691
+ ratio: rounded,
5692
+ required,
5693
+ fontSizePt: fact.fontSizePt,
5694
+ bold: fact.bold,
5695
+ backgroundHexes
5696
+ },
5697
+ evidence: {
5698
+ actual: rounded,
5699
+ expected: required,
5700
+ unit: ":1"
5701
+ }
5702
+ }
5703
+ ];
5704
+ });
5705
+ }
5706
+ };
5707
+ var PPTX_QUALITY_RULES = {
5708
+ id: "pptx/default",
5709
+ rules: [
5710
+ pptxCanvasRule,
5711
+ pptxMinimumFontRule,
5712
+ pptxTextFitRule,
5713
+ pptxSlideDensityRule,
5714
+ pptxTextContrastRule
5715
+ ]
5716
+ };
5717
+ var PPTX_QUALITY_PROFILES = {
5718
+ "executive-presentation": {
5719
+ id: "executive-presentation",
5720
+ formats: ["pptx"],
5721
+ description: "Decision deck optimized for scan speed and projection.",
5722
+ rules: {
5723
+ "pptx/minimum-font-size": { parameters: { minimumFontPt: 14 } },
5724
+ "pptx/slide-density": { parameters: { maximumBodyWords: 70 } }
5725
+ }
5726
+ },
5727
+ "technical-presentation": {
5728
+ id: "technical-presentation",
5729
+ formats: ["pptx"],
5730
+ description: "Portable professional presentation defaults."
5731
+ }
5732
+ };
5733
+ var PPTX_DEFAULT_QUALITY_PROFILE = PPTX_QUALITY_PROFILES["technical-presentation"];
5734
+ var PPTX_PROFILES_BY_ID = PPTX_QUALITY_PROFILES;
5735
+ function resolvePptxQualityProfile(requested) {
5736
+ if (!requested) return void 0;
5737
+ const registered = PPTX_PROFILES_BY_ID[requested.id];
5738
+ if (!registered) return requested;
5739
+ return mergeQualityProfiles(registered, requested);
5740
+ }
5741
+ var pptxQualityEngine = new QualityEngine(PPTX_QUALITY_RULES.rules);
5742
+
5743
+ // src/quality/preflight.ts
5744
+ var PREPARE_RULE_ID = "quality/prepare";
5745
+ function emptyAnalysis(ruleErrors = [], blocked = false) {
5746
+ return {
5747
+ diagnostics: [],
5748
+ counts: { error: 0, warning: 0, info: 0 },
5749
+ blocked,
5750
+ truncated: false,
5751
+ suppressedCount: 0,
5752
+ evaluatedRuleIds: [],
5753
+ ruleErrors
5754
+ };
5755
+ }
5756
+ function analyzePptxQuality(doc, options = {}) {
5757
+ assertValidQualityPolicy(options.policy);
5758
+ assertValidQualityProfile(options.profile);
5759
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc) || doc.name !== "pptx") {
5760
+ return emptyAnalysis();
5761
+ }
5762
+ let prepared = options.prepared;
5763
+ try {
5764
+ prepared ??= preparePptxQualityDocument(
5765
+ doc,
5766
+ options
5767
+ );
5768
+ } catch (error) {
5769
+ if (options.policy?.onRuleError === "throw") throw error;
5770
+ const gate = options.policy?.gate;
5771
+ return emptyAnalysis(
5772
+ [
5773
+ {
5774
+ ruleId: PREPARE_RULE_ID,
5775
+ message: error instanceof Error ? error.message : String(error)
5776
+ }
5777
+ ],
5778
+ gate !== void 0 && gate !== "none"
5779
+ );
5780
+ }
5781
+ return pptxQualityEngine.analyzeSync(prepared, {
5782
+ profile: resolvePptxQualityProfile(options.profile) ?? PPTX_DEFAULT_QUALITY_PROFILE,
5783
+ policy: options.policy
5784
+ });
5785
+ }
5786
+
4871
5787
  // src/plugin/index.ts
4872
5788
  import {
4873
5789
  createComponent,
@@ -5375,10 +6291,14 @@ export {
5375
6291
  DEFAULT_PPTX_RENDERER_ID,
5376
6292
  DEFAULT_PPTX_THEME,
5377
6293
  DuplicateComponentError,
6294
+ PPTX_DEFAULT_QUALITY_PROFILE,
6295
+ PPTX_QUALITY_PROFILES,
6296
+ PPTX_QUALITY_RULES,
5378
6297
  PresentationGenerator,
5379
6298
  PresentationValidationError,
5380
6299
  UncompiledComponentError,
5381
6300
  W as WarningCodes,
6301
+ analyzePptxQuality,
5382
6302
  cleanComponentProps,
5383
6303
  createComponent,
5384
6304
  createPresentationGenerator,
@@ -5395,9 +6315,13 @@ export {
5395
6315
  isPresentationComponent,
5396
6316
  isPresentationComponentDefinition,
5397
6317
  isSlideComponent,
6318
+ pptxQualityEngine,
5398
6319
  pptxRendererIds,
6320
+ pptxRendererStatuses,
5399
6321
  pptxThemes,
6322
+ preparePptxQualityDocument,
5400
6323
  resolveComponentVersion3 as resolveComponentVersion,
6324
+ resolvePptxQualityProfile,
5401
6325
  validateComponentProps,
5402
6326
  validatePresentation
5403
6327
  };