@json-to-office/core-pptx 6.8.0 → 7.0.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
@@ -5163,36 +5163,8 @@ var DEFAULT_STYLES = {
5163
5163
  body: { fontSize: 14 },
5164
5164
  caption: { fontSize: 10, italic: true, fontColor: "text2" }
5165
5165
  };
5166
- var DEFAULT_PPTX_THEME = {
5167
- name: "default",
5168
- displayName: "Office Default",
5169
- description: "The stock Office look: blue primary, orange and green accents, Arial throughout, centred titles",
5170
- whenToUse: "A deck that has to match a plain PowerPoint look; pick a designed theme for anything a client will see.",
5171
- colors: {
5172
- primary: "#4472C4",
5173
- secondary: "#ED7D31",
5174
- accent: "#70AD47",
5175
- background: "#FFFFFF",
5176
- text: "#333333",
5177
- text2: "#44546A",
5178
- background2: "#E7E6E6",
5179
- accent4: "#FFC000",
5180
- accent5: "#5B9BD5",
5181
- accent6: "#70AD47"
5182
- },
5183
- fonts: {
5184
- heading: "Arial",
5185
- body: "Arial"
5186
- },
5187
- defaults: {
5188
- fontSize: 18,
5189
- fontColor: "#333333"
5190
- },
5191
- styles: DEFAULT_STYLES,
5192
- componentDefaults: { table: DEFAULT_TABLE }
5193
- };
5166
+ var DEFAULT_PPTX_THEME = CONSULTING_PPTX_THEME;
5194
5167
  var PPTX_THEMES = {
5195
- default: DEFAULT_PPTX_THEME,
5196
5168
  consulting: CONSULTING_PPTX_THEME,
5197
5169
  vermilion: VERMILION_PPTX_THEME,
5198
5170
  devportal: DEVPORTAL_PPTX_THEME,
@@ -5332,7 +5304,7 @@ function resolveThemeContext(documentIn, options = {}) {
5332
5304
  inlineTheme = document.props.theme;
5333
5305
  }
5334
5306
  const authoredThemeName = typeof document.props.theme === "string" ? document.props.theme : void 0;
5335
- const baseThemeName = inlineTheme ? inlineTheme.name || "inline-theme" : authoredThemeName ?? defaultThemeName ?? "default";
5307
+ const baseThemeName = inlineTheme ? inlineTheme.name || "inline-theme" : authoredThemeName ?? defaultThemeName ?? "consulting";
5336
5308
  let theme = inlineTheme ?? (resolveNamedTheme ? resolveNamedTheme(baseThemeName, authoredThemeName !== void 0) : customThemes?.[baseThemeName] ?? getPptxTheme(baseThemeName));
5337
5309
  theme = resolvePptxDesignSystem(
5338
5310
  theme,
@@ -5913,10 +5885,22 @@ async function expandPptxBlocksWithPlugins(document, theme, plugins, render, pre
5913
5885
  preserve
5914
5886
  });
5915
5887
  const finished = finishPptxBlocks(composed.standard, evaluator, effects2);
5916
- return { ...finished, preserved: composed.preserved };
5888
+ return {
5889
+ ...finished,
5890
+ preserved: composed.preserved,
5891
+ pluginOutputs: evaluator.pluginOutputs
5892
+ };
5917
5893
  }
5918
5894
 
5919
5895
  // src/quality/facts.ts
5896
+ function textInsetsPt(props, componentName) {
5897
+ const margin = props.margin;
5898
+ if (typeof margin === "number" && Number.isFinite(margin))
5899
+ return [margin, margin, margin, margin];
5900
+ if (Array.isArray(margin) && margin.length === 4 && margin.every((side) => typeof side === "number" && Number.isFinite(side)))
5901
+ return margin;
5902
+ return componentName === "shape" ? [3.6, 7.2, 3.6, 7.2] : [0, 0, 0, 0];
5903
+ }
5920
5904
  function asRecord(value) {
5921
5905
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
5922
5906
  }
@@ -6174,7 +6158,13 @@ function collectSlideNodes(component, path4, text, richText, surfaces, boxes, co
6174
6158
  const content = typeof props.text === "string" ? props.text : void 0;
6175
6159
  if (content !== void 0 && content.trim() !== "" && props.runs === void 0) {
6176
6160
  if (rec.name === "text" || rec.name === "shape") {
6177
- text.push({ props, path: path4, text: content, order });
6161
+ text.push({
6162
+ props,
6163
+ path: path4,
6164
+ componentName: rec.name,
6165
+ text: content,
6166
+ order
6167
+ });
6178
6168
  }
6179
6169
  }
6180
6170
  if (Array.isArray(props.runs) && (rec.name === "text" || rec.name === "shape")) {
@@ -6182,7 +6172,13 @@ function collectSlideNodes(component, path4, text, richText, surfaces, boxes, co
6182
6172
  (run) => run && typeof run.text === "string" ? `${run.text}${run.breakLine === true ? "\n" : ""}` : ""
6183
6173
  ).join("");
6184
6174
  if (joined.trim() !== "")
6185
- richText.push({ props, path: path4, text: joined, order });
6175
+ richText.push({
6176
+ props,
6177
+ path: path4,
6178
+ componentName: String(rec.name),
6179
+ text: joined,
6180
+ order
6181
+ });
6186
6182
  }
6187
6183
  const componentName = typeof rec.name === "string" ? rec.name : "";
6188
6184
  if (componentName === "chart" || componentName === "highcharts" || componentName === "table") {
@@ -6357,6 +6353,16 @@ function tableFact(node, slidePath, authoredProps) {
6357
6353
  }
6358
6354
  return { ...base, columns };
6359
6355
  }
6356
+ function authoredNameAtPointer(root, pointer) {
6357
+ let node = root;
6358
+ for (const token of pointer.split("/").slice(1)) {
6359
+ const key = token.replace(/~1/g, "/").replace(/~0/g, "~");
6360
+ node = Array.isArray(node) ? node[Number(key)] : asRecord(node)?.[key];
6361
+ if (node === void 0) return void 0;
6362
+ }
6363
+ const name = asRecord(node)?.name;
6364
+ return typeof name === "string" ? name : void 0;
6365
+ }
6360
6366
  function authoredPropsAtPointer(root, pointer) {
6361
6367
  let node = root;
6362
6368
  for (const token of pointer.split("/").slice(1)) {
@@ -6374,7 +6380,7 @@ function authoredPropsAtPointer(root, pointer) {
6374
6380
  }
6375
6381
  return asRecord(asRecord(node)?.props);
6376
6382
  }
6377
- function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn, slideHeightIn, ctx, theme, slideBackground, analyzedTextPaths, analyzedContentPaths, paletteTokens, authoredPropsAt, statedPropsAt, authoredPath, addFact) {
6383
+ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn, slideHeightIn, ctx, theme, slideBackground, analyzedTextPaths, analyzedContentPaths, paletteTokens, authoredPropsAt, statedPropsAt, authoredPath, addFact, ownsProps = () => true) {
6378
6384
  const nodes = [];
6379
6385
  const richNodes = [];
6380
6386
  const surfaces = [];
@@ -6406,6 +6412,8 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6406
6412
  statedPropsAt
6407
6413
  );
6408
6414
  if (!isCompleteBox(box)) return;
6415
+ const onSlide = /^\/children\/\d+\/children\/\d+$/.test(node.path) && authoredPath(node.path) === node.path;
6416
+ const authored = onSlide ? authoredPropsAt(node.path) : void 0;
6409
6417
  addFact({
6410
6418
  id: `pptx:box:${renderedIndex}:${boxIndex}:${node.path}`,
6411
6419
  kind: "pptx/box",
@@ -6418,7 +6426,10 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6418
6426
  xPt: box.xPt,
6419
6427
  yPt: box.yPt,
6420
6428
  widthPt: box.widthPt,
6421
- heightPt: box.heightPt
6429
+ heightPt: box.heightPt,
6430
+ ...typeof authored?.x === "number" && typeof authored?.y === "number" && authored.grid === void 0 && {
6431
+ authoredPositionIn: { x: authored.x, y: authored.y }
6432
+ }
6422
6433
  });
6423
6434
  });
