@openpresentation/opf 0.15.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +19 -1
  2. package/dist/{catalog-refs-BSfGELpt.d.ts → catalog-refs-C4K4y-vh.d.ts} +1 -1
  3. package/dist/{catalog-schemas-CinPh5WA.d.ts → catalog-schemas-g7RFQBdc.d.ts} +1 -1
  4. package/dist/catalog.d.ts +2 -2
  5. package/dist/catalog.js +3 -23224
  6. package/dist/{chart-data-BnNqgnC-.d.ts → chart-data-DTxdRRT1.d.ts} +1 -1
  7. package/dist/{chunk-3JG4YXWF.js → chunk-BIC3SWPK.js} +12 -5
  8. package/dist/{chunk-O744SXXA.js → chunk-CW7GEAV3.js} +350 -141
  9. package/dist/{chunk-374R35TJ.js → chunk-ESRYBYLY.js} +1 -1
  10. package/dist/chunk-IPW5BS3X.js +973 -0
  11. package/dist/{chunk-3RKB4RDJ.js → chunk-IS5H5JKH.js} +4 -2
  12. package/dist/{chunk-TWQJLI6E.js → chunk-M4UR3NPN.js} +2 -2
  13. package/dist/chunk-N4EVOFEA.js +58 -0
  14. package/dist/{chunk-QDI75P2M.js → chunk-NLNYNBTS.js} +217 -20
  15. package/dist/{chunk-U4J75TRG.js → chunk-O263APNI.js} +5 -2
  16. package/dist/{chunk-UBFJ2QHH.js → chunk-O2CX5NEH.js} +36 -14
  17. package/dist/{chunk-5FTUIDUH.js → chunk-O3CVNJTL.js} +1 -1
  18. package/dist/{chunk-VGAOJEY7.js → chunk-PPROJP2E.js} +1 -1
  19. package/dist/{chunk-2CJPC4HI.js → chunk-RA64UQKI.js} +203 -53
  20. package/dist/chunk-TCD2FB2K.js +319 -0
  21. package/dist/chunk-WCG3THTD.js +23712 -0
  22. package/dist/chunk-WWYWFVOI.js +793 -0
  23. package/dist/{chunk-ZPU46TFA.js → chunk-YT7FMQER.js} +1 -1
  24. package/dist/{composition-D2tghM82.d.ts → composition-BbZ_9C-c.d.ts} +238 -139
  25. package/dist/composition.d.ts +3 -3
  26. package/dist/composition.js +1 -1
  27. package/dist/conversion-SBpTOwdg.d.ts +444 -0
  28. package/dist/convert.d.ts +4 -4
  29. package/dist/convert.js +7 -7
  30. package/dist/data.d.ts +28 -11
  31. package/dist/data.js +2 -2
  32. package/dist/deck-report-Bx3naFlF.d.ts +40 -0
  33. package/dist/deck.d.ts +47 -0
  34. package/dist/deck.js +12 -0
  35. package/dist/diff.d.ts +1 -1
  36. package/dist/diff.js +5 -5
  37. package/dist/docs.js +33 -27
  38. package/dist/examples.d.ts +1 -1
  39. package/dist/examples.js +3229 -985
  40. package/dist/{finding-CT_f_vbD.d.ts → finding-CDQ_wH8j.d.ts} +1 -1
  41. package/dist/format.js +2 -2
  42. package/dist/index.d.ts +43 -12
  43. package/dist/index.js +15 -805
  44. package/dist/markdown.d.ts +7 -8
  45. package/dist/markdown.js +7 -7
  46. package/dist/node-engine.d.ts +94 -0
  47. package/dist/node-engine.js +21 -0
  48. package/dist/node.d.ts +60 -0
  49. package/dist/node.js +54 -0
  50. package/dist/{pagination-wedbe7eq.d.ts → pagination-BhX4M48Y.d.ts} +21 -7
  51. package/dist/pagination.d.ts +4 -4
  52. package/dist/pagination.js +4 -4
  53. package/dist/patch.d.ts +1 -1
  54. package/dist/patch.js +4 -4
  55. package/dist/{presentation-CDKQVkyI.d.ts → presentation-fb8CZmRq.d.ts} +28 -83
  56. package/dist/repo-readme.js +1 -1
  57. package/dist/schemas.d.ts +14 -0
  58. package/dist/schemas.js +1 -1
  59. package/dist/spec/catalogs/layouts/business-model-canvas.json +66 -1
  60. package/dist/spec/catalogs/layouts/capability-map.json +53 -3
  61. package/dist/spec/catalogs/layouts/chart-2x.json +32 -8
  62. package/dist/spec/catalogs/layouts/chart-3x-bottom-vertical-title-left.json +16 -10
  63. package/dist/spec/catalogs/layouts/chart-3x-left-title-center.json +16 -10
  64. package/dist/spec/catalogs/layouts/chart-3x-left.json +16 -10
  65. package/dist/spec/catalogs/layouts/chart-3x-right-title-center.json +16 -10
  66. package/dist/spec/catalogs/layouts/chart-3x-right.json +16 -10
  67. package/dist/spec/catalogs/layouts/chart-3x-top-vertical-title-left.json +16 -10
  68. package/dist/spec/catalogs/layouts/chart-3x.json +48 -12
  69. package/dist/spec/catalogs/layouts/dashboard.json +37 -5
  70. package/dist/spec/catalogs/layouts/image-2x.json +32 -8
  71. package/dist/spec/catalogs/layouts/image-3x.json +48 -12
  72. package/dist/spec/catalogs/layouts/index.json +1 -1
  73. package/dist/spec/catalogs/layouts/kpi-scorecard.json +48 -3
  74. package/dist/spec/catalogs/layouts/logic-tree.json +69 -0
  75. package/dist/spec/catalogs/layouts/market-size.json +26 -2
  76. package/dist/spec/catalogs/layouts/org-chart.json +53 -6
  77. package/dist/spec/catalogs/manifest.json +3 -3
  78. package/dist/spec/schemas/layout.schema.json +172 -5
  79. package/dist/spec/schemas/opf.schema.json +27 -43
  80. package/dist/types.d.ts +10 -32
  81. package/dist/{validation-types-BOcY9GTs.d.ts → validation-types-iZbXfES6.d.ts} +19 -8
  82. package/dist/validator.d.ts +8 -8
  83. package/dist/validator.js +3 -3
  84. package/dist/yaml.d.ts +6 -6
  85. package/dist/yaml.js +8 -321
  86. package/package.json +38 -1
@@ -319,7 +319,6 @@ function visitContentPayloads(slide, slidePath, visit) {
319
319
 
320
320
  // src/furniture-fields.ts
321
321
  var DEFAULT_FURNITURE_DATE_FORMAT = "M/d/yyyy";
322
- var DEFAULT_SLIDE_NUMBER_FORMAT = "{current}";
323
322
  var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
324
323
  var weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
325
324
  var tokenWidths = { y: [1, 2, 4], M: [1, 2, 3, 4], d: [1, 2], E: [1, 2, 3, 4] };
@@ -407,21 +406,6 @@ function formatFurnitureDate(iso, pattern) {
407
406
  }
408
407
  return { text: text2 };
409
408
  }
410
- function formatSlideNumber(format, current, total) {
411
- if (!format.includes("{current}")) return { error: "slideNumberFormat must contain {current}." };
412
- if (format.includes("{total}") && total === void 0) return { error: "slideNumberFormat uses {total}, which needs the rendered slide count." };
413
- let text2 = "";
414
- const fields2 = [];
415
- for (const piece of format.split(/(\{current\}|\{total\})/)) {
416
- if (piece === "{current}") {
417
- const value = String(current);
418
- fields2.push({ type: "slideNumber", start: text2.length, end: text2.length + value.length });
419
- text2 += value;
420
- } else if (piece === "{total}") text2 += String(total);
421
- else text2 += piece;
422
- }
423
- return { text: text2, fields: fields2 };
424
- }
425
409
 
426
410
  // src/deck-metadata.ts