6424
6435
  const surfaceBoxes = surfaces.flatMap((surface) => {
@@ -6522,6 +6533,8 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6522
6533
  }
6523
6534
  }
6524
6535
  const colorHex = typeof node.props.color === "string" ? resolveColor(node.props.color, theme)?.toUpperCase() : void 0;
6536
+ const stated = statedPropsAt(node.path);
6537
+ const setSize = stated && node.props.fit !== void 0 ? resolveTypography(stated, ctx).fontSize : void 0;
6525
6538
  addFact({
6526
6539
  id: `pptx:text:${renderedIndex}:${nodeIndex}:${node.path}`,
6527
6540
  kind: "pptx/text",
@@ -6531,6 +6544,7 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6531
6544
  nodePath: node.path,
6532
6545
  text: node.text,
6533
6546
  fontSizePt: typography.fontSize,
6547
+ ...setSize !== void 0 && setSize !== typography.fontSize && { fitFromPt: setSize },
6534
6548
  lineSpacingPt: typography.lineSpacing,
6535
6549
  paraSpaceBeforePt: typography.paraSpaceBefore,
6536
6550
  paraSpaceAfterPt: typography.paraSpaceAfter,
@@ -6539,6 +6553,7 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6539
6553
  ...boxYPt !== void 0 && { boxYPt },
6540
6554
  ...boxWidthPt !== void 0 && boxWidthPt > 0 && { boxWidthPt },
6541
6555
  ...boxHeightPt !== void 0 && boxHeightPt > 0 && { boxHeightPt },
6556
+ insetsPt: textInsetsPt(node.props, node.componentName),
6542
6557
  verticalAlign: node.props.valign === "middle" || node.props.valign === "bottom" ? node.props.valign : "top",
6543
6558
  align: horizontalAlign(node.props, ctx),
6544
6559
  rotationDeg: asNumber(node.props.rotate) ?? 0,
@@ -6550,6 +6565,7 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6550
6565
  sizePath: `${node.path}/props/fontSize`
6551
6566
  },
6552
6567
  generated: authoredPath(node.path) !== node.path,
6568
+ ownsProps: ownsProps(node.path),
6553
6569
  autoFit: node.props.h === void 0 && gridPos === void 0,
6554
6570
  ...colorHex !== void 0 && { colorHex },
6555
6571
  ...!backgroundUnknown && backgroundHexes.length > 0 && { backgroundHexes }
@@ -6630,7 +6646,10 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6630
6646
  ...stretched && isCompleteBox(resolved) && resolved.heightPt > 0 && {
6631
6647
  drawnRatio: resolved.widthPt / resolved.heightPt
6632
6648
  },
6633
- ...natural !== void 0 && { naturalRatio: natural }
6649
+ ...natural !== void 0 && { naturalRatio: natural },
6650
+ ...stretched && typeof authored.w === "number" && typeof authored.h === "number" && {
6651
+ authoredSizeIn: { w: authored.w, h: authored.h }
6652
+ }
6634
6653
  });
6635
6654
  }
6636
6655
  const bullets = bulletItems(box.props);
@@ -6651,7 +6670,7 @@ function preparePptxQualityDocument(document, options = {}) {
6651
6670
  const facts = [];
6652
6671
  const provenance = {};
6653
6672
  let sourceMap = {};
6654
- const authoredPath = (path4) => toAuthoredPointer(sourceMap, path4);
6673
+ const authoredPath = (path4) => toAuthoredPointer(sourceMap, path4, options.expanded?.pluginOutputs);
6655
6674
  const addFact = (raw) => {
6656
6675
  const fact = {
6657
6676
  ...raw,
@@ -6698,12 +6717,12 @@ function preparePptxQualityDocument(document, options = {}) {
6698
6717
  });
6699
6718
  });
6700
6719
  const warnings = options.warnings ?? [];