427
411
  function recordsOf(value) {
@@ -441,6 +425,111 @@ function primaryOrganization(presentation) {
441
425
  return organizations.find((item) => item.role === "primary") ?? organizations[0];
442
426
  }
443
427
 
428
+ // src/slide-variables.ts
429
+ var SLIDE_SCOPED_BUILTINS = ["slide.number", "slide.section", "deck.slideCount"];
430
+ function isSlideScopedBuiltin(name) {
431
+ return SLIDE_SCOPED_BUILTINS.includes(name);
432
+ }
433
+ function isSlideScopedName(name) {
434
+ return name === "deck.slideCount" || name.startsWith("slide.");
435
+ }
436
+ var SLIDE_TOKEN_BODY = String.raw`\s*(?:slide(?:\.[A-Za-z0-9_-]+){1,2}|deck\.slideCount)\s*(?:\|[^{}]*)?\}\}`;
437
+ var slideTokenAhead = new RegExp(`^${SLIDE_TOKEN_BODY}`);
438
+ var slideTokenPattern = new RegExp(String.raw`\\\{\{(?=${SLIDE_TOKEN_BODY})|\{\{\s*(slide\.number|slide\.section|deck\.slideCount)\s*(?:\|[^{}]*)?\}\}`, "g");
439
+ var slideTokenSpanPattern = new RegExp(String.raw`\\?\{\{${SLIDE_TOKEN_BODY}`, "g");
440
+ var quickCheck = /\{\{\s*(?:slide\.|deck\.slideCount)/;
441
+ function slideTokenFollows(text2) {
442
+ return slideTokenAhead.test(text2);
443
+ }
444
+ function checkValues(values) {
445
+ if (!isRecord(values)) throw new TypeError("Slide variable values must be an object with slideNumber and slideCount.");
446
+ if (!Number.isSafeInteger(values.slideNumber) || values.slideNumber < 1) throw new RangeError("slideNumber must be a positive safe integer.");
447
+ if (values.slideCount !== void 0 && (!Number.isSafeInteger(values.slideCount) || values.slideCount < 1)) throw new RangeError("slideCount must be a positive safe integer.");
448
+ }
449
+ function substituteSlideTokens(text2, values) {
450
+ if (!text2.includes("{{")) return { text: text2, fields: [], changed: false };
451
+ const fields2 = [];
452
+ let out = "";
453
+ let last = 0;
454
+ let changed = false;
455
+ slideTokenPattern.lastIndex = 0;
456
+ for (let match = slideTokenPattern.exec(text2); match; match = slideTokenPattern.exec(text2)) {
457
+ const name = match[1];
458
+ let value;
459
+ if (name === void 0) value = "{{";
460
+ else if (name === "slide.number") value = String(values.slideNumber);
461
+ else if (name === "deck.slideCount") {
462
+ if (values.slideCount === void 0) continue;
463
+ value = String(values.slideCount);
464
+ } else value = typeof values.section === "string" ? values.section : "";
465
+ out += text2.slice(last, match.index);
466
+ if (name === "slide.number") fields2.push({ type: "slideNumber", start: out.length, end: out.length + value.length });
467
+ out += value;
468
+ last = match.index + match[0].length;
469
+ changed = true;
470
+ }
471
+ if (!changed) return { text: text2, fields: fields2, changed };
472
+ return { text: out + text2.slice(last), fields: fields2, changed };
473
+ }
474
+ function slideTokenSpans(text2) {
475
+ if (!text2.includes("{{")) return [];
476
+ return Array.from(text2.matchAll(slideTokenSpanPattern), (match) => [match.index, match.index + match[0].length]);
477
+ }
478
+ var skippedKeys = /* @__PURE__ */ new Set(["extensions"]);
479
+ function substitute(value, values, root) {
480
+ if (typeof value === "string") {
481
+ const result = substituteSlideTokens(value, values);
482
+ return result.changed ? result.text : value;
483
+ }
484
+ if (Array.isArray(value)) {
485
+ let changed2 = false;
486
+ const out2 = value.map((entry) => {
487
+ const next = substitute(entry, values, false);
488
+ if (next !== entry) changed2 = true;
489
+ return next;
490
+ });
491
+ return changed2 ? out2 : value;
492
+ }
493
+ if (!isRecord(value)) return value;
494
+ let changed = false;
495
+ const out = {};
496
+ for (const [key, entry] of Object.entries(value)) {
497
+ let next = entry;
498
+ if (skippedKeys.has(key)) next = entry;
499
+ else if (root && key === "design" && isRecord(entry)) {
500
+ const design = {};
501
+ let designChanged = false;
502
+ for (const [designKey, designEntry] of Object.entries(entry)) {
503
+ const designNext = designKey === "header" || designKey === "footer" ? designEntry : substitute(designEntry, values, false);
504
+ if (designNext !== designEntry) designChanged = true;
505
+ design[designKey] = designNext;
506
+ }
507
+ next = designChanged ? design : entry;
508
+ } else next = substitute(entry, values, false);
509
+ if (next !== entry) changed = true;
510
+ out[key] = next;
511
+ }
512
+ return changed ? out : value;
513
+ }
514
+ function resolveSlideVariables(slide, values) {
515
+ if (!isRecord(slide)) return slide;
516
+ const full = { slideNumber: values?.slideNumber, ...values?.slideCount !== void 0 ? { slideCount: values.slideCount } : {}, section: slide.section };
517
+ checkValues(full);
518
+ if (!quickCheck.test(JSON.stringify(slide))) return slide;
519
+ return substitute(slide, full, true);
520
+ }
521
+ function usesSlideBuiltin(value, name) {
522
+ const pattern = new RegExp(String.raw`(?:^|[^\\])\{\{\s*${name.replace(".", String.raw`\.`)}\s*(?:\|[^{}]*)?\}\}`);
523
+ const visit = (entry) => {
524
+ if (typeof entry === "string") return entry.includes("{{") && pattern.test(entry);
525
+ if (Array.isArray(entry)) return entry.some(visit);
526
+ if (!isRecord(entry)) return false;
527
+ for (const [key, child] of Object.entries(entry)) if (!skippedKeys.has(key) && visit(child)) return true;
528
+ return false;
529
+ };
530
+ return visit(value);
531
+ }
532
+
444
533
  // src/variables.ts
445
534
  var VARIABLE_KINDS = ["color", "text", "number", "date", "image", "url", "list"];
446
535
  var DEFAULT_VARIABLE_DATE_FORMAT = "MMMM d, yyyy";
@@ -454,10 +543,10 @@ var OPFVariableError = class extends Error {
454
543
  }
455
544
  };
456
545
  var idPattern = /^[a-z][a-z0-9-]*$/;
457
- var nameSource = String.raw`[a-z][a-z0-9-]*|(?:deck|speaker|organization)(?:\.[A-Za-z0-9_-]+){1,2}`;
546
+ var nameSource = String.raw`[a-z][a-z0-9-]*|(?:deck|speaker|organization|slide)(?:\.[A-Za-z0-9_-]+){1,2}`;
458
547
  var referencePattern = new RegExp(String.raw`^var:(${nameSource})$`);
459
- var builtinNamePattern = /^(?:speakers|(?:deck|speaker|organization)(?:\.[A-Za-z0-9_-]+){1,2})$/;
460
- var builtinUsePattern = /\{\{\s*(?:speakers\b|(?:deck|speaker|organization)\.)|^var:(?:speakers$|(?:deck|speaker|organization)\.)/;
548
+ var builtinNamePattern = /^(?:speakers|(?:deck|speaker|organization|slide)(?:\.[A-Za-z0-9_-]+){1,2})$/;
549
+ var builtinUsePattern = /\{\{\s*(?:speakers\b|(?:deck|speaker|organization|slide)\.)|^var:(?:speakers$|(?:deck|speaker|organization|slide)\.)/;
461
550
  var hexPattern = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
462
551
  var urlPattern = /^(?:https?:\/\/|mailto:|tel:)\S+$/;
463
552
  var datePrefixPattern = /^(\d{4}-\d{2}-\d{2})(?:[T ].*)?$/;
@@ -560,7 +649,7 @@ function usesBuiltins(value, root = true) {
560
649
  if (Array.isArray(value)) return value.some((entry) => usesBuiltins(entry, false));
561
650
  if (!isRecord(value)) return false;
562
651
  for (const [key, entry] of Object.entries(value)) {
563
- if (skippedKeys.has(key) || root && skippedRootKeys.has(key)) continue;
652
+ if (skippedKeys2.has(key) || root && skippedRootKeys.has(key)) continue;
564
653
  if (usesBuiltins(entry, false)) return true;
565
654
  }
566
655
  return false;
@@ -672,13 +761,17 @@ function builtinSource(name, doc) {
672
761
  const names = speakersOf(doc).map((speaker) => present(speaker.name)).filter((entry) => typeof entry === "string");
673
762
  return { ok: true, kind: "list", raw: names.length ? names : void 0 };
674
763
  }
764
+ const list = (names) => names.map((entry) => `'${entry}'`).join(", ");
765
+ if (isSlideScopedName(name)) {
766
+ if (isSlideScopedBuiltin(name)) return { ok: false, message: `'var:${name}' is not supported: '${name}' is a slide-scoped built-in, written only as the inline token '{{${name}}}'.` };
767
+ return { ok: false, message: `'${name}' is not a built-in variable; the slide-scoped built-ins are ${list(SLIDE_SCOPED_BUILTINS)}.` };
768
+ }
675
769
  const parts = name.split(".");
676
770
  const root = parts[0];
677
771
  const fields2 = builtinFields(root);
678
- const list = (names) => names.map((entry) => `'${entry}'`).join(", ");
679
772
  if (root === "deck") {
680
773
  const field2 = parts[1];
681
- if (parts.length !== 2 || !Object.hasOwn(fields2, field2)) return { ok: false, message: `'${name}' is not a built-in variable; the deck built-ins are ${list(Object.keys(fields2).map((key) => `deck.${key}`))}.` };
774
+ if (parts.length !== 2 || !Object.hasOwn(fields2, field2)) return { ok: false, message: `'${name}' is not a built-in variable; the deck built-ins are ${list(Object.keys(fields2).map((key) => `deck.${key}`))} and the slide-scoped 'deck.slideCount'.` };
682
775
  const value = doc[field2];
683
776
  return { ok: true, kind: "text", raw: field2 === "author" && Array.isArray(value) ? present(value.filter((entry) => typeof entry === "string").join(", ")) : present(value) };
684
777
  }
@@ -742,15 +835,16 @@ function lookupVariable(id, path, context) {
742
835
  }
743
836
  function interpolate(text2, path, context) {
744
837
  let changed = false;
745
- const value = text2.replace(tokenPattern, (match, id, format) => {
838
+ const value = text2.replace(tokenPattern, (match, id, format, offset) => {
746
839
  if (id === void 0) {
747
- if (context.unescape) {
840
+ if (context.unescape && !slideTokenFollows(text2.slice(offset + match.length))) {
748
841
  changed = true;
749
842
  return "{{";
750
843
  }
751
844
  return match;
752
845
  }
753
846
  context.uses.push({ id, path, form: "token" });
847
+ if (isSlideScopedBuiltin(id)) return match;
754
848
  const effective = lookupVariable(id, path, context);
755
849
  if (!effective) {
756
850
  if (!isBuiltinName(id)) diag(context, { code: "variable-unknown", severity: "warning", path, id, message: `'{{${id}}}' names no declared variable; declare '${id}' in the top-level variables map, or write '\\{{' for literal braces.` }, `unk:${id}:${path}`);
@@ -803,7 +897,7 @@ function reduceString(text2, path, parentIsArray, context) {
803
897
  const result = interpolate(text2, path, context);
804
898
  return result.changed ? { kind: "replace", value: result.value } : { kind: "keep" };
805
899
  }
806
- var skippedKeys = /* @__PURE__ */ new Set(["extensions"]);
900
+ var skippedKeys2 = /* @__PURE__ */ new Set(["extensions"]);
807
901
  var skippedRootKeys = /* @__PURE__ */ new Set(["variables", "catalogs", "$schema", "template"]);
808
902
  function walk(value, path, parentIsArray, context) {
809
903
  if (typeof value === "string") {
@@ -836,7 +930,7 @@ function walk(value, path, parentIsArray, context) {
836
930
  let changed = false;
837
931
  const entries = [];
838
932
  for (const [key, entry] of Object.entries(value)) {
839
- if (skippedKeys.has(key) || path === "" && skippedRootKeys.has(key)) {
933
+ if (skippedKeys2.has(key) || path === "" && skippedRootKeys.has(key)) {
840
934
  entries.push([key, entry]);
841
935
  continue;
842
936
  }
@@ -940,6 +1034,21 @@ function rebuildVariables(presentation, built, keepAll) {
940
1034
  }
941
1035
  return out.length ? Object.fromEntries(out) : void 0;
942
1036
  }
1037
+ function slideSectionDiagnostics(presentation, uses) {
1038
+ const paths = uses.filter((use) => use.id === "slide.section").map((use) => use.path);
1039
+ if (!paths.length || !Array.isArray(presentation.slides)) return [];
1040
+ const out = [];
1041
+ presentation.slides.forEach((slide, index) => {
1042
+ if (!isRecord(slide) || typeof slide.section === "string" && slide.section !== "") return;
1043
+ const own2 = `/slides/${index}/`;
1044
+ const design = isRecord(slide.design) ? slide.design : {};
1045
+ const inherited = ["header", "footer"].filter((kind2) => design[kind2] === void 0).map((kind2) => `/design/${kind2}/`);
1046
+ const where = paths.find((path) => path.startsWith(own2) || inherited.some((prefix) => path.startsWith(prefix)));
1047
+ if (where === void 0) return;
1048
+ out.push({ code: "variable-builtin-missing", severity: "warning", path: `/slides/${index}`, id: "slide.section", message: `Slide ${index + 1} has no section, so '{{slide.section}}' (used at ${where}) resolves to nothing on it; give the slide a section or remove the token.` });
1049
+ });
1050
+ return out;
1051
+ }
943
1052
  function finish(presentation, built, options) {
944
1053
  const { context } = built;
945
1054
  const complete = built.unfilled.length === 0;
@@ -958,6 +1067,7 @@ function finish(presentation, built, options) {
958
1067
  }
959
1068
  }
960
1069
  }
1070
+ if (context.shape) context.diagnostics.push(...slideSectionDiagnostics(presentation, context.uses));
961
1071
  const diagnostics = context.diagnostics;
962
1072
  if (options.strict && diagnostics.some((entry) => entry.severity === "error")) throw new OPFVariableError(diagnostics);
963
1073
  return { presentation: result, diagnostics, unfilled: built.unfilled, examplesUsed: built.examplesUsed, complete };
@@ -991,6 +1101,7 @@ function listVariables(presentation, values = {}) {
991
1101
  };
992
1102
  });
993
1103
  }
1104
+ var SLIDE_SCOPED_LABELS = { "slide.number": "Slide number", "slide.section": "Section", "deck.slideCount": "Slide count" };
994
1105
  var FIELD_LABELS = { name: "name", legalName: "legal name", tagline: "tagline", domain: "domain", email: "email", phone: "phone", logo: "logo", title: "title", bio: "bio", photo: "photo", description: "description", author: "author" };
995
1106
  function listBuiltinVariables(presentation) {
996
1107
  if (!isRecord(presentation)) return [];
@@ -1013,7 +1124,12 @@ function listBuiltinVariables(presentation) {
1013
1124
  const source = builtinSource(name, presentation);
1014
1125
  if (!source.ok) continue;
1015
1126
  const coerced = source.raw === void 0 ? void 0 : coerceVariableValue(source.kind, source.raw);
1016
- out.push({ name, kind: source.kind, label, ...coerced?.ok ? { value: coerced.value } : {}, available: coerced?.ok === true, uses: built.context.uses.filter((use) => use.id === name) });
1127
+ out.push({ name, kind: source.kind, label, scope: "deck", ...coerced?.ok ? { value: coerced.value } : {}, available: coerced?.ok === true, uses: built.context.uses.filter((use) => use.id === name) });
1128
+ }
1129
+ const slides = Array.isArray(presentation.slides) ? presentation.slides : [];
1130
+ for (const name of SLIDE_SCOPED_BUILTINS) {
1131
+ const available = name !== "slide.section" || slides.some((slide) => isRecord(slide) && typeof slide.section === "string" && slide.section !== "");
1132
+ out.push({ name, kind: "text", label: SLIDE_SCOPED_LABELS[name], scope: "slide", available, uses: built.context.uses.filter((use) => use.id === name) });
1017
1133
  }
1018
1134
  return out;
1019
1135
  }
@@ -1771,6 +1887,75 @@ function readingRows(items) {
1771
1887
  return rows3.map((row) => row.items.sort((a, b) => index.get(a) - index.get(b)));
1772
1888
  }
1773
1889
 
1890
+ // src/layout-content.ts
1891
+ var LAYOUT_BODY_KINDS = ["text", "list", "image", "video", "chart", "table", "code", "metric", "quote", "timeline"];
1892
+ var MAX_PLACEHOLDER_GROUP_DEPTH = 3;
1893
+ var bodyKinds = new Set(LAYOUT_BODY_KINDS);
1894
+ var record4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
1895
+ function isPlaceholderGroup(placeholder) {
1896
+ return record4(placeholder).type === "group";
1897
+ }
1898
+ function layoutSlots(layout, base = "layout.placeholders") {
1899
+ const walk2 = (entries, path, depth) => (Array.isArray(entries) ? entries : []).map((entry, index) => {
1900
+ const placeholder = record4(entry), slotPath = `${path}.${index}`;
1901
+ const type = typeof placeholder.type === "string" ? placeholder.type : "";
1902
+ if (type !== "group") {
1903
+ if (depth > 0 && !bodyKinds.has(type)) throw new RangeError(`${slotPath}: a placeholder group holds body regions only, not '${type}'.`);
1904
+ return { type, path: slotPath, depth, placeholder };
1905
+ }
1906
+ if (depth >= MAX_PLACEHOLDER_GROUP_DEPTH) throw new RangeError(`${slotPath}: placeholder groups nest at most ${MAX_PLACEHOLDER_GROUP_DEPTH} levels inside a layout record.`);
1907
+ if (!Array.isArray(placeholder.placeholders) || !placeholder.placeholders.length) throw new RangeError(`${slotPath}: a placeholder group needs at least one placeholder.`);
1908
+ return { type, path: slotPath, depth, placeholder, children: walk2(placeholder.placeholders, `${slotPath}.placeholders`, depth + 1) };
1909
+ });
1910
+ return walk2(layout?.placeholders, base, 0);
1911
+ }
1912
+ function layoutLeaves(layout) {
1913
+ const leaves = [];
1914
+ const visit = (slots) => {
1915
+ for (const slot of slots) slot.children ? visit(slot.children) : leaves.push({ type: slot.type, path: slot.path, depth: slot.depth, placeholder: slot.placeholder });
1916
+ };
1917
+ visit(layoutSlots(layout));
1918
+ return leaves;
1919
+ }
1920
+ function layoutStructure(layout) {
1921
+ const describe = (slots) => slots.map((slot) => {
1922
+ if (!slot.children) return slot.type;
1923
+ const mode = record4(slot.placeholder.composition).mode;
1924
+ return `${typeof mode === "string" && mode !== "auto" ? mode : "group"} (${describe(slot.children)})`;
1925
+ }).join(", ");
1926
+ return describe(layoutSlots(layout));
1927
+ }
1928
+ function hasPlaceholderGroups(layout) {
1929
+ return Array.isArray(layout?.placeholders) && layout.placeholders.some(isPlaceholderGroup);
1930
+ }
1931
+ function layoutContent(layout) {
1932
+ const heading = { title: false, subtitle: false, tag: false };
1933
+ const counts = /* @__PURE__ */ new Map();
1934
+ let count = 0;
1935
+ const visit = (entries, depth) => {
1936
+ for (const entry of Array.isArray(entries) ? entries : []) {
1937
+ const type = record4(entry).type;
1938
+ if (type === "group") {
1939
+ if (depth < MAX_PLACEHOLDER_GROUP_DEPTH) visit(record4(entry).placeholders, depth + 1);
1940
+ continue;
1941
+ }
1942
+ if (type === "title" || type === "subtitle" || type === "tag") {
1943
+ if (depth === 0) heading[type] = true;
1944
+ } else if (typeof type === "string" && bodyKinds.has(type)) {
1945
+ counts.set(type, (counts.get(type) ?? 0) + 1);
1946
+ count += 1;
1947
+ }
1948
+ }
1949
+ };
1950
+ visit(layout?.placeholders, 0);
1951
+ let kind2 = "title", best = 0;
1952
+ for (const [candidate, total] of counts) if (total > best) {
1953
+ kind2 = candidate;
1954
+ best = total;
1955
+ }
1956
+ return { kind: kind2, count, heading };
1957
+ }
1958
+
1774
1959
  // src/direction.ts
1775
1960
  var strongRtl = /[\u200F\u061C]|(?=\p{L})[\p{Script=Arabic}\p{Script=Hebrew}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}\p{Script=Adlam}\p{Script=Hanifi_Rohingya}\p{Script=Mandaic}\p{Script=Samaritan}\p{Script=Mende_Kikakui}\p{Script=Imperial_Aramaic}\p{Script=Phoenician}\p{Script=Kharoshthi}\p{Script=Old_South_Arabian}\p{Script=Old_North_Arabian}\p{Script=Avestan}\p{Script=Inscriptional_Parthian}\p{Script=Inscriptional_Pahlavi}\p{Script=Psalter_Pahlavi}\p{Script=Old_Turkic}\p{Script=Old_Hungarian}\p{Script=Nabataean}\p{Script=Palmyrene}\p{Script=Hatran}\p{Script=Manichaean}\p{Script=Sogdian}\p{Script=Old_Sogdian}\p{Script=Elymaic}\p{Script=Chorasmian}\p{Script=Yezidi}\p{Script=Cypriot}\p{Script=Lydian}\p{Script=Meroitic_Cursive}\p{Script=Meroitic_Hieroglyphs}\u{10F70}-\u{10FAF}\u{10D40}-\u{10D8F}]/u;
1776
1961
  var strongLtr = /[\u200E\p{L}\p{Nl}]/u;
@@ -3283,8 +3468,8 @@ function codeSyntaxPalette(theme = {}, options = {}) {
3283
3468
  return palette;
3284
3469
  }
3285
3470
  function codeSyntaxPaletteForScheme(scheme, options = {}) {
3286
- const record6 = scheme && typeof scheme === "object" ? scheme : {};
3287
- const pick = (role, slot, fallback) => normalize(record6[role]) ?? normalize(record6[slot]) ?? fallback;
3471
+ const record7 = scheme && typeof scheme === "object" ? scheme : {};
3472
+ const pick = (role, slot, fallback) => normalize(record7[role]) ?? normalize(record7[slot]) ?? fallback;
3288
3473
  return codeSyntaxPalette({ primary: pick("primary", "accent1", "#2563EB"), secondary: pick("secondary", "accent2", "#0F766E"), accent: pick("accent", "accent3", "#F59E0B") }, options);
3289
3474
  }
3290
3475
 
@@ -3436,8 +3621,8 @@ var blend = (from, to, amount) => {
3436
3621
  return toHex(a.map((value, index) => value + (b[index] - value) * amount));
3437
3622
  };
3438
3623
  function codeHighlightColors(scheme, palette = codeSyntaxPaletteForScheme(scheme)) {
3439
- const record6 = scheme && typeof scheme === "object" ? scheme : {};
3440
- const primary = normalize(record6.primary) ?? normalize(record6.accent1) ?? "#2563EB";
3624
+ const record7 = scheme && typeof scheme === "object" ? scheme : {};
3625
+ const primary = normalize(record7.primary) ?? normalize(record7.accent1) ?? "#2563EB";
3441
3626
  const [hue, saturation] = toHsl(primary);
3442
3627
  const tint = fromHsl(hue, Math.min(saturation, 0.7), 0.45);
3443
3628
  const band = blend(CODE_PANEL_BACKGROUND, tint, CODE_HIGHLIGHT_BAND_TINT);
@@ -3634,7 +3819,7 @@ var CAPTION_FONT_RATIO = 0.6;
3634
3819
  var CAPTION_MAX_RATIO = 0.35;
3635
3820
  var FOOTNOTE_MAX_RATIO = 0.35;
3636
3821
  var CAPTIONABLE_FIELDS = Object.freeze(["image", "chart", "table", "video"]);
3637
- var record4 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3822
+ var record5 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3638
3823
  var isRecord3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
3639
3824
  var round = (value) => Math.round(value * 1e6) / 1e6 || value;
3640
3825
  var regionKey = /^(top|middle|bottom|left|center|right)([+:]|$)/;
@@ -3642,7 +3827,7 @@ var TEXT_FIELDS = ["text", "items", "bullets"];
3642
3827
  var HEADING_FIELDS = ["tag", "title", "subtitle"];
3643
3828
  function annotationText(value) {
3644
3829
  if (typeof value === "string") return value;
3645
- if (Array.isArray(value)) return value.map((run) => typeof run === "string" ? run : record4(run).text ?? "").join("");
3830
+ if (Array.isArray(value)) return value.map((run) => typeof run === "string" ? run : record5(run).text ?? "").join("");
3646
3831
  if (isRecord3(value) && value.text !== void 0) return annotationText(value.text);
3647
3832
  return value === void 0 || value === null ? "" : String(value);
3648
3833
  }
@@ -3674,7 +3859,7 @@ function walkCitationRuns(slide, basePath, visit) {
3674
3859
  });
3675
3860
  };
3676
3861
  const host = (node, path, depth) => {
3677
- const payload = record4(node);
3862
+ const payload = record5(node);
3678
3863
  if (Array.isArray(payload.blocks)) {
3679
3864
  if (depth > 32) return;
3680
3865
  payload.blocks.forEach((block, index) => {
@@ -3689,7 +3874,7 @@ function walkCitationRuns(slide, basePath, visit) {
3689
3874
  }
3690
3875
  if (isRecord3(payload.quote)) runs(payload.quote.text, `${path}.quote.text`);
3691
3876
  };
3692
- const value = record4(slide);
3877
+ const value = record5(slide);
3693
3878
  for (const field of HEADING_FIELDS) runs(value[field], `${basePath}.${field}`);
3694
3879
  const regions = Object.keys(value).filter((key) => regionKey.test(key)).sort();
3695
3880
  if (regions.length) {
@@ -3706,7 +3891,7 @@ function walkCitationRuns(slide, basePath, visit) {
3706
3891
  }
3707
3892
  var citationMarkerText = (numbers) => numbers.join(",");
3708
3893
  function referenceList(presentation) {
3709
- const list = record4(presentation).references;
3894
+ const list = record5(presentation).references;
3710
3895
  return Array.isArray(list) ? list.filter((item) => isRecord3(item) && typeof item.id === "string") : [];
3711
3896
  }
3712
3897
  function numberSlide(slide, slideIndex, references, numbering) {
@@ -3739,7 +3924,7 @@ function numberSlide(slide, slideIndex, references, numbering) {
3739
3924
  }
3740
3925
  function collectCitations(presentation) {
3741
3926
  const references = referenceList(presentation), numbering = { notes: [], byId: /* @__PURE__ */ new Map() }, slides = /* @__PURE__ */ new Map();
3742
- const list = record4(presentation).slides;
3927
+ const list = record5(presentation).slides;
3743
3928
  if (Array.isArray(list)) list.forEach((slide, index) => {
3744
3929
  const result = numberSlide(slide, index, references, numbering);
3745
3930
  if (result) slides.set(index, result);
@@ -3749,7 +3934,7 @@ function collectCitations(presentation) {
3749
3934
  }
3750
3935
  function slideCitations(slide, slideIndex, presentation) {
3751
3936
  const references = referenceList(presentation), numbering = { notes: [], byId: /* @__PURE__ */ new Map() };
3752
- const list = record4(presentation).slides;
3937
+ const list = record5(presentation).slides;
3753
3938
  if (Array.isArray(list)) for (let index = 0; index < Math.min(slideIndex, list.length); index += 1) numberSlide(list[index], index, references, numbering);
3754
3939
  return numberSlide(slide, slideIndex, references, numbering);
3755
3940
  }
@@ -3812,35 +3997,12 @@ function referencesSlide(presentation, options = {}) {
3812
3997
  return { title, items };
3813
3998
  }
3814
3999
 
3815
- // src/layout-content.ts
3816
- var LAYOUT_BODY_KINDS = ["text", "list", "image", "video", "chart", "table", "code", "metric", "quote", "timeline"];
3817
- var bodyKinds = new Set(LAYOUT_BODY_KINDS);
3818
- function layoutContent(record6) {
3819
- const heading = { title: false, subtitle: false, tag: false };
3820
- const counts = /* @__PURE__ */ new Map();
3821
- let count = 0;
3822
- for (const placeholder of Array.isArray(record6?.placeholders) ? record6.placeholders : []) {
3823
- const type = placeholder?.type;
3824
- if (type === "title" || type === "subtitle" || type === "tag") heading[type] = true;
3825
- else if (typeof type === "string" && bodyKinds.has(type)) {
3826
- counts.set(type, (counts.get(type) ?? 0) + 1);
3827
- count += 1;
3828
- }
3829
- }
3830
- let kind2 = "title", best = 0;
3831
- for (const [candidate, total] of counts) if (total > best) {
3832
- kind2 = candidate;
3833
- best = total;
3834
- }
3835
- return { kind: kind2, count, heading };
3836
- }
3837
-
3838
4000
  // src/composition.ts
3839
4001
  var MAX_COMPOSITION_DEPTH = 32;
3840
4002
  var FALLBACK_CODE_FAMILY = "Roboto Mono";
3841
4003
  var DEFAULT_FONT_FAMILIES = { heading: String(ENGINE_DEFAULT_FONT_SCHEME.major), body: String(ENGINE_DEFAULT_FONT_SCHEME.minor) };
3842
4004
  function resolveFontFamilies(input) {
3843
- const scheme = record5(input);
4005
+ const scheme = record6(input);
3844
4006
  const family = (value) => typeof value === "string" && value ? value : void 0;
3845
4007
  const accent = family(scheme.accent);
3846
4008
  return {
@@ -3911,6 +4073,12 @@ function translateTextFit(fit, dy) {
3911
4073
  }
3912
4074
  };
3913
4075
  }
4076
+ var FIELD_KINDS = { text: "text", bullets: "text", items: "list", image: "image", video: "video", chart: "chart", table: "table", code: "code", metric: "metric", quote: "quote", timeline: "timeline" };
4077
+ function chartPrimaryLayout(side, kinds) {
4078
+ const first = side === "left" || side === "top";
4079
+ const group = { type: "group", composition: {}, placeholders: kinds.map((type) => ({ type })) };
4080
+ return { composition: { mode: side === "left" || side === "right" ? "row" : "column", weights: first ? [3, 2] : [2, 3] }, placeholders: first ? [{ type: "chart" }, group] : [group, { type: "chart" }] };
4081
+ }
3914
4082
  var OPFCompositionError = class extends Error {
3915
4083
  constructor(diagnostics, explanation) {
3916
4084
  super("Slide content does not fit its composition.");
@@ -3926,7 +4094,7 @@ var fields = ["text", "items", "bullets", "image", "video", "chart", "table", "c
3926
4094
  var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
3927
4095
  var rows2 = ["top", "middle", "bottom"];
3928
4096
  var columns = ["left", "center", "right"];
3929
- var record5 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
4097
+ var record6 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3930
4098
  var kind = (field) => field === "items" ? "list" : field === "bullets" ? "text" : field;
3931
4099
  var round2 = (value) => Math.round(value * 1e6) / 1e6 || value;
3932
4100
  var PLACEMENT_BAND = 0.5;
@@ -3940,13 +4108,13 @@ var imagePayload = (host) => {
3940
4108
  for (const key of IMAGE_OPTION_KEYS) if (host[key] !== void 0) payload[key] = host[key];
3941
4109
  return payload;
3942
4110
  };
3943
- var assetSource = (value) => typeof value === "string" ? value : record5(value).src;
4111
+ var assetSource = (value) => typeof value === "string" ? value : record6(value).src;
3944
4112
  var finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max ? value : void 0;
3945
4113
  var pathNumber = (value) => String(round2(value));
3946
4114
  var IMAGE_SHAPES = { rectangle: "rect", rounded: "roundRect", circle: "ellipse", hexagon: "hexagon" };
3947
4115
  var roundBox = (box) => ({ x: round2(box.x), y: round2(box.y), width: round2(box.width), height: round2(box.height) });
3948
4116
  var imageFocus = (value) => {
3949
- const focus = record5(value), x = finite(focus.x, 0, 1), y = finite(focus.y, 0, 1);
4117
+ const focus = record6(value), x = finite(focus.x, 0, 1), y = finite(focus.y, 0, 1);
3950
4118
  return x !== void 0 && y !== void 0 ? { x, y } : { x: 0.5, y: 0.5 };
3951
4119
  };
3952
4120
  function imageShape(kind2, box, cornerRadius = 1 / 6) {
@@ -3987,7 +4155,7 @@ function fitImage(frame, fit, aspect, focus = { x: 0.5, y: 0.5 }) {
3987
4155
  }
3988
4156
  function imageBackground(value) {
3989
4157
  if (typeof value === "string") return IMAGE_SOURCE.test(value) ? { src: value, fit: "cover", focus: { x: 0.5, y: 0.5 } } : void 0;
3990
- const background = record5(value);
4158
+ const background = record6(value);
3991
4159
  if (background.type !== "image" || typeof background.src !== "string" || !background.src) return void 0;
3992
4160
  const opacity = finite(background.opacity, 0, 1);
3993
4161
  return {
@@ -4002,11 +4170,11 @@ function imageBackground(value) {
4002
4170
  }
4003
4171
  function composeRecolor(value) {
4004
4172
  if (value === "grayscale") return { type: "grayscale" };
4005
- const duotone = record5(value);
4173
+ const duotone = record6(value);
4006
4174
  return duotone.dark !== void 0 && duotone.light !== void 0 ? { type: "duotone", dark: duotone.dark, light: duotone.light } : void 0;
4007
4175
  }
4008
4176
  function composeOverlay(value, frame, shape, path, mirrorSide, diagnostics) {
4009
- const overlay = record5(value), opacity = finite(overlay.opacity, 0, 1);
4177
+ const overlay = record6(value), opacity = finite(overlay.opacity, 0, 1);
4010
4178
  if (overlay.color === void 0 || opacity === void 0) return void 0;
4011
4179
  const edge = IMAGE_EDGES.includes(overlay.edge) ? mirrorSide(overlay.edge) : void 0;
4012
4180
  if (edge && shape.kind !== "rectangle") {
@@ -4031,7 +4199,7 @@ function composeImage(host, hostPath, region, frameArea, context) {
4031
4199
  const focus = imageFocus(host.focus);
4032
4200
  const shape = imageShape(kindName, box, finite(host.cornerRadius, 0, 0.5));
4033
4201
  const result = { region: roundBox(region), box, fit, focus, shape };
4034
- const border = record5(host.border), borderWidth = finite(border.width, 0, 64);
4202
+ const border = record6(host.border), borderWidth = finite(border.width, 0, 64);
4035
4203
  if (border.color !== void 0 && borderWidth) result.border = { color: border.color, width: round2(borderWidth * scale) };
4036
4204
  const opacity = finite(host.opacity, 0, 1);
4037
4205
  if (opacity !== void 0 && opacity < 1) result.opacity = opacity;
@@ -4045,7 +4213,7 @@ function composeImage(host, hostPath, region, frameArea, context) {
4045
4213
  return result;
4046
4214
  }
4047
4215
  function resolveBackgroundImage(slide, presentation, themeBackground, width, height, path, mirrorSide, diagnostics) {
4048
- const own2 = record5(slide.design), deck = record5(record5(presentation).design);
4216
+ const own2 = record6(slide.design), deck = record6(record6(presentation).design);
4049
4217
  const [value, at3] = own2.background !== void 0 ? [own2.background, `${path}.design.background`] : deck.background !== void 0 ? [deck.background, "design.background"] : [themeBackground, "theme"];
4050
4218
  const background = imageBackground(value);
4051
4219
  if (!background) return void 0;
@@ -4056,7 +4224,7 @@ function resolveBackgroundImage(slide, presentation, themeBackground, width, hei
4056
4224
  if (recolor) result.recolor = recolor;
4057
4225
  const overlay = composeOverlay(background.overlay, box, imageShape("rectangle", box), `${at3}.overlay`, mirrorSide, diagnostics);
4058
4226
  if (overlay) result.overlay = overlay;
4059
- const aspect = background.fit === "tile" ? void 0 : intrinsicImageAspect(background.src, record5(presentation).assets);
4227
+ const aspect = background.fit === "tile" ? void 0 : intrinsicImageAspect(background.src, record6(presentation).assets);
4060
4228
  if (aspect) result.picture = fitImage(box, background.fit, aspect, background.focus);
4061
4229
  return result;
4062
4230
  }
@@ -4077,10 +4245,10 @@ function resolveLogo(presentation, slide, options = {}) {
4077
4245
  };
4078
4246
  const pick = (value, path) => {
4079
4247
  if (value === void 0 || value === null || value === false) return null;
4080
- if (typeof value === "string" || typeof value === "object" && !Array.isArray(value) && "src" in record5(value)) {
4248
+ if (typeof value === "string" || typeof value === "object" && !Array.isArray(value) && "src" in record6(value)) {
4081
4249
  return usable(value) ? { source: value, path, variant: "default", slot } : null;
4082
4250
  }
4083
- const set2 = record5(value);
4251
+ const set2 = record6(value);
4084
4252
  if (!Object.keys(set2).some((key) => LOGO_SET_KEYS.has(key))) return null;
4085
4253
  for (const key of chain) {
4086
4254
  const variant = set2[key];
@@ -4088,16 +4256,16 @@ function resolveLogo(presentation, slide, options = {}) {
4088
4256
  }
4089
4257
  return null;
4090
4258
  };
4091
- const deck = record5(presentation), slideIndex = Number.isSafeInteger(options.slideIndex) && options.slideIndex >= 0 ? options.slideIndex : 0;
4092
- const own2 = pick(record5(record5(slide).design).logo, `slides.${slideIndex}.design.logo`);
4259
+ const deck = record6(presentation), slideIndex = Number.isSafeInteger(options.slideIndex) && options.slideIndex >= 0 ? options.slideIndex : 0;
4260
+ const own2 = pick(record6(record6(slide).design).logo, `slides.${slideIndex}.design.logo`);
4093
4261
  if (own2) return own2;
4094
- const shared = pick(record5(deck.design).logo, "design.logo");
4262
+ const shared = pick(record6(deck.design).logo, "design.logo");
4095
4263
  if (shared) return shared;
4096
4264
  const organizations = Array.isArray(deck.organization) ? deck.organization : [deck.organization];
4097
- const primaryIndex = organizations.findIndex((item) => record5(item).role === "primary");
4265
+ const primaryIndex = organizations.findIndex((item) => record6(item).role === "primary");
4098
4266
  const index = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
4099
4267
  if (index < 0) return null;
4100
- return pick(record5(organizations[index]).logo, Array.isArray(deck.organization) ? `organization.${index}.logo` : "organization.logo");
4268
+ return pick(record6(organizations[index]).logo, Array.isArray(deck.organization) ? `organization.${index}.logo` : "organization.logo");
4101
4269
  }
4102
4270
  var webUrl = /^https?:\/\/\S+$/i;
4103
4271
  function resolveSocialProfile(platform, value, owner = "organization") {
@@ -4115,8 +4283,8 @@ function resolveSocialProfile(platform, value, owner = "organization") {
4115
4283
  return { text: display(url), href: encodeURI(url), resolved: true };
4116
4284
  }
4117
4285
  function layoutFurniture(input, options = {}) {
4118
- const slide = record5(input), width = options.width ?? 1280, height = options.height ?? 720, scale = Math.min(width, height) / 720;
4119
- const settings = { ...record5(record5(options.layout).composition), ...record5(slide.composition) };
4286
+ const slide = record6(input), width = options.width ?? 1280, height = options.height ?? 720, scale = Math.min(width, height) / 720;
4287
+ const settings = { ...record6(record6(options.layout).composition), ...record6(slide.composition) };
4120
4288
  assertComposition(settings);
4121
4289
  const furnitureRtl = (options.direction ?? resolveSlideDirection(options.presentation, options.slideIndex)) === "rtl";
4122
4290
  const minimum = snapFontSizeUp((settings.minFontSize ?? 16) * scale), size = gridFontSize(13 * scale, minimum), padding = (options.textRasterPadding ?? 2) * scale;
@@ -4128,14 +4296,13 @@ function layoutFurniture(input, options = {}) {
4128
4296
  if (options.date !== void 0 && (typeof options.date !== "string" || !parseIsoDate(options.date))) throw new RangeError("The furniture date option must be an ISO YYYY-MM-DD calendar date.");
4129
4297
  const outlines = options.textMeasurement?.outlineBounds !== void 0, parts = [], diagnostics = [];
4130
4298
  const sourceRoot = `slides.${options.slideIndex ?? 0}`, organizations = Array.isArray(options.presentation?.organization) ? options.presentation.organization : [options.presentation?.organization];
4131
- const primaryIndex = organizations.findIndex((item) => record5(item).role === "primary"), organizationIndex = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
4132
- const organization = record5(organizations[organizationIndex]), organizationRoot = Array.isArray(options.presentation?.organization) ? `organization.${organizationIndex}` : "organization", organizationPath = `${organizationRoot}.name`;
4133
- const speakers = Array.isArray(options.presentation?.speaker) ? options.presentation.speaker : [options.presentation?.speaker], speaker = record5(speakers[0]), speakerRoot = Array.isArray(options.presentation?.speaker) ? "speaker.0" : "speaker";
4299
+ const primaryIndex = organizations.findIndex((item) => record6(item).role === "primary"), organizationIndex = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
4300
+ const organization = record6(organizations[organizationIndex]), organizationRoot = Array.isArray(options.presentation?.organization) ? `organization.${organizationIndex}` : "organization";
4134
4301
  const fontFamily = options.fontFamilies?.body ?? "sans-serif";
4135
4302
  let headerBottom = 0, footerTop = height, configured = false;
4136
4303
  const error = (path, message, code = "text-overflow") => diagnostics.push({ code, path, message });
4137
4304
  for (const kind2 of ["header", "footer"]) {
4138
- const local = record5(slide.design)[kind2] !== void 0, value = local ? record5(slide.design)[kind2] : options.presentation?.design?.[kind2];
4305
+ const local = record6(slide.design)[kind2] !== void 0, value = local ? record6(slide.design)[kind2] : options.presentation?.design?.[kind2];
4139
4306
  if (value === void 0 || value === false) continue;
4140
4307
  const root = local ? `${sourceRoot}.design.${kind2}` : `design.${kind2}`;
4141
4308
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Header/footer content must be an object or false.");
@@ -4143,7 +4310,7 @@ function layoutFurniture(input, options = {}) {
4143
4310
  const zones = [];
4144
4311
  for (const [index, zone] of ["left", "center", "right"].entries()) {
4145
4312
  const side = furnitureRtl && zone !== "center" ? zone === "left" ? "right" : "left" : zone;
4146
- const content = record5(record5(value)[zone]), path = `${root}.${zone}`, x = width * (0.07 + (furnitureRtl ? 2 - index : index) * 0.3), zoneWidth = width * 0.26, zoneParts = [];
4313
+ const content = record6(record6(value)[zone]), path = `${root}.${zone}`, x = width * (0.07 + (furnitureRtl ? 2 - index : index) * 0.3), zoneWidth = width * 0.26, zoneParts = [];
4147
4314
  let y = 0;
4148
4315
  const add = (field, text2, generated = false, sourcePath, extras = {}) => {
4149
4316
  if (text2 === void 0) return;
@@ -4180,7 +4347,7 @@ function layoutFurniture(input, options = {}) {
4180
4347
  y += box.height;
4181
4348
  };
4182
4349
  const imageBox = (source) => {
4183
- const imageHeight = Math.max(32 * scale, Math.min(height * 0.05, 72 * scale)), imageWidth = Math.min(zoneWidth, imageHeight * (intrinsicImageAspect(source, record5(options.presentation).assets) ?? 1));
4350
+ const imageHeight = Math.max(32 * scale, Math.min(height * 0.05, 72 * scale)), imageWidth = Math.min(zoneWidth, imageHeight * (intrinsicImageAspect(source, record6(options.presentation).assets) ?? 1));
4184
4351
  return { x: side === "left" ? x : side === "center" ? x + (zoneWidth - imageWidth) / 2 : x + zoneWidth - imageWidth, y, width: imageWidth, height: imageHeight };
4185
4352
  };
4186
4353
  if (content.logo === true) {
@@ -4196,30 +4363,17 @@ function layoutFurniture(input, options = {}) {
4196
4363
  zoneParts.push({ type: "image", kind: kind2, zone, field: "image", path: `${path}.image`, sourcePath: `${path}.image`, generated: false, image: content.image, box, alignment: side });
4197
4364
  y += box.height;
4198
4365
  }
4199
- add("text", content.text);
4200
- if (content.organization === true) {
4201
- if (typeof organization.name === "string") add("organization", organization.name, true, organizationPath);
4202
- else error(`${path}.organization`, "Generated organization name needs a named organization in the presentation.", "unresolved-content");
4203
- }
4204
- if (content.speaker === true) {
4205
- if (typeof speaker.name === "string" && speaker.name.trim()) add("speaker", typeof speaker.title === "string" && speaker.title.trim() ? `${speaker.name}, ${speaker.title}` : speaker.name, true, `${speakerRoot}.name`);
4206
- else error(`${path}.speaker`, "Generated speaker needs a named speaker in the presentation.", "unresolved-content");
4207
- }
4366
+ if (typeof content.text === "string") {
4367
+ const resolved = substituteSlideTokens(content.text, { slideNumber: number, ...slideCount !== void 0 ? { slideCount } : {}, section: slide.section });
4368
+ if (slideCount === void 0 && usesSlideBuiltin(content.text, "deck.slideCount")) error(`${path}.text`, "{{deck.slideCount}} needs the rendered slide count (the slideCount option or the presentation slides).", "unresolved-content");
4369
+ add("text", resolved.text, false, void 0, { fields: resolved.fields });
4370
+ } else add("text", content.text);
4208
4371
  if (content.socials === true) {
4209
- const links = Object.entries(record5(organization.socials)).filter(([, value2]) => typeof value2 === "string" && value2.trim()).map(([platform, value2]) => ({ platform, sourcePath: `${organizationRoot}.socials.${platform}`, ...resolveSocialProfile(platform, value2, "organization") }));
4372
+ const links = Object.entries(record6(organization.socials)).filter(([, value2]) => typeof value2 === "string" && value2.trim()).map(([platform, value2]) => ({ platform, sourcePath: `${organizationRoot}.socials.${platform}`, ...resolveSocialProfile(platform, value2, "organization") }));
4210
4373
  if (links.length) add("socials", links.map((link) => link.text).join("\n"), true, `${organizationRoot}.socials`, { links });
4211
4374
  else error(`${path}.socials`, "Generated social profiles need a primary organization with socials.", "unresolved-content");
4212
4375
  }
4213
- if (content.section === true) {
4214
- if (typeof slide.section === "string") add("section", slide.section, true, `${sourceRoot}.section`);
4215
- else error(`${path}.section`, "Generated section needs a literal slide section.", "unresolved-content");
4216
- }
4217
- for (const setting of ["slideNumberFormat", "dateFormat"]) if (content[setting] !== void 0 && typeof content[setting] !== "string") throw new TypeError(`Furniture setting ${path}.${setting} requires a string.`);
4218
- if (content.slideNumber === true) {
4219
- const resolved = formatSlideNumber(content.slideNumberFormat ?? DEFAULT_SLIDE_NUMBER_FORMAT, number, slideCount);
4220
- if ("error" in resolved) error(`${path}.slideNumberFormat`, resolved.error, "unresolved-content");
4221
- else add("slideNumber", resolved.text, true, void 0, { fields: resolved.fields });
4222
- }
4376
+ if (content.dateFormat !== void 0 && typeof content.dateFormat !== "string") throw new TypeError(`Furniture setting ${path}.dateFormat requires a string.`);
4223
4377
  if (content.date === true) {
4224
4378
  const format = content.dateFormat ?? DEFAULT_FURNITURE_DATE_FORMAT;
4225
4379
  if (options.date === void 0) error(`${path}.date`, "A current date needs a host-supplied ISO date option. For fixed content use a literal date, or an ISO date with dateFormat.", "unresolved-content");
@@ -4755,19 +4909,19 @@ function fitSourceText(text2, box, size, minimum, step, measure, prose = false,
4755
4909
  const layout = (fontSize) => {
4756
4910
  const tabWidth = measure(" ", fontSize) * 4;
4757
4911
  if (!Number.isFinite(tabWidth) || text2.includes(" ") && tabWidth <= 0) throw new RangeError("Text tabs require a positive finite measured space advance.");
4758
- const measureLine = (start2, end2, record6 = false) => {
4912
+ const measureLine = (start2, end2, record7 = false) => {
4759
4913
  let x = 0, offset = start2;
4760
4914
  const segments = [];
4761
4915
  for (const [index, segment2] of text2.slice(start2, end2).split(" ").entries()) {
4762
4916
  if (index) {
4763
4917
  const next = (Math.floor(x / tabWidth + 1e-9) + 1) * tabWidth;
4764
- if (record6) segments.push({ kind: "tab", start: offset, end: offset + 1, x, width: next - x });
4918
+ if (record7) segments.push({ kind: "tab", start: offset, end: offset + 1, x, width: next - x });
4765
4919
  x = next;
4766
4920
  offset++;
4767
4921
  }
4768
4922
  if (segment2) {
4769
4923
  const advance = measure(segment2, fontSize);
4770
- if (record6) segments.push({ kind: "text", start: offset, end: offset + segment2.length, x, width: advance });
4924
+ if (record7) segments.push({ kind: "text", start: offset, end: offset + segment2.length, x, width: advance });
4771
4925
  x += advance;
4772
4926
  offset += segment2.length;
4773
4927
  }
@@ -5124,12 +5278,12 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
5124
5278
  function contentText(field, value) {
5125
5279
  if (field === "text" || headings.has(field)) return flatten(value);
5126
5280
  if (field === "items" || field === "bullets") return (Array.isArray(value) ? value : [value]).map((item) => {
5127
- const entry = record5(item);
5281
+ const entry = record6(item);
5128
5282
  return `\u2022 ${flatten(item)}${entry.description ? `
5129
5283
  ${flatten(entry.description)}` : ""}`;
5130
5284
  }).join("\n");
5131
- if (field === "code") return flatten(record5(value).source ?? value);
5132
- if (field === "quote") return flatten(record5(value).text ?? value);
5285
+ if (field === "code") return flatten(record6(value).source ?? value);
5286
+ if (field === "quote") return flatten(record6(value).text ?? value);
5133
5287
  return void 0;
5134
5288
  }
5135
5289
  function layoutTable(value, box, options = {}) {
@@ -5200,7 +5354,7 @@ function flatten(value) {
5200
5354
  if (value == null) return "";
5201
5355
  if (typeof value !== "object") return String(value);
5202
5356
  if (Array.isArray(value)) return value.map(flatten).join("");
5203
- const item = record5(value);
5357
+ const item = record6(value);
5204
5358
  return flatten(item.text ?? item.value ?? item.source ?? JSON.stringify(value));
5205
5359
  }
5206
5360
  function regionParts(key) {
@@ -5256,7 +5410,7 @@ function assertComposition(value) {
5256
5410
  if (value.overflow !== void 0 && !["warn", "error"].includes(value.overflow)) throw new RangeError("Invalid composition.overflow.");
5257
5411
  }
5258
5412
  function composeSlide(input, options = {}) {
5259
- const slide = record5(input), layout = record5(options.layout);
5413
+ const slide = record6(input), layout = record6(options.layout);
5260
5414
  const width = options.width ?? 1280, height = options.height ?? 720;
5261
5415
  if (![width, height].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
5262
5416
  const scale = Math.min(width, height) / 720;
@@ -5264,7 +5418,7 @@ function composeSlide(input, options = {}) {
5264
5418
  const rtl = deckDirection === "rtl", textDirection = rtl ? "rtl" : void 0;
5265
5419
  const hints = resolveDesignHints({ slide, layout, presentation: options.presentation, slideIndex: options.slideIndex, deck: { titleAlignment: options.titleAlignment, contentAlignment: options.contentAlignment, contentBox: options.contentBox } });
5266
5420
  const hasCards = hints.contentBox ?? false;
5267
- const composition = { ...record5(layout.composition), ...record5(slide.composition) };
5421
+ const composition = { ...record6(layout.composition), ...record6(slide.composition) };
5268
5422
  assertComposition(composition);
5269
5423
  const padding = (composition.padding ?? 0.08) * Math.min(width, height);
5270
5424
  const gap = (composition.gap ?? 1 / 30) * Math.min(width, height);
@@ -5345,16 +5499,16 @@ function composeSlide(input, options = {}) {
5345
5499
  const mirrorSide = (side) => !rtl ? side : side === "left" ? "right" : side === "right" ? "left" : side;
5346
5500
  const backgroundImage = resolveBackgroundImage(slide, options.presentation, options.themeBackground, width, height, path, mirrorSide, imageDiagnostics);
5347
5501
  const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
5348
- const assets = record5(options.presentation).assets;
5502
+ const assets = record6(options.presentation).assets;
5349
5503
  const imageContext = (placement) => ({ fit: hints.imageFit, scale, mirrorSide, diagnostics: imageDiagnostics, assets, ...placement ? { placement } : {} });
5350
5504
  const area = { left: 0, top: 0, right: width, bottom: height };
5351
- const imagePlaceholders = (Array.isArray(layout.placeholders) ? layout.placeholders : []).flatMap((placeholder, index) => record5(placeholder).type === "image" ? [{ placeholder: record5(placeholder), index }] : []);
5352
- const topImages = regions.length ? [] : Array.isArray(slide.blocks) ? slide.blocks.flatMap((block, index) => !Array.isArray(record5(block).blocks) && record5(block).image !== void 0 ? [{ host: record5(block), hostPath: `${path}.blocks.${index}`, block: index }] : []) : slide.image !== void 0 ? [{ host: { type: "image", image: slide.image }, hostPath: path }] : [];
5505
+ const imagePlaceholders = layoutLeaves(layout).flatMap((leaf) => leaf.type === "image" ? [{ placeholder: leaf.placeholder, path: leaf.path, depth: leaf.depth }] : []);
5506
+ const topImages = regions.length ? [] : Array.isArray(slide.blocks) ? slide.blocks.flatMap((block, index) => !Array.isArray(record6(block).blocks) && record6(block).image !== void 0 ? [{ host: record6(block), hostPath: `${path}.blocks.${index}`, block: index }] : []) : slide.image !== void 0 ? [{ host: { type: "image", image: slide.image }, hostPath: path }] : [];
5353
5507
  const placed = [], usedEdges = /* @__PURE__ */ new Set();
5354
5508
  topImages.forEach((node, order) => {
5355
5509
  const slot = imagePlaceholders[order];
5356
- const own2 = node.block !== void 0 && node.host.placement !== void 0 ? record5(node.host.placement) : void 0;
5357
- const value = own2 ?? (slot?.placeholder.placement !== void 0 ? record5(slot.placeholder.placement) : void 0);
5510
+ const own2 = node.block !== void 0 && node.host.placement !== void 0 ? record6(node.host.placement) : void 0;
5511
+ const value = own2 ?? (slot?.depth === 0 && slot.placeholder.placement !== void 0 ? record6(slot.placeholder.placement) : void 0);
5358
5512
  if (!value || !IMAGE_EDGES.includes(value.edge)) return;
5359
5513
  const edge = mirrorSide(value.edge);
5360
5514
  if (usedEdges.has(edge)) return;
@@ -5367,7 +5521,7 @@ function composeSlide(input, options = {}) {
5367
5521
  else if (edge === "right") area.right = region.x;
5368
5522
  else if (edge === "top") area.top += band;
5369
5523
  else area.bottom = region.y;
5370
- const placement = { edge, size, inset: value.inset === true, path: own2 ? `${node.hostPath}.placement` : `layout.placeholders.${slot.index}.placement` };
5524
+ const placement = { edge, size, inset: value.inset === true, path: own2 ? `${node.hostPath}.placement` : `${slot.path}.placement` };
5371
5525
  placed.push({ ...node, placement, region, slot: order });
5372
5526
  });
5373
5527
  const placedBlocks = new Set(placed.flatMap((entry) => entry.block !== void 0 ? [entry.block] : []));
@@ -5390,7 +5544,7 @@ function composeSlide(input, options = {}) {
5390
5544
  const emptyPayload = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
5391
5545
  const emptyHost = (host) => emptyPayload(host.blocks) && fields.every((field) => emptyPayload(host[field]));
5392
5546
  const flowBlocks = Array.isArray(slide.blocks) ? slide.blocks.filter((_, index) => !placedBlocks.has(index)) : slide.blocks;
5393
- const hasBodyPayload = regions.some((key) => !emptyHost(record5(slide[key]))) || !emptyPayload(flowBlocks) || fields.some((field) => !(placedRoot && field === "image") && !emptyPayload(slide[field]));
5547
+ const hasBodyPayload = regions.some((key) => !emptyHost(record6(slide[key]))) || !emptyPayload(flowBlocks) || fields.some((field) => !(placedRoot && field === "image") && !emptyPayload(slide[field]));
5394
5548
  const isCover = !hasBodyPayload && (headingOnlyLayout || !hasLayoutRecord);
5395
5549
  coverGroup = isCover;
5396
5550
  let logo;
@@ -5457,7 +5611,7 @@ function composeSlide(input, options = {}) {
5457
5611
  const collect = (host, basePath, depth = 0, ancestors = []) => {
5458
5612
  if (Array.isArray(host.blocks)) {
5459
5613
  if (depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(host)) throw new RangeError(`Content groups must be acyclic and nest at most ${MAX_COMPOSITION_DEPTH} levels.`);
5460
- const settings = record5(host.composition);
5614
+ const settings = record6(host.composition);
5461
5615
  assertComposition(settings);
5462
5616
  return [{
5463
5617
  field: "blocks",
@@ -5466,7 +5620,7 @@ function composeSlide(input, options = {}) {
5466
5620
  path: basePath,
5467
5621
  payload: host,
5468
5622
  composition: settings,
5469
- children: host.blocks.flatMap((block, index) => collect(record5(block), `${basePath}.blocks.${index}`, depth + 1, [...ancestors, host]))
5623
+ children: host.blocks.flatMap((block, index) => collect(record6(block), `${basePath}.blocks.${index}`, depth + 1, [...ancestors, host]))
5470
5624
  }];
5471
5625
  }
5472
5626
  const captioned = host.caption !== void 0 ? fields.filter((field) => host[field] !== void 0 && CAPTIONABLE_FIELDS.includes(field)) : [];
@@ -5482,7 +5636,7 @@ function composeSlide(input, options = {}) {
5482
5636
  }));
5483
5637
  };
5484
5638
  const regionsInReadingOrder = visualReadingOrder(regions.map((key) => ({ key, box: regionBox(regionParts(key), contentBox, gap) }))).map((entry) => entry.key);
5485
- const pending = regions.length ? regionsInReadingOrder.flatMap((key) => collect(record5(slide[key]), `${path}.${key}`).map((item) => ({ ...item, region: regionParts(key) }))) : Array.isArray(slide.blocks) ? slide.blocks.flatMap((block, index) => placedBlocks.has(index) ? [] : collect(record5(block), `${path}.blocks.${index}`)) : collect(placedRoot ? { ...slide, image: void 0 } : slide, path);
5639
+ const pending = regions.length ? regionsInReadingOrder.flatMap((key) => collect(record6(slide[key]), `${path}.${key}`).map((item) => ({ ...item, region: regionParts(key) }))) : Array.isArray(slide.blocks) ? slide.blocks.flatMap((block, index) => placedBlocks.has(index) ? [] : collect(record6(block), `${path}.blocks.${index}`)) : collect(placedRoot ? { ...slide, image: void 0 } : slide, path);
5486
5640
  const groups = [], flows = [];
5487
5641
  const decisions = options.explain ? [] : void 0;
5488
5642
  const inheritedSettings = (parent, own2 = {}) => ({
@@ -5580,12 +5734,23 @@ function composeSlide(input, options = {}) {
5580
5734
  };
5581
5735
  const scoreNode = (node, box, settings, penalties) => {
5582
5736
  if (!node.children) return leafScore(node, box, settings, penalties);
5583
- const own2 = inheritedSettings(settings, node.composition), area2 = inset(box, own2);
5584
- const cols = defaultColumns(node.children.length, area2, own2);
5585
- const boxes = gridBoxes(node.children.length, area2, cols, (own2.gap ?? 1 / 30) * Math.min(box.width, box.height), own2.weights ?? [], modeFor(own2) === "column");
5737
+ const own2 = inheritedSettings(settings, node.composition), area2 = inset(box, own2), slotCount = Math.max(node.children.length, node.reserved ?? 0);
5738
+ const cols = defaultColumns(slotCount, area2, own2);
5739
+ const boxes = gridBoxes(slotCount, area2, cols, (own2.gap ?? 1 / 30) * Math.min(box.width, box.height), own2.weights ?? [], modeFor(own2) === "column");
5586
5740
  return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own2, penalties), 0);
5587
5741
  };
5588
- const arrange = (nodes, area2, settings, reserved = 0, gapOverride, containerPath = path, recorded = true) => {
5742
+ const slots = [];
5743
+ const emptySlots = (entries, boxes, settings) => entries.forEach((entry, index) => {
5744
+ const box = boxes[index];
5745
+ if (!box) return;
5746
+ slots.push({ path: entry.path, type: entry.type, depth: entry.depth, box: acceptedBox(box) });
5747
+ if (!entry.children) return;
5748
+ const own2 = inheritedSettings(settings, record6(entry.placeholder.composition)), inner = inset(box, own2);
5749
+ const cols = defaultColumns(entry.children.length, inner, own2);
5750
+ const mirror = (cell) => rtl ? { ...cell, x: inner.x + inner.width - (cell.x - inner.x) - cell.width } : cell;
5751
+ emptySlots(entry.children, gridBoxes(entry.children.length, inner, cols, (own2.gap ?? 1 / 30) * Math.min(box.width, box.height), own2.weights ?? [], modeFor(own2) === "column").map(mirror), own2);
5752
+ });
5753
+ const arrange = (nodes, area2, settings, reserved = 0, gapOverride, containerPath = path, recorded = true, template) => {
5589
5754
  const count = Math.max(nodes.length, reserved);
5590
5755
  if (!count) return;
5591
5756
  const mode = modeFor(settings), localGap = (settings.gap ?? 1 / 30) * Math.min(area2.width, area2.height);
@@ -5597,9 +5762,9 @@ function composeSlide(input, options = {}) {
5597
5762
  let best = Infinity;
5598
5763
  for (let candidate = 1; candidate <= Math.min(count, settings.columns ?? 6); candidate++) {
5599
5764
  const boxes2 = gridBoxes(count, area2, candidate, actualGap, settings.weights ?? [], false);
5600
- const emptySlots = (Math.ceil(count / candidate) * candidate - count) * 2;
5601
- const penalties = candidates ? { cellProportions: 0, fontReduction: 0, textOverflow: 0, tableOverflow: 0, smallCells: 0, emptySlots } : void 0;
5602
- const score = nodes.reduce((sum, node, i) => sum + scoreNode(node, boxes2[i], settings, penalties), 0) + emptySlots;
5765
+ const emptySlots2 = (Math.ceil(count / candidate) * candidate - count) * 2;
5766
+ const penalties = candidates ? { cellProportions: 0, fontReduction: 0, textOverflow: 0, tableOverflow: 0, smallCells: 0, emptySlots: emptySlots2 } : void 0;
5767
+ const score = nodes.reduce((sum, node, i) => sum + scoreNode(node, boxes2[i], settings, penalties), 0) + emptySlots2;
5603
5768
  if (penalties && candidates) candidates.push({ columns: candidate, rows: Math.ceil(count / candidate), score, penalties });
5604
5769
  if (score < best) {
5605
5770
  best = score;
@@ -5617,13 +5782,15 @@ function composeSlide(input, options = {}) {
5617
5782
  const grid = gridGeometry(count, area2, cols, actualGap, settings.weights ?? [], mode === "column");
5618
5783
  const mirrored = (box) => rtl ? { ...box, x: area2.x + area2.width - (box.x - area2.x) - box.width } : box;
5619
5784
  const boxes = grid.boxes.map(mirrored);
5785
+ if (template) emptySlots(template.slice(nodes.length), boxes.slice(nodes.length), settings);
5620
5786
  if (recorded && !hasRegions) flows.push({ path: containerPath, box: { ...area2 }, composition: { ...settings }, columns: rtl ? grid.columns.map((track) => ({ offset: area2.width - track.offset - track.size, size: track.size })) : grid.columns, rows: grid.rows, gap: grid.gap, itemCount: nodes.length, slotCount: count });
5621
5787
  nodes.forEach((node, index) => {
5622
5788
  let box = node.region ? mirrored(regionBox(node.region, area2, actualGap)) : boxes[index];
5789
+ if (node.slot) slots.push({ path: node.slot.path, type: node.slot.type, depth: node.slot.depth, box: acceptedBox(box), ...node.slot.children ? {} : { content: node.path } });
5623
5790
  if (node.children) {
5624
5791
  const own2 = inheritedSettings(settings, node.composition), inner = inset(box, own2);
5625
5792
  if (!node.synthetic) groups.push({ path: node.path, box, contentBox: inner, composition: own2 });
5626
- arrange(node.children, inner, own2, 0, (own2.gap ?? 1 / 30) * Math.min(box.width, box.height), node.path, !node.synthetic);
5793
+ arrange(node.children, inner, own2, node.reserved ?? 0, (own2.gap ?? 1 / 30) * Math.min(box.width, box.height), node.path, !node.synthetic, node.slots);
5627
5794
  } else {
5628
5795
  const frameBox = hasCards ? acceptedBox(box) : void 0;
5629
5796
  box = payloadBox(box);
@@ -5669,18 +5836,59 @@ function composeSlide(input, options = {}) {
5669
5836
  if (placedSlots.size) {
5670
5837
  for (let index = placeholders.length - 1; index >= 0; index--) if (placedSlots.has(placeholders[index])) placeholders.splice(index, 1);
5671
5838
  }
5672
- const ownMode = record5(slide.composition).mode;
5839
+ const ownMode = record6(slide.composition).mode;
5673
5840
  const direction = hints.contentDirection;
5674
5841
  const directionMode = direction === "vertical" ? "column" : direction === "horizontal" ? "row" : "auto";
5675
5842
  const chartHint = hints.chartPrimary;
5676
5843
  const chartSide = chartHint === "left" || chartHint === "right" || chartHint === "top" || chartHint === "bottom" ? chartHint : void 0;
5677
5844
  const chartIndex = pending.findIndex((node) => !node.children && node.field === "chart");
5678
5845
  const chartPrimary = chartSide !== void 0 && !ownMode && !regions.length && chartIndex >= 0 && pending.some((node) => node.children || node.field !== "chart") ? chartSide : void 0;
5679
- const rootSettings = chartPrimary ? { ...composition, mode: chartPrimary === "left" || chartPrimary === "right" ? "row" : "column", columns: void 0, weights: chartPrimary === "left" || chartPrimary === "top" ? [3, 2] : [2, 3] } : { ...composition, mode: composition.mode ?? directionMode };
5680
- if (chartPrimary) {
5846
+ const kindOf = (node) => node.children ? node.children[0] ? kindOf(node.children[0]) : "text" : FIELD_KINDS[node.field] ?? "text";
5847
+ const sugar = chartPrimary ? chartPrimaryLayout(chartPrimary, pending.filter((_, index) => index !== chartIndex).map(kindOf)) : void 0;
5848
+ const rootSettings = sugar ? { ...composition, ...sugar.composition, columns: void 0 } : { ...composition, mode: composition.mode ?? directionMode };
5849
+ const recordSlots = sugar ? [] : layoutSlots(layout).filter((slot) => !headings.has(slot.type) && !placedSlots.has(slot.placeholder));
5850
+ const templated = !sugar && !regions.length && recordSlots.some((slot) => slot.children) && pending.every((node) => !node.children);
5851
+ const fillSlots = (entries, queue, cursor, recordSlot) => {
5852
+ const nodes = [];
5853
+ for (const entry of entries) {
5854
+ if (cursor.next >= queue.length) break;
5855
+ if (!entry.children) {
5856
+ nodes.push({ ...queue[cursor.next++], ...recordSlot ? { slot: entry } : {} });
5857
+ continue;
5858
+ }
5859
+ const own2 = record6(entry.placeholder.composition);
5860
+ assertComposition(own2);
5861
+ const children = fillSlots(entry.children, queue, cursor, recordSlot);
5862
+ nodes.push({
5863
+ field: "blocks",
5864
+ type: "group",
5865
+ value: children.map((child) => child.payload),
5866
+ path,
5867
+ payload: {},
5868
+ children,
5869
+ composition: own2,
5870
+ synthetic: true,
5871
+ reserved: own2.mode ? 0 : entry.children.length,
5872
+ ...recordSlot ? { slot: entry, slots: entry.children } : {}
5873
+ });
5874
+ }
5875
+ return nodes;
5876
+ };
5877
+ if (sugar) {
5681
5878
  const primary = pending[chartIndex], rest = pending.filter((_, index) => index !== chartIndex);
5682
- const container = { field: "blocks", type: "group", value: rest.map((node) => node.payload), path, payload: {}, children: rest, composition: {}, synthetic: true };
5683
- arrange(chartPrimary === "left" || chartPrimary === "top" ? [primary, container] : [container, primary], contentBox, rootSettings, 0);
5879
+ const queue = chartPrimary === "left" || chartPrimary === "top" ? [primary, ...rest] : [...rest, primary];
5880
+ arrange(fillSlots(layoutSlots(sugar, "design.chartPrimary"), queue, { next: 0 }, false), contentBox, rootSettings, 0);
5881
+ } else if (templated) {
5882
+ const cursor = { next: 0 };
5883
+ const nodes = fillSlots(recordSlots, pending, cursor, true);
5884
+ arrange([...nodes, ...pending.slice(cursor.next)], contentBox, rootSettings, composition.mode ? 0 : placeholders.length, void 0, path, true, recordSlots);
5885
+ const order = /* @__PURE__ */ new Map();
5886
+ const number = (entries) => entries.forEach((entry) => {
5887
+ order.set(entry.path, order.size);
5888
+ if (entry.children) number(entry.children);
5889
+ });
5890
+ number(recordSlots);
5891
+ slots.sort((a, b) => order.get(a.path) - order.get(b.path));
5684
5892
  } else arrange(pending, contentBox, rootSettings, composition.mode ? 0 : placeholders.length);
5685
5893
  if (listBullet?.value === "image" && !bulletImage && items.some((item) => (item.field === "items" || item.field === "bullets") && item.payload.numbering === void 0)) {
5686
5894
  diagnostics.push({ code: "unresolved-content", path: listBullet.path, message: "Picture bullets (listBullet: image) need design.logo or a primary organization logo; the marker glyph is drawn instead." });
@@ -5721,7 +5929,8 @@ function composeSlide(input, options = {}) {
5721
5929
  } : void 0;
5722
5930
  if (failures.length) throw new OPFCompositionError(failures, explanation);
5723
5931
  diagnostics.push(...imageDiagnostics, ...numberingDiagnostics);
5724
- return { width, height, contentBox, items, groups, flows, diagnostics, composition, design: hints, ...furniture ? { furniture } : {}, ...backgroundImage ? { backgroundImage } : {}, ...logo ? { logo } : {}, ...rtl ? { direction: "rtl" } : {}, ...footnotes ? { footnotes } : {}, ...explanation ? { explanation } : {} };
5932
+ for (const slot of slots) for (const key of ["x", "y", "width", "height"]) slot.box[key] = round2(slot.box[key]);
5933
+ return { width, height, contentBox, items, groups, flows, ...slots.length ? { slots } : {}, diagnostics, composition, design: hints, ...furniture ? { furniture } : {}, ...backgroundImage ? { backgroundImage } : {}, ...logo ? { logo } : {}, ...rtl ? { direction: "rtl" } : {}, ...footnotes ? { footnotes } : {}, ...explanation ? { explanation } : {} };
5725
5934
  }
5726
5935
  function resolveCanvasDimensions(input) {
5727
5936
  const presets = {
@@ -5736,7 +5945,7 @@ function resolveCanvasDimensions(input) {
5736
5945
  letter: [11, 8.5],
5737
5946
  a4: [11.69, 8.27]
5738
5947
  };
5739
- const value = record5(input);
5948
+ const value = record6(input);
5740
5949
  const preset = presets[typeof input === "string" ? input : value.preset] ?? presets.widescreen;
5741
5950
  const width = (value.widthInches ?? preset[0]) * 96;
5742
5951
  const height = (value.heightInches ?? preset[1]) * 96;
@@ -5744,4 +5953,4 @@ function resolveCanvasDimensions(input) {
5744
5953
  return { width, height };
5745
5954
  }
5746
5955
 
5747
- export { CAPTIONABLE_FIELDS, CAPTION_FONT_RATIO, CAPTION_MAX_RATIO, CHART_HIGHLIGHT_MUTED_MIN_CONTRAST, CHART_HIGHLIGHT_MUTED_MIX, CHART_SERIES_MIN_DIFFERENCE, CHART_SERIES_MIN_LIGHTNESS_STEP, CHART_TYPES, CITATION_MARKER_RAISE, CITATION_MARKER_SCALE, CODE_HIGHLIGHT_LANGUAGES, CODE_HIGHLIGHT_MAX_LENGTH, CODE_PANEL_BACKGROUND, CODE_PANEL_FOREGROUND, CODE_SYNTAX_MIN_CONTRAST, DARK_BACKGROUND_LUMINANCE, DEFAULT_CHART_LABEL_SEPARATOR, DEFAULT_FURNITURE_DATE_FORMAT, DEFAULT_SLIDE_NUMBER_FORMAT, DEFAULT_VARIABLE_DATE_FORMAT, DESIGN_HINT_KEYS, ENGINE_DEFAULT_CHART_TYPES, ENGINE_DEFAULT_COLOR_SCHEME, ENGINE_DEFAULT_FONT_SCHEME, ENGINE_DEFAULT_THEME, FONT_SIZE_GRID_PER_PX, FOOTNOTE_MAX_RATIO, LANGUAGES, LAYOUT_BODY_KINDS, MAX_COMPOSITION_DEPTH, MAX_NUMBERING_LEVELS, MAX_NUMBERING_VALUE, MAX_ROMAN_VALUE, METRIC_TREND_MIN_CONTRAST, METRIC_TREND_SHAPES, NUMBERING_STYLES, NUMBERING_SUFFIXES, OPFCompositionError, OPFVariableError, PATTERN_PRESETS, PATTERN_PRESET_ALIASES, PATTERN_TILE_SIZE, PICTURE_BULLET_SCALE, SOCIAL_PLATFORMS, SURFACE_ALT_MIN_CONTRAST, SURFACE_ALT_MIX, TIMELINE_OUTLINE_MIN_CONTRAST, TIMELINE_STATUSES, TIMELINE_TEXT_MIN_CONTRAST, VARIABLE_KINDS, annotationText, captionSettings, chartColorForFill, chartHighlightColors, chartHighlightMarks, chartLabelText, chartNumber, chartNumberFixesByCell, chartOptionSupport, chartOptionTarget, chartPaletteForFill, citationMarkerText, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeHighlightSlice, codeLineNumbers, codeLineRuns, codeSyntaxPalette, codeSyntaxPaletteForScheme, coerceVariableValue, collectCitations, colorContrast, composeSlide, datasetDiagnostics, defaultSlideBackground, fitImage, fitList, fitRichText, fitText, formatChartLabelNumber, formatChartLabelPercent, formatDataNumber, formatFurnitureDate, formatListNumber, formatSlideNumber, formatVariableNumber, fromExcelNumberFormat, hasContentVariables, imageBackground, imageShape, inlineChartData, inlineDatasets, inlineTableData, instantiateForValidation, intrinsicImageAspect, intrinsicImageSize, isDarkColor, isDatasetRef, isRecord, isTemplate, isXYChartType, layoutCaption, layoutCode, layoutContent, layoutFootnotes, layoutFurniture, layoutMetric, layoutQuote, layoutTable, layoutTimeline, layoutWatermark, listBuiltinVariables, listNumbers, listVariables, measureText, measureTextOutline, metricTrendColor, metricTrendMark, metricTrendPoints, normalizeHexColor, normalizeLanguageFamily, numberFormatError, numberingAtLevel, numberingFindings, numberingStyleDraws, paragraphDirection, paragraphDirectionAt, pathFor, patternBitmap, patternRuns, physicalAlignment, placeTextLines, promotedRegionKeys, readingRows, referencesSlide, resolveCanvasDimensions, resolveChartData, resolveChartOptions, resolveCodeLanguage, resolveColorRef, resolveColorRoles, resolveDesignHints, resolveDesignRecords, resolveFontFamilies, resolveFontScheme, resolveLogo, resolveNumbering, resolvePatternPreset, resolveScriptFonts, resolveSlideDirection, resolveSocialProfile, resolveTableData, resolveTextStyle, resolveVariables, scriptFontRole, sliceNumberedItems, slideCitations, snapFontSizeDown, snapFontSizeUp, suggestChartNumberFix, surfaceAltColor, tableCellDisplayValue, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, timelineMarkerShapes, timelineTextColor, toExcelNumberFormat, tokenizeCode, unusedDatasets, variableDeclarations, visitContentPayloads, visualReadingOrder, walkCitationRuns, wrapText };
5956
+ export { CAPTIONABLE_FIELDS, CAPTION_FONT_RATIO, CAPTION_MAX_RATIO, CHART_HIGHLIGHT_MUTED_MIN_CONTRAST, CHART_HIGHLIGHT_MUTED_MIX, CHART_SERIES_MIN_DIFFERENCE, CHART_SERIES_MIN_LIGHTNESS_STEP, CHART_TYPES, CITATION_MARKER_RAISE, CITATION_MARKER_SCALE, CODE_HIGHLIGHT_LANGUAGES, CODE_HIGHLIGHT_MAX_LENGTH, CODE_PANEL_BACKGROUND, CODE_PANEL_FOREGROUND, CODE_SYNTAX_MIN_CONTRAST, DARK_BACKGROUND_LUMINANCE, DEFAULT_CHART_LABEL_SEPARATOR, DEFAULT_FURNITURE_DATE_FORMAT, DEFAULT_VARIABLE_DATE_FORMAT, DESIGN_HINT_KEYS, ENGINE_DEFAULT_CHART_TYPES, ENGINE_DEFAULT_COLOR_SCHEME, ENGINE_DEFAULT_FONT_SCHEME, ENGINE_DEFAULT_THEME, FONT_SIZE_GRID_PER_PX, FOOTNOTE_MAX_RATIO, LANGUAGES, LAYOUT_BODY_KINDS, MAX_COMPOSITION_DEPTH, MAX_NUMBERING_LEVELS, MAX_NUMBERING_VALUE, MAX_PLACEHOLDER_GROUP_DEPTH, MAX_ROMAN_VALUE, METRIC_TREND_MIN_CONTRAST, METRIC_TREND_SHAPES, NUMBERING_STYLES, NUMBERING_SUFFIXES, OPFCompositionError, OPFVariableError, PATTERN_PRESETS, PATTERN_PRESET_ALIASES, PATTERN_TILE_SIZE, PICTURE_BULLET_SCALE, SLIDE_SCOPED_BUILTINS, SOCIAL_PLATFORMS, SURFACE_ALT_MIN_CONTRAST, SURFACE_ALT_MIX, TIMELINE_OUTLINE_MIN_CONTRAST, TIMELINE_STATUSES, TIMELINE_TEXT_MIN_CONTRAST, VARIABLE_KINDS, annotationText, captionSettings, chartColorForFill, chartHighlightColors, chartHighlightMarks, chartLabelText, chartNumber, chartNumberFixesByCell, chartOptionSupport, chartOptionTarget, chartPaletteForFill, chartPrimaryLayout, citationMarkerText, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeHighlightSlice, codeLineNumbers, codeLineRuns, codeSyntaxPalette, codeSyntaxPaletteForScheme, coerceVariableValue, collectCitations, colorContrast, composeSlide, datasetDiagnostics, defaultSlideBackground, fitImage, fitList, fitRichText, fitText, formatChartLabelNumber, formatChartLabelPercent, formatDataNumber, formatFurnitureDate, formatListNumber, formatVariableNumber, fromExcelNumberFormat, hasContentVariables, hasPlaceholderGroups, imageBackground, imageShape, inlineChartData, inlineDatasets, inlineTableData, instantiateForValidation, intrinsicImageAspect, intrinsicImageSize, isDarkColor, isDatasetRef, isPlaceholderGroup, isRecord, isTemplate, isXYChartType, layoutCaption, layoutCode, layoutContent, layoutFootnotes, layoutFurniture, layoutLeaves, layoutMetric, layoutQuote, layoutSlots, layoutStructure, layoutTable, layoutTimeline, layoutWatermark, listBuiltinVariables, listNumbers, listVariables, measureText, measureTextOutline, metricTrendColor, metricTrendMark, metricTrendPoints, normalizeHexColor, normalizeLanguageFamily, numberFormatError, numberingAtLevel, numberingFindings, numberingStyleDraws, paragraphDirection, paragraphDirectionAt, pathFor, patternBitmap, patternRuns, physicalAlignment, placeTextLines, promotedRegionKeys, readingRows, referencesSlide, resolveCanvasDimensions, resolveChartData, resolveChartOptions, resolveCodeLanguage, resolveColorRef, resolveColorRoles, resolveDesignHints, resolveDesignRecords, resolveFontFamilies, resolveFontScheme, resolveLogo, resolveNumbering, resolvePatternPreset, resolveScriptFonts, resolveSlideDirection, resolveSlideVariables, resolveSocialProfile, resolveTableData, resolveTextStyle, resolveVariables, scriptFontRole, sliceNumberedItems, slideCitations, slideTokenSpans, snapFontSizeDown, snapFontSizeUp, suggestChartNumberFix, surfaceAltColor, tableCellDisplayValue, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, timelineMarkerShapes, timelineTextColor, toExcelNumberFormat, tokenizeCode, unusedDatasets, usesSlideBuiltin, variableDeclarations, visitContentPayloads, visualReadingOrder, walkCitationRuns, wrapText };