6701
- const context = resolveThemeContext(document, {
6720
+ const context = options.context ?? resolveThemeContext(document, {
6702
6721
  customThemes: options.customThemes,
6703
6722
  fonts: options.fonts,
6704
6723
  warnings
6705
6724
  });
6706
- const expanded = expandPptxBlocks(context.document, context.theme);
6725
+ const expanded = options.expanded ?? expandPptxBlocks(context.document, context.theme);
6707
6726
  sourceMap = expanded.sourceMap;
6708
6727
  const processed = processPresentation(expanded.document, {
6709
6728
  theme: context.theme,
@@ -6797,6 +6816,10 @@ function preparePptxQualityDocument(document, options = {}) {
6797
6816
  (value) => `#${resolveColor(value, processed.theme)}`
6798
6817
  ) ?? SERIES_COLOR_TOKENS.filter((token) => paletteHexes[token] !== void 0);
6799
6818
  const authoredPropsAt = (pointer) => authoredPropsAtPointer(document, authoredPath(pointer));
6819
+ const pluginOutputs = options.expanded?.pluginOutputs ?? [];
6820
+ const ownsProps = (pointer) => !pluginOutputs.some(
6821
+ (prefix) => pointer === prefix || pointer.startsWith(`${prefix}/`)
6822
+ ) && authoredNameAtPointer(document, authoredPath(pointer)) !== "block";
6800
6823
  const statedPropsAt = (pointer) => authoredPropsAtPointer(expanded.document, pointer);
6801
6824
  let exportedPages = 0;
6802
6825
  processed.slides.forEach((slide, renderedIndex) => {
@@ -6834,7 +6857,8 @@ function preparePptxQualityDocument(document, options = {}) {
6834
6857
  authoredPropsAt,
6835
6858
  statedPropsAt,
6836
6859
  authoredPath,
6837
- addFact
6860
+ addFact,
6861
+ ownsProps
6838
6862
  );
6839
6863
  });
6840
6864
  for (const budget of blockSlotBudgets(context.document, expanded.blocks)) {
@@ -6844,10 +6868,10 @@ function preparePptxQualityDocument(document, options = {}) {
6844
6868
  ...budget
6845
6869
  });
6846
6870
  }
6847
- const slotTextNodes = textNodesBySlot(processed, slideIndexes, sourceMap);
6871
+ const slotNodes = nodesBySlot(processed, slideIndexes, sourceMap);
6848
6872
  for (const role of blockSlotRoles(context.document, expanded.blocks)) {
6849
6873
  const present = role.value !== void 0 && role.value !== null && role.value !== false && (typeof role.value !== "string" || role.value.trim() !== "") && (!Array.isArray(role.value) || role.value.length > 0);
6850
- const bound = slotTextNodes.get(role.path);
6874
+ const bound = slotNodes.get(role.path);
6851
6875
  let measured;
6852
6876
  if (bound && typeof bound.props.text === "string") {
6853
6877
  const typography = resolveTypography(bound.props, ctx);
@@ -6885,6 +6909,30 @@ function preparePptxQualityDocument(document, options = {}) {
6885
6909
  ...measured
6886
6910
  });
6887
6911
  }
6912
+ const footerType = typeof processed.theme.chrome?.confidentialFooter?.type === "string" ? processed.theme.chrome.confidentialFooter.type : "footer";
6913
+ processed.slides.forEach((slide, renderedIndex) => {
6914
+ const authoredIndex = slideIndexes[renderedIndex];
6915
+ if (authoredIndex === void 0) return;
6916
+ const slidePath = `/children/${authoredIndex}`;
6917
+ const onSlide = (fact) => "slidePath" in fact && fact.slidePath === slidePath;
6918
+ const texts = facts.filter(
6919
+ (fact) => (fact.kind === "pptx/text" || fact.kind === "pptx/rich-text") && onSlide(fact)
6920
+ );
6921
+ addFact({
6922
+ id: `pptx:slide-chrome:${slidePath}`,
6923
+ kind: "pptx/slide-chrome",
6924
+ path: slidePath,
6925
+ index: renderedIndex,
6926
+ ...slide.hidden === true && { hidden: true },
6927
+ blocks: expanded.blocks.filter(
6928
+ (pointer) => pointer.startsWith(`${slidePath}/children/`)
6929
+ ).length,
6930
+ pageNumber: texts.some((fact) => fact.text.includes("{PAGE_NUMBER}")),
6931
+ footer: texts.some((fact) => fact.styleName === footerType) || facts.some(
6932
+ (fact) => fact.kind === "pptx/chrome-slot" && fact.role === "footer" && fact.present && onSlide(fact)
6933
+ )
6934
+ });
6935
+ });
6888
6936
  return {
6889
6937
  format: "pptx",
6890
6938
  model: {
@@ -6907,7 +6955,7 @@ function preparePptxQualityDocument(document, options = {}) {
6907
6955
  }
6908
6956
  };
6909
6957
  }
6910
- function textNodesBySlot(processed, slideIndexes, sourceMap) {
6958
+ function nodesBySlot(processed, slideIndexes, sourceMap) {
6911
6959
  const found = /* @__PURE__ */ new Map();
6912
6960
  const visit = (component, path4) => {
6913
6961
  if (component.enabled === false) return;
@@ -6915,6 +6963,10 @@ function textNodesBySlot(processed, slideIndexes, sourceMap) {
6915
6963
  const origin = toAuthoredPointer(sourceMap, `${path4}/props/text`);
6916
6964
  if (origin !== `${path4}/props/text` && !found.has(origin))
6917
6965
  found.set(origin, { props: asRecord(component.props) ?? {}, path: path4 });
6966
+ } else {
6967
+ const origin = toAuthoredPointer(sourceMap, path4);
6968
+ if (origin !== path4 && origin.includes("/props/slots/") && !found.has(origin))
6969
+ found.set(origin, { props: asRecord(component.props) ?? {}, path: path4 });
6918
6970
  }
6919
6971
  (component.children ?? []).forEach(
6920
6972
  (child, index) => visit(child, `${path4}/children/${index}`)
@@ -7673,6 +7725,8 @@ import {
7673
7725
  // src/quality/rules.ts
7674
7726
  import {
7675
7727
  chartInfoDesignFindings,
7728
+ configurationLabel,
7729
+ configurationSource,
7676
7730
  DEFAULT_MAXIMUM_CHART_SERIES,
7677
7731
  DEFAULT_MAXIMUM_PIE_SLICES,
7678
7732
  fontCountFinding,
@@ -7813,19 +7867,24 @@ var pptxMinimumFontRule = {
7813
7867
  );
7814
7868
  return textFacts(facts).filter((fact) => fact.fontSizePt < minimum).map((fact) => ({
7815
7869
  message: `Effective font size is ${fact.fontSizePt}pt \u2014 unreadable on a projected slide.`,
7816
- path: `${fact.path}/props`,
7870
+ // Text a definition or a plugin generated reports at the invocation
7871
+ // the author wrote, whose props are the block's or the plugin's: no
7872
+ // fontSize of theirs to lift, so no patch.
7873
+ path: fact.ownsProps ? `${fact.path}/props` : fact.path,
7817
7874
  suggestion: `Use at least ${minimum}pt; captions rarely work below 10pt.`,
7818
7875
  context: { fontSize: fact.fontSizePt, threshold: minimum },
7819
7876
  evidence: { actual: fact.fontSizePt, expected: minimum, unit: "pt" },
7820
7877
  // `add` replaces an existing member, so this lifts an explicit
7821
7878
  // fontSize and overrides an inherited style value alike.
7822
- fixes: [
7823
- {
7824
- op: "add",
7825
- path: `${fact.path}/props/fontSize`,
7826
- value: minimum
7827
- }
7828
- ]
7879
+ ...fact.ownsProps && {
7880
+ fixes: [
7881
+ {
7882
+ op: "add",
7883
+ path: `${fact.path}/props/fontSize`,
7884
+ value: minimum
7885
+ }
7886
+ ]
7887
+ }
7829
7888
  }));
7830
7889
  }
7831
7890
  };
@@ -8429,18 +8488,67 @@ var pptxRequiredChromeRule = {
8429
8488
  if (required.length === 0) return [];
8430
8489
  return facts.filter(
8431
8490
  (fact) => fact.kind === "pptx/chrome-slot"
8432
- ).filter((fact) => required.includes(fact.role) && !fact.present).map((fact) => ({
8433
- path: fact.path,
8434
- relatedPaths: [fact.invocation],
8435
- message: `${fact.block} states no ${fact.role} in its "${fact.slot}" slot; the ${profile?.id ?? "selected"} profile expects one on every ${fact.block}.`,
8436
- suggestion: `Fill the "${fact.slot}" slot. The theme already styles it.`,
8437
- context: { block: fact.block, slot: fact.slot, role: fact.role },
8438
- evidence: {
8439
- actual: "empty",
8440
- expected: fact.role,
8441
- values: { source: "profile", required }
8442
- }
8443
- }));
8491
+ ).filter((fact) => required.includes(fact.role) && !fact.present).map((fact) => {
8492
+ const source = configurationSource(configuration, "required");
8493
+ return {
8494
+ path: fact.path,
8495
+ relatedPaths: [fact.invocation],
8496
+ message: `${fact.block} states no ${fact.role} in its "${fact.slot}" slot; ${configurationLabel(source, profile)} expects one on every ${fact.block}.`,
8497
+ suggestion: `Fill the "${fact.slot}" slot. The theme already styles it.`,
8498
+ context: { block: fact.block, slot: fact.slot, role: fact.role },
8499
+ evidence: {
8500
+ actual: "empty",
8501
+ expected: fact.role,
8502
+ values: { source, required }
8503
+ }
8504
+ };
8505
+ });
8506
+ }
8507
+ };
8508
+ var SLIDE_CHROME_PARTS = ["pageNumber", "footer"];
8509
+ var pptxSlideFooterRule = {
8510
+ id: "pptx/slide-footer",
8511
+ description: "A block-built slide without the running chrome a profile or policy expects: a page number, a footer line. Off until one names parts.",
8512
+ code: QUALITY_CODES.CHROME_MISSING,
8513
+ category: "consistency",
8514
+ defaultSeverity: "warning",
8515
+ defaultCertainty: "deterministic",
8516
+ formats: ["pptx"],
8517
+ defaultParameters: { required: [], fromSlide: 1 },
8518
+ evaluate: ({ facts, configuration, profile }) => {
8519
+ const required = stringListParameter(
8520
+ configuration.parameters,
8521
+ "required"
8522
+ ).filter(
8523
+ (part) => SLIDE_CHROME_PARTS.includes(part)
8524
+ );
8525
+ if (required.length === 0) return [];
8526
+ const from = numberParameter(configuration.parameters, "fromSlide", 1);
8527
+ return facts.filter(
8528
+ (fact) => fact.kind === "pptx/slide-chrome" && fact.blocks > 0 && fact.hidden !== true && fact.index >= from
8529
+ ).flatMap((fact) => {
8530
+ const missing = required.filter((part) => !fact[part]);
8531
+ if (missing.length === 0) return [];
8532
+ const parts = missing.map((part) => part === "pageNumber" ? "page number" : "footer").join(" or ");
8533
+ const source = configurationSource(
8534
+ configuration,
8535
+ "required",
8536
+ "fromSlide"
8537
+ );
8538
+ return [
8539
+ {
8540
+ path: fact.path,
8541
+ message: `Slide ${fact.index + 1} carries no ${parts}; ${configurationLabel(source, profile)} expects one on every slide a block builds from slide ${from + 1} on.`,
8542
+ suggestion: "Build the slide with a block that draws the running footer \u2014 the house blocks set {PAGE_NUMBER} / {PAGE_COUNT} in the footer role \u2014 or add that text to the definition the slide invokes.",
8543
+ context: { slide: fact.index, missing },
8544
+ evidence: {
8545
+ actual: required.filter((part) => fact[part]),
8546
+ expected: required,
8547
+ values: { source, fromSlide: from }
8548
+ }
8549
+ }
8550
+ ];
8551
+ });
8444
8552
  }
8445
8553
  };
8446
8554
  var pptxActionTitleRule = {
@@ -8473,6 +8581,8 @@ var pptxActionTitleRule = {
8473
8581
  }
8474
8582
  };
8475
8583
  var TITLE_DRIFT_TOLERANCE_PT = 2;
8584
+ var FIRST_BASELINE_EM = 0.8;
8585
+ var TITLE_PLACEMENT_LINES = 2;
8476
8586
  var PPTX_TYPE_VOCABULARY = {
8477
8587
  subject: "deck",
8478
8588
  keepTo: "Keep to the theme styles \u2014 title, heading, body, label, statistic \u2014 and drop the ad-hoc sizes."
@@ -8549,11 +8659,15 @@ var pptxSizeCountRule = {
8549
8659
  defaultParameters: { maximumSizes: 8, maximumSizesPerSlide: 0 },
8550
8660
  evaluate: ({ facts, configuration, profile }) => {
8551
8661
  const sizes = paintedSizes(facts);
8662
+ const setBy = (parameter) => {
8663
+ const source = configurationSource(configuration, parameter);
8664
+ return { source, label: configurationLabel(source, profile) };
8665
+ };
8552
8666
  const findings = sizeCountFinding(
8553
8667
  sizes,
8554
8668
  numberParameter(configuration.parameters, "maximumSizes", 8),
8555
8669
  pptxThemeFact(facts)?.path ?? "/props",
8556
- profile?.id,
8670
+ setBy("maximumSizes"),
8557
8671
  PPTX_TYPE_VOCABULARY
8558
8672
  ).map((finding) => ({
8559
8673
  ...finding,
@@ -8576,7 +8690,7 @@ var pptxSizeCountRule = {
8576
8690
  onSlide,
8577
8691
  perSlide,
8578
8692
  slidePath,
8579
- profile?.id,
8693
+ setBy("maximumSizesPerSlide"),
8580
8694
  PPTX_SLIDE_TYPE_VOCABULARY
8581
8695
  ))
8582
8696
  findings.push({
@@ -8600,53 +8714,107 @@ var pptxRoleDriftRule = {
8600
8714
  return theme ? roleDriftFindings(paintedSizes(facts), theme.roleSizesPt) : [];
8601
8715
  }
8602
8716
  };
8603
- function titleGroups(facts, styles) {
8604
- const texts = textFacts(facts).filter(
8605
- (fact) => fact.boxXPt !== void 0 && fact.boxYPt !== void 0
8717
+ var EDGE_NAMES = {
8718
+ left: "left edge",
8719
+ center: "centre line",
8720
+ right: "right edge"
8721
+ };
8722
+ function laidOutTitle(fact, path4, order) {
8723
+ if (fact.boxXPt === void 0 || fact.boxYPt === void 0 || fact.boxWidthPt === void 0 || fact.rotationDeg % 360 !== 0)
8724
+ return void 0;
8725
+ const [top, right, bottom, left] = fact.insetsPt;
8726
+ const innerWidth = Math.max(1, fact.boxWidthPt - left - right);
8727
+ const { heightPt, lines } = estimateTextHeightPt(
8728
+ fact.text,
8729
+ innerWidth,
8730
+ fact.fontSizePt,
8731
+ fact.lineSpacingPt,
8732
+ fact.paraSpaceBeforePt,
8733
+ fact.paraSpaceAfterPt
8606
8734
  );
8607
- const groups = /* @__PURE__ */ new Map();
8735
+ let textTop = fact.boxYPt + top;
8736
+ if (fact.boxHeightPt !== void 0 && !fact.autoFit) {
8737
+ const room = fact.boxHeightPt - top - bottom - heightPt;
8738
+ if (fact.verticalAlign === "middle") textTop += room / 2;
8739
+ else if (fact.verticalAlign === "bottom") textTop += room;
8740
+ }
8741
+ const align = fact.align === "justify" ? "left" : fact.align;
8742
+ const edgePt = align === "center" ? fact.boxXPt + left + innerWidth / 2 : align === "right" ? fact.boxXPt + fact.boxWidthPt - right : fact.boxXPt + left;
8743
+ const style = fact.styleName ?? "unstyled";
8744
+ const setSizePt = fact.fitFromPt ?? fact.fontSizePt;
8745
+ return {
8746
+ fact,
8747
+ path: path4,
8748
+ kind: `${style}|${setSizePt}|${align}`,
8749
+ style,
8750
+ setSizePt,
8751
+ align,
8752
+ lines,
8753
+ baselinePt: textTop + FIRST_BASELINE_EM * fact.fontSizePt,
8754
+ edgePt,
8755
+ order
8756
+ };
8757
+ }
8758
+ function deckTitles(facts, styles) {
8759
+ const texts = textFacts(facts).filter((fact) => !fact.slideHidden);
8760
+ const slideIndex = (fact) => Number(/^\/children\/(\d+)/.exec(fact.slidePath)?.[1] ?? 0);
8761
+ const order = new Map(
8762
+ texts.map((fact, index) => [fact, slideIndex(fact) * 1e4 + index])
8763
+ );
8764
+ const byNode = new Map(texts.map((fact) => [fact.nodePath, fact]));
8765
+ const titles = [];
8608
8766
  const claimed = /* @__PURE__ */ new Set();
8609
- const push = (key, fact) => {
8767
+ const add = (fact, path4) => {
8610
8768
  if (claimed.has(fact)) return;
8611
8769
  claimed.add(fact);
8612
- groups.set(key, [...groups.get(key) ?? [], fact]);
8770
+ const title = laidOutTitle(fact, path4, order.get(fact));
8771
+ if (title) titles.push(title);
8613
8772
  };
8614
- for (const slot of facts) {
8615
- if (slot.kind !== "pptx/chrome-slot" || slot.role !== "actionTitle")
8616
- continue;
8617
- const chrome = slot;
8618
- if (chrome.text === void 0) continue;
8619
- const painted = texts.find(
8620
- (fact) => fact.slidePath === chrome.slidePath && fact.text === chrome.text
8621
- );
8622
- if (painted) push(chrome.block, painted);
8623
- }
8773
+ for (const slot of facts)
8774
+ if (slot.kind === "pptx/chrome-slot" && slot.role === "actionTitle" && slot.nodePath !== void 0) {
8775
+ const painted = byNode.get(slot.nodePath);
8776
+ if (painted) add(painted, slot.path);
8777
+ }
8624
8778
  for (const fact of texts)
8625
8779
  if (fact.styleName !== void 0 && styles.includes(fact.styleName))
8626
- push(`style:${fact.styleName}`, fact);
8627
- return groups;
8628
- }
8629
- function prevailing(values) {
8630
- const counts = /* @__PURE__ */ new Map();
8631
- for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
8632
- let best;
8633
- let bestCount = 0;
8634
- for (const value of values) {
8635
- const count = counts.get(value);
8636
- if (count > bestCount) {
8637
- best = value;
8638
- bestCount = count;
8780
+ add(fact, fact.path);
8781
+ return titles.sort((a, b) => a.order - b.order);
8782
+ }
8783
+ function titlePlacements(titles, tolerance) {
8784
+ const lands = (a, b) => Math.abs(a.baselinePt - b.baselinePt) <= tolerance && Math.abs(a.edgePt - b.edgePt) <= tolerance;
8785
+ const byKind = /* @__PURE__ */ new Map();
8786
+ for (const title of titles)
8787
+ byKind.set(title.kind, [...byKind.get(title.kind) ?? [], title]);
8788
+ const placements = [];
8789
+ for (const kind of byKind.values()) {
8790
+ let pending = kind;
8791
+ while (pending.length > 0) {
8792
+ let reference = pending[0];
8793
+ let most = 0;
8794
+ for (const candidate of pending) {
8795
+ const count = pending.filter((other) => lands(other, candidate)).length;
8796
+ if (count > most) {
8797
+ reference = candidate;
8798
+ most = count;
8799
+ }
8800
+ }
8801
+ const reach = TITLE_PLACEMENT_LINES * reference.fact.lineSpacingPt;
8802
+ const members = pending.filter(
8803
+ (title) => Math.abs(title.baselinePt - reference.baselinePt) <= reach && Math.abs(title.edgePt - reference.edgePt) <= reach
8804
+ );
8805
+ placements.push({ reference, members });
8806
+ pending = pending.filter((title) => !members.includes(title));
8639
8807
  }
8640
8808
  }
8641
- return best;
8809
+ return placements;
8642
8810
  }
8643
8811
  var pptxTitleDriftRule = {
8644
8812
  id: "pptx/title-drift",
8645
- description: "A slide title away from the left edge or baseline the deck\u2019s other titles of that kind share. Off until a profile or policy enables it.",
8813
+ description: "A slide title whose laid-out first baseline or aligned edge is away from where the deck\u2019s other titles of that kind land. Off until a profile or policy enables it.",
8646
8814
  code: QUALITY_CODES.TITLE_DRIFT,
8647
8815
  category: "consistency",
8648
8816
  defaultSeverity: "warning",
8649
- defaultCertainty: "deterministic",
8817
+ defaultCertainty: "estimated",
8650
8818
  formats: ["pptx"],
8651
8819
  defaultEnabled: false,
8652
8820
  defaultParameters: {
@@ -8661,40 +8829,53 @@ var pptxTitleDriftRule = {
8661
8829
  TITLE_DRIFT_TOLERANCE_PT
8662
8830
  );
8663
8831
  const findings = [];
8664
- for (const [kind, titles] of titleGroups(facts, styles)) {
8665
- if (titles.length < 2) continue;
8666
- const axes = [
8667
- {
8668
- key: "left edge",
8669
- prop: "boxXPt",
8670
- value: prevailing(titles.map((fact) => fact.boxXPt))
8671
- },
8672
- {
8673
- key: "baseline",
8674
- prop: "boxYPt",
8675
- value: prevailing(titles.map((fact) => fact.boxYPt))
8676
- }
8677
- ];
8678
- for (const fact of titles) {
8679
- const off = axes.filter(
8680
- (axis) => axis.value !== void 0 && Math.abs(fact[axis.prop] - axis.value) > tolerance
8832
+ for (const { reference, members } of titlePlacements(
8833
+ deckTitles(facts, styles),
8834
+ tolerance
8835
+ )) {
8836
+ if (members.length < 2) continue;
8837
+ const agreeing = members.filter(
8838
+ (title) => Math.abs(title.baselinePt - reference.baselinePt) <= tolerance && Math.abs(title.edgePt - reference.edgePt) <= tolerance
8839
+ );
8840
+ for (const title of members) {
8841
+ if (agreeing.includes(title)) continue;
8842
+ const edge = EDGE_NAMES[title.align];
8843
+ const off = [
8844
+ {
8845
+ axis: "first baseline",
8846
+ actual: title.baselinePt,
8847
+ expected: reference.baselinePt
8848
+ },
8849
+ { axis: edge, actual: title.edgePt, expected: reference.edgePt }
8850
+ ].filter(
8851
+ (entry) => Math.abs(entry.actual - entry.expected) > tolerance
8681
8852
  );
8682
- if (off.length === 0) continue;
8683
8853
  const [first] = off;
8854
+ const size = title.fact.fontSizePt !== reference.fact.fontSizePt ? ` The fit pass sets it at ${title.fact.fontSizePt}pt, where they set at ${reference.fact.fontSizePt}pt.` : "";
8684
8855
  findings.push({
8685
- path: fact.path,
8686
- message: `This ${kind.startsWith("style:") ? kind.slice(6) : kind} title sits at ${off.map((axis) => `${axis.key} ${round(fact[axis.prop])}pt`).join(", ")}; the deck's others share ${off.map((axis) => `${axis.key} ${round(axis.value)}pt`).join(", ")}.`,
8687
- suggestion: "Place every title of one kind with the same block or the same coordinates, so the deck holds one line down the page.",
8856
+ path: title.path,
8857
+ relatedPaths: agreeing.map((other) => other.path),
8858
+ message: `This ${title.style} title lays out at ${off.map((entry) => `${entry.axis} ${round(entry.actual)}pt`).join(", ")}; the deck's other titles of that kind land at ${off.map((entry) => `${entry.axis} ${round(entry.expected)}pt`).join(", ")}.${size}`,
8859
+ suggestion: "Place every title of one kind with the same block or the same coordinates, anchor them alike, and keep each short enough to set at the size the others keep.",
8688
8860
  context: {
8689
- kind,
8690
- axes: off.map((axis) => axis.key),
8691
- slide: fact.slidePath
8861
+ style: title.style,
8862
+ setSizePt: title.setSizePt,
8863
+ align: title.align,
8864
+ axes: off.map((entry) => entry.axis),
8865
+ slide: title.fact.slidePath
8692
8866
  },
8693
8867
  evidence: {
8694
- actual: round(fact[first.prop]),
8695
- expected: round(first.value),
8868
+ summary: "estimated first baseline and aligned edge",
8869
+ actual: round(first.actual),
8870
+ expected: round(first.expected),
8696
8871
  unit: "pt",
8697
- values: { axis: first.key, source: "profile" }
8872
+ values: {
8873
+ axis: first.axis,
8874
+ lines: title.lines,
8875
+ fontSizePt: title.fact.fontSizePt,
8876
+ verticalAlign: title.fact.verticalAlign,
8877
+ source: configurationSource(configuration, "tolerancePt")
8878
+ }
8698
8879
  }
8699
8880
  });
8700
8881
  }
@@ -8747,7 +8928,9 @@ var pptxBulletRule = {
8747
8928
  actual: bullets.items,
8748
8929
  expected: maximumBullets,
8749
8930
  unit: "bullets",
8750
- values: { source: "profile" }
8931
+ values: {
8932
+ source: configurationSource(configuration, "maximumBullets")
8933
+ }
8751
8934
  }
8752
8935
  });
8753
8936
  if (maximumWords > 0 && bullets.longestWords > maximumWords)
@@ -8761,16 +8944,39 @@ var pptxBulletRule = {
8761
8944
  actual: bullets.longestWords,
8762
8945
  expected: maximumWords,
8763
8946
  unit: "words",
8764
- values: { source: "profile" }
8947
+ values: {
8948
+ source: configurationSource(
8949
+ configuration,
8950
+ "maximumWordsPerBullet"
8951
+ )
8952
+ }
8765
8953
  }
8766
8954
  });
8767
8955
  }
8768
8956
  return findings;
8769
8957
  }
8770
8958
  };
8959
+ function safeAreaFixes(fact, canvas, safe) {
8960
+ const position = fact.authoredPositionIn;
8961
+ if (position === void 0 || fact.widthPt > canvas.widthPt - 2 * safe || fact.heightPt > canvas.heightPt - 2 * safe)
8962
+ return [];
8963
+ const clamp = (valuePt, sizePt, extentPt) => {
8964
+ const low = Math.ceil(safe / 72 * 1e3) / 1e3;
8965
+ const high = Math.floor((extentPt - safe - sizePt) / 72 * 1e3) / 1e3;
8966
+ return Math.min(Math.max(valuePt / 72, low), high);
8967
+ };
8968
+ const fixes = [];
8969
+ const x = clamp(fact.xPt, fact.widthPt, canvas.widthPt);
8970
+ const y = clamp(fact.yPt, fact.heightPt, canvas.heightPt);
8971
+ if (Math.abs(x - position.x) > 5e-4)
8972
+ fixes.push({ op: "replace", path: `${fact.path}/props/x`, value: x });
8973
+ if (Math.abs(y - position.y) > 5e-4)
8974
+ fixes.push({ op: "replace", path: `${fact.path}/props/y`, value: y });
8975
+ return fixes;
8976
+ }
8771
8977
  var pptxSafeAreaRule = {
8772
8978
  id: "pptx/safe-area",
8773
- description: "Content outside the theme\u2019s safe area that is neither chrome nor a full bleed. Off until a profile or policy enables it.",
8979
+ description: "Content outside the theme\u2019s safe area that is neither chrome (a tracker, footer, source or logo) nor a full bleed. Off until a profile or policy enables it.",
8774
8980
  code: QUALITY_CODES.SAFE_AREA,
8775
8981
  category: "composition",
8776
8982
  defaultSeverity: "warning",
@@ -8791,7 +8997,7 @@ var pptxSafeAreaRule = {
8791
8997
  if (canvas === void 0 || safe === void 0 || safe <= 0) return [];
8792
8998
  const chromeNodes = new Set(
8793
8999
  facts.filter(
8794
- (fact) => fact.kind === "pptx/chrome-slot" && (fact.role === "tracker" || fact.role === "footer" || fact.role === "source")
9000
+ (fact) => fact.kind === "pptx/chrome-slot" && (fact.role === "tracker" || fact.role === "footer" || fact.role === "source" || fact.role === "logo")
8795
9001
  ).flatMap((fact) => fact.nodePath ? [fact.nodePath] : [])
8796
9002
  );
8797
9003
  const chromeStyles = /* @__PURE__ */ new Set(["footer", "tracker", "source"]);
@@ -8817,6 +9023,7 @@ var pptxSafeAreaRule = {
8817
9023
  right - (canvas.widthPt - safe),
8818
9024
  bottom - (canvas.heightPt - safe)
8819
9025
  );
9026
+ const fixes = safeAreaFixes(fact, canvas, safe);
8820
9027
  return [
8821
9028
  {
8822
9029
  path: fact.path,
@@ -8828,7 +9035,8 @@ var pptxSafeAreaRule = {
8828
9035
  expected: 0,
8829
9036
  unit: "pt",
8830
9037
  values: { source: "theme" }
8831
- }
9038
+ },
9039
+ ...fixes.length > 0 && { fixes }
8832
9040
  }
8833
9041
  ];
8834
9042
  });
@@ -8864,7 +9072,7 @@ var pptxSlideTitleRule = {
8864
9072
  actual: 0,
8865
9073
  expected: 1,
8866
9074
  unit: "titles",
8867
- values: { source: "profile" }
9075
+ values: { source: configurationSource(configuration) }
8868
9076
  }
8869
9077
  }));
8870
9078
  }
@@ -8913,7 +9121,20 @@ var pptxImageAspectRule = {
8913
9121
  {
8914
9122
  path: fact.path,
8915
9123
  drawn: fact.drawnRatio,
8916
- natural: fact.naturalRatio
9124
+ natural: fact.naturalRatio,
9125
+ ...fact.authoredSizeIn && {
9126
+ sides: {
9127
+ width: {
9128
+ path: `${fact.path}/props/w`,
9129
+ value: fact.authoredSizeIn.w
9130
+ },
9131
+ height: {
9132
+ path: `${fact.path}/props/h`,
9133
+ value: fact.authoredSizeIn.h
9134
+ },
9135
+ decimals: 3
9136
+ }
9137
+ }
8917
9138
  },
8918
9139
  "slide",
8919
9140
  tolerance
@@ -8939,6 +9160,7 @@ var PPTX_QUALITY_RULES = {
8939
9160
  pptxOffCanvasRule,
8940
9161
  pptxSlotBudgetRule,
8941
9162
  pptxRequiredChromeRule,
9163
+ pptxSlideFooterRule,
8942
9164
  pptxActionTitleRule,
8943
9165
  pptxTypeScaleRule,
8944
9166
  pptxSizeCountRule,
@@ -8973,11 +9195,12 @@ var PPTX_QUALITY_PROFILES = {
8973
9195
  "consulting-deck": {
8974
9196
  id: "consulting-deck",
8975
9197
  formats: ["pptx"],
8976
- description: "Consulting readout: every content slide leads with a two-line action title, every chart carries a takeaway and a source, content stays inside the theme\u2019s safe area, a box holds at most five bullets of at most twelve words, every figure carries alt text, every size is on the theme scale with at most nine in the deck and six on a slide, and titles of one kind hold one line.",
9198
+ description: "Consulting readout: every content slide leads with a two-line action title, every chart carries a takeaway and a source, every slide a block builds after the cover carries its page number, content stays inside the theme\u2019s safe area, a box holds at most five bullets of at most twelve words, every figure carries alt text, every size is on the theme scale with at most nine in the deck and six on a slide, and titles of one kind hold one line.",
8977
9199
  rules: {
8978
9200
  "pptx/required-chrome": {
8979
9201
  parameters: { required: ["takeaway", "source"] }
8980
9202
  },
9203
+ "pptx/slide-footer": { parameters: { required: ["pageNumber"] } },
8981
9204
  "pptx/action-title": { parameters: { maxLines: 2 } },
8982
9205
  "pptx/slide-density": { parameters: { maximumBodyWords: 90 } },
8983
9206
  "pptx/type-scale": { enabled: true },
@@ -9270,97 +9493,152 @@ function createBuilderImpl(state) {
9270
9493
  newState
9271
9494
  );
9272
9495
  }
9273
- async function generate(document, options) {
9274
- try {
9275
- let internalDocument = document;
9276
- const renderer = options?.renderer ?? state.renderer ?? internalDocument.renderer;
9277
- const validationDocument = renderer === void 0 ? internalDocument : { ...internalDocument, renderer };
9278
- const validationOptions = {
9279
- ...state.validation,
9280
- ...options?.validation
9281
- };
9282
- if (validationOptions.enabled !== false) {
9283
- const result = validatePresentation(
9284
- validationDocument,
9285
- state.components,
9286
- { allowUnknownFields: validationOptions.allowUnknownFields }
9496
+ async function expandPresentation(document, options) {
9497
+ let internalDocument = document;
9498
+ const renderer = options?.renderer ?? state.renderer ?? internalDocument.renderer;
9499
+ const validationDocument = renderer === void 0 ? internalDocument : { ...internalDocument, renderer };
9500
+ const validationOptions = {
9501
+ ...state.validation,
9502
+ ...options?.validation
9503
+ };
9504
+ if (validationOptions.enabled !== false) {
9505
+ const result = validatePresentation(
9506
+ validationDocument,
9507
+ state.components,
9508
+ { allowUnknownFields: validationOptions.allowUnknownFields }
9509
+ );
9510
+ if (!result.valid) {
9511
+ throw new ComponentValidationError3(result.errors, internalDocument);
9512
+ }
9513
+ } else if (!internalDocument || internalDocument.name !== "pptx") {
9514
+ throw new Error("Top-level component must be a pptx component");
9515
+ }
9516
+ const warnings = [];
9517
+ const context = resolveThemeContext(internalDocument, {
9518
+ customThemes: state.customThemes,
9519
+ fonts: state.fonts,
9520
+ warnings,
9521
+ defaultThemeName: typeof state.theme === "string" ? state.theme : void 0,
9522
+ resolveNamedTheme: (name, authored) => state.customThemes?.[name] ?? (authored && hasPptxTheme(name) ? getPptxTheme(name) : void 0) ?? (typeof state.theme === "object" && state.theme !== null ? state.theme : getPptxTheme(name))
9523
+ });
9524
+ const modedRoot = context.document;
9525
+ const resolvedTheme = context.theme;
9526
+ const validateEmitted = validationOptions.enabled === false ? void 0 : (emitted, componentLabel, parentName) => {
9527
+ let validationDocument2;
9528
+ if (parentName === "pptx") {
9529
+ validationDocument2 = {
9530
+ ...modedRoot,
9531
+ ...renderer !== void 0 ? { renderer } : {},
9532
+ children: emitted
9533
+ };
9534
+ } else if (parentName === "slide") {
9535
+ validationDocument2 = {
9536
+ ...modedRoot,
9537
+ ...renderer !== void 0 ? { renderer } : {},
9538
+ children: [{ name: "slide", props: {}, children: emitted }]
9539
+ };
9540
+ } else {
9541
+ return;
9542
+ }
9543
+ const result = validatePresentation(
9544
+ validationDocument2,
9545
+ state.components,
9546
+ { allowUnknownFields: validationOptions.allowUnknownFields }
9547
+ );
9548
+ if (!result.valid) {
9549
+ throw new ComponentValidationError3(
9550
+ result.errors.map((error) => ({
9551
+ ...error,
9552
+ message: `custom component '${componentLabel}' emitted invalid output \u2014 ${error.message}`
9553
+ })),
9554
+ emitted
9287
9555
  );
9288
- if (!result.valid) {
9289
- throw new ComponentValidationError3(result.errors, internalDocument);
9556
+ }
9557
+ };
9558
+ const expanded = await expandPptxBlocksWithPlugins(
9559
+ modedRoot,
9560
+ resolvedTheme,
9561
+ new Set(componentMap.keys()),
9562
+ pluginRenderer(warnings, resolvedTheme, validateEmitted)
9563
+ );
9564
+ const processedDocument = expanded.document;
9565
+ if (validationOptions.enabled !== false) {
9566
+ const result = validatePresentation(
9567
+ {
9568
+ ...processedDocument,
9569
+ ...renderer !== void 0 ? { renderer } : {}
9570
+ },
9571
+ [],
9572
+ {
9573
+ allowUnknownFields: validationOptions.allowUnknownFields
9290
9574
  }
9291
- } else if (!internalDocument || internalDocument.name !== "pptx") {
9292
- throw new Error("Top-level component must be a pptx component");
9575
+ );
9576
+ if (!result.valid) {
9577
+ throw new ComponentValidationError3(
9578
+ result.errors.map((error) => ({
9579
+ ...error,
9580
+ message: `expanded plugin output failed validation \u2014 ${error.message}`
9581
+ })),
9582
+ processedDocument
9583
+ );
9293
9584
  }
9294
- const warnings = [];
9295
- const context = resolveThemeContext(internalDocument, {
9585
+ }
9586
+ return { context, expanded, warnings, renderer };
9587
+ }
9588
+ async function prepareQuality(document, options) {
9589
+ const { context, expanded, warnings, renderer } = await expandPresentation(
9590
+ document,
9591
+ options
9592
+ );
9593
+ const prepared = preparePptxQualityDocument(
9594
+ document,
9595
+ {
9596
+ context,
9597
+ expanded,
9296
9598
  customThemes: state.customThemes,
9297
9599
  fonts: state.fonts,
9298
- warnings,
9299
- defaultThemeName: typeof state.theme === "string" ? state.theme : void 0,
9300
- resolveNamedTheme: (name, authored) => state.customThemes?.[name] ?? (authored && hasPptxTheme(name) ? getPptxTheme(name) : void 0) ?? (typeof state.theme === "object" && state.theme !== null ? state.theme : getPptxTheme(name))
9301
- });
9302
- const modedRoot = context.document;
9303
- const resolvedTheme = context.theme;
9304
- const validateEmitted = validationOptions.enabled === false ? void 0 : (emitted, componentLabel, parentName) => {
9305
- let validationDocument2;
9306
- if (parentName === "pptx") {
9307
- validationDocument2 = {
9308
- ...modedRoot,
9309
- ...renderer !== void 0 ? { renderer } : {},
9310
- children: emitted
9311
- };
9312
- } else if (parentName === "slide") {
9313
- validationDocument2 = {
9314
- ...modedRoot,
9315
- ...renderer !== void 0 ? { renderer } : {},
9316
- children: [{ name: "slide", props: {}, children: emitted }]
9317
- };
9318
- } else {
9319
- return;
9320
- }
9321
- const result = validatePresentation(
9322
- validationDocument2,
9323
- state.components,
9324
- { allowUnknownFields: validationOptions.allowUnknownFields }
9600
+ services: state.services,
9601
+ ...renderer !== void 0 && { renderer },
9602
+ warnings
9603
+ }
9604
+ );
9605
+ return { prepared, warnings };
9606
+ }
9607
+ async function generateFromPrepared(options) {
9608
+ const { prepared } = options;
9609
+ const { document: processedDocument, theme, processed } = prepared.model;
9610
+ const warnings = [];
9611
+ const renderer = options.renderer ?? state.renderer ?? prepared.renderer;
9612
+ const hasHighcharts = containsHighcharts(processed);
9613
+ const resolvedFonts = await resolveDocumentFonts(
9614
+ processedDocument,
9615
+ theme,
9616
+ warnings,
9617
+ state.fonts,
9618
+ hasHighcharts
9619
+ );
9620
+ const chartFonts = hasHighcharts ? toChartFontFaces2(resolvedFonts) : [];
9621
+ const buffer = await runWithBaseDir(
9622
+ options.baseDir ?? state.baseDir,
9623
+ () => renderProcessedViaIr(processed, warnings, {
9624
+ renderer,
9625
+ services: state.services,
9626
+ deterministic: options.deterministic ?? state.packaging.deterministic,
9627
+ generatedAt: options.generatedAt ?? state.packaging.generatedAt,
9628
+ ...chartFonts.length > 0 ? { chartFonts } : {}
9629
+ })
9630
+ );
9631
+ return { buffer, warnings };
9632
+ }
9633
+ async function generate(document, options) {
9634
+ try {
9635
+ if (options?.prepared)
9636
+ return await generateFromPrepared(
9637
+ options
9325
9638
  );
9326
- if (!result.valid) {
9327
- throw new ComponentValidationError3(
9328
- result.errors.map((error) => ({
9329
- ...error,
9330
- message: `custom component '${componentLabel}' emitted invalid output \u2014 ${error.message}`
9331
- })),
9332
- emitted
9333
- );
9334
- }
9335
- };
9336
- const expanded = await expandPptxBlocksWithPlugins(
9337
- modedRoot,
9338
- resolvedTheme,
9339
- new Set(componentMap.keys()),
9340
- pluginRenderer(warnings, resolvedTheme, validateEmitted)
9341
- );
9639
+ const { context, expanded, warnings, renderer } = await expandPresentation(document, options);
9640
+ const resolvedTheme = context.theme;
9342
9641
  const processedDocument = expanded.document;
9343
- if (validationOptions.enabled !== false) {
9344
- const result = validatePresentation(
9345
- {
9346
- ...processedDocument,
9347
- ...renderer !== void 0 ? { renderer } : {}
9348
- },
9349
- [],
9350
- {
9351
- allowUnknownFields: validationOptions.allowUnknownFields
9352
- }
9353
- );
9354
- if (!result.valid) {
9355
- throw new ComponentValidationError3(
9356
- result.errors.map((error) => ({
9357
- ...error,
9358
- message: `expanded plugin output failed validation \u2014 ${error.message}`
9359
- })),
9360
- processedDocument
9361
- );
9362
- }
9363
- }
9364
9642
  const processed = processPresentation(processedDocument, {
9365
9643
  theme: resolvedTheme,
9366
9644
  services: state.services,
@@ -9459,6 +9737,7 @@ function createBuilderImpl(state) {
9459
9737
  generate,
9460
9738
  generateBuffer: generate,
9461
9739
  generateFile,
9740
+ prepareQuality,
9462
9741
  getComponentNames,
9463
9742
  validate,
9464
9743
  generateSchema,