@openpresentation/opf 0.10.0 → 0.11.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 (85) hide show
  1. package/dist/{catalogs-HCTav5Wv.d.ts → catalogs-atkOF4Vy.d.ts} +7 -7
  2. package/dist/catalogs.d.ts +2 -2
  3. package/dist/catalogs.js +1 -1
  4. package/dist/{chunk-TWMRZ43O.js → chunk-6FU2KICU.js} +365 -327
  5. package/dist/{chunk-GCB6LGJM.js → chunk-CHKJ4QHQ.js} +225 -73
  6. package/dist/{chunk-NPOKUFEX.js → chunk-EQHZ3ZO4.js} +3 -3
  7. package/dist/{chunk-BAWLYSUN.js → chunk-FTWNWFIV.js} +180 -6
  8. package/dist/{chunk-TK3QHM3U.js → chunk-FWA5XB45.js} +54 -21
  9. package/dist/{chunk-GQSK3T7X.js → chunk-GN52EE3P.js} +146 -16
  10. package/dist/{chunk-OZULJBSG.js → chunk-RS5GZ5RW.js} +14 -0
  11. package/dist/composition-Tv9Wm3Oh.d.ts +691 -0
  12. package/dist/composition.d.ts +1 -624
  13. package/dist/composition.js +1 -1
  14. package/dist/docs.js +59 -11
  15. package/dist/examples.d.ts +1 -1
  16. package/dist/index.d.ts +65 -4
  17. package/dist/index.js +205 -7
  18. package/dist/lint.d.ts +3 -3
  19. package/dist/lint.js +5 -5
  20. package/dist/pagination.d.ts +4 -2
  21. package/dist/pagination.js +5 -5
  22. package/dist/{presentation-BMTdX6O1.d.ts → presentation-Ct42Mitb.d.ts} +51 -8
  23. package/dist/repo-readme.js +1 -1
  24. package/dist/{schemas-BYe5y8-i.d.ts → schemas-CiVmUT2p.d.ts} +6 -0
  25. package/dist/schemas.d.ts +1 -1
  26. package/dist/schemas.js +1 -1
  27. package/dist/spec/catalogs/layouts/extract/canonical.json +45 -21
  28. package/dist/spec/catalogs/layouts/index.json +10 -0
  29. package/dist/spec/catalogs/layouts/number-1x.json +1 -1
  30. package/dist/spec/catalogs/layouts/number-2x.json +2 -2
  31. package/dist/spec/catalogs/layouts/number-3x.json +3 -3
  32. package/dist/spec/catalogs/layouts/number-4x.json +4 -4
  33. package/dist/spec/catalogs/layouts/number-5x.json +5 -5
  34. package/dist/spec/catalogs/layouts/number-6x.json +6 -6
  35. package/dist/spec/catalogs/layouts/quote-1x.json +13 -0
  36. package/dist/spec/catalogs/layouts/timeline-1x.json +13 -0
  37. package/dist/spec/catalogs/narratives/board-meeting.json +21 -21
  38. package/dist/spec/catalogs/narratives/business-narrative.json +8 -8
  39. package/dist/spec/catalogs/narratives/business-review.json +12 -12
  40. package/dist/spec/catalogs/narratives/capacity-planning.json +8 -8
  41. package/dist/spec/catalogs/narratives/challenge-resolution.json +7 -7
  42. package/dist/spec/catalogs/narratives/classic-story.json +7 -7
  43. package/dist/spec/catalogs/narratives/company-intro.json +9 -9
  44. package/dist/spec/catalogs/narratives/conference-talk.json +1 -1
  45. package/dist/spec/catalogs/narratives/early-startup-pitch.json +12 -12
  46. package/dist/spec/catalogs/narratives/educate.json +9 -9
  47. package/dist/spec/catalogs/narratives/employee-review.json +8 -8
  48. package/dist/spec/catalogs/narratives/failure-analysis.json +10 -10
  49. package/dist/spec/catalogs/narratives/focus.json +12 -12
  50. package/dist/spec/catalogs/narratives/golden-circle.json +1 -1
  51. package/dist/spec/catalogs/narratives/innovation.json +9 -9
  52. package/dist/spec/catalogs/narratives/justice.json +11 -11
  53. package/dist/spec/catalogs/narratives/marketing-strategy.json +11 -11
  54. package/dist/spec/catalogs/narratives/performance-improvement-plan.json +8 -8
  55. package/dist/spec/catalogs/narratives/performance-review.json +11 -11
  56. package/dist/spec/catalogs/narratives/persuade.json +7 -7
  57. package/dist/spec/catalogs/narratives/persuasive-sales.json +8 -8
  58. package/dist/spec/catalogs/narratives/pitch-deck.json +3 -3
  59. package/dist/spec/catalogs/narratives/problem-solution.json +2 -2
  60. package/dist/spec/catalogs/narratives/product-launch.json +10 -10
  61. package/dist/spec/catalogs/narratives/project-proposal.json +10 -10
  62. package/dist/spec/catalogs/narratives/qbr.json +3 -3
  63. package/dist/spec/catalogs/narratives/rags-to-riches.json +7 -7
  64. package/dist/spec/catalogs/narratives/reveal.json +8 -8
  65. package/dist/spec/catalogs/narratives/scqa.json +1 -1
  66. package/dist/spec/catalogs/narratives/status-update.json +6 -6
  67. package/dist/spec/catalogs/narratives/strategic-advisory.json +7 -7
  68. package/dist/spec/catalogs/narratives/strategic-narrative.json +1 -1
  69. package/dist/spec/catalogs/narratives/survey-analysis.json +10 -10
  70. package/dist/spec/catalogs/narratives/survival-story.json +10 -10
  71. package/dist/spec/catalogs/narratives/transformation-arc.json +1 -1
  72. package/dist/spec/catalogs/narratives/trend-analysis.json +7 -7
  73. package/dist/spec/catalogs/narratives/underdog-victory.json +8 -8
  74. package/dist/spec/catalogs/narratives/venture-pitch.json +12 -12
  75. package/dist/spec/catalogs/narratives/weekly-progress.json +10 -10
  76. package/dist/spec/openapi.yaml +5 -0
  77. package/dist/spec/schemas/layout.schema.json +12 -3
  78. package/dist/spec/schemas/narrative.schema.json +1 -1
  79. package/dist/spec/schemas/opf.schema.json +83 -12
  80. package/dist/spec-files.d.ts +1 -1
  81. package/dist/spec-files.js +1 -1
  82. package/dist/types.d.ts +3 -3
  83. package/dist/validator.d.ts +7 -4
  84. package/dist/validator.js +4 -4
  85. package/package.json +1 -1
@@ -1,9 +1,89 @@
1
- import { MAX_COMPOSITION_DEPTH, tableGrid } from './chunk-BAWLYSUN.js';
2
- import { schemas, schemaEntries } from './chunk-GQSK3T7X.js';
3
- import { catalogSchemaNames } from './chunk-TWMRZ43O.js';
1
+ import { MAX_COMPOSITION_DEPTH, tableGrid } from './chunk-FTWNWFIV.js';
2
+ import { schemas, schemaEntries } from './chunk-GN52EE3P.js';
3
+ import { catalogSchemaNames } from './chunk-6FU2KICU.js';
4
4
  import Ajv2020 from 'ajv/dist/2020.js';
5
5
  import addFormats from 'ajv-formats';
6
6
 
7
+ // src/content-walk.ts
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ function pathFor(parentPath, key) {
12
+ return parentPath === "/" ? `/${key}` : `${parentPath}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`;
13
+ }
14
+ var bareIdPattern = /^[a-z0-9][a-z0-9-]*$/;
15
+ var promotedRegionKeys = [
16
+ "left",
17
+ "center",
18
+ "right",
19
+ "left+center",
20
+ "center+right",
21
+ "left+center+right",
22
+ "top",
23
+ "middle",
24
+ "bottom",
25
+ "top+middle",
26
+ "middle+bottom",
27
+ "top+middle+bottom",
28
+ "top:left",
29
+ "top:center",
30
+ "top:right",
31
+ "top:left+center",
32
+ "top:center+right",
33
+ "top:left+center+right",
34
+ "middle:left",
35
+ "middle:center",
36
+ "middle:right",
37
+ "middle:left+center",
38
+ "middle:center+right",
39
+ "middle:left+center+right",
40
+ "bottom:left",
41
+ "bottom:center",
42
+ "bottom:right",
43
+ "bottom:left+center",
44
+ "bottom:center+right",
45
+ "bottom:left+center+right",
46
+ "top+middle:left",
47
+ "top+middle:center",
48
+ "top+middle:right",
49
+ "top+middle:left+center",
50
+ "top+middle:center+right",
51
+ "top+middle:left+center+right",
52
+ "middle+bottom:left",
53
+ "middle+bottom:center",
54
+ "middle+bottom:right",
55
+ "middle+bottom:left+center",
56
+ "middle+bottom:center+right",
57
+ "middle+bottom:left+center+right",
58
+ "top+middle+bottom:left",
59
+ "top+middle+bottom:center",
60
+ "top+middle+bottom:right",
61
+ "top+middle+bottom:left+center",
62
+ "top+middle+bottom:center+right",
63
+ "top+middle+bottom:left+center+right"
64
+ ];
65
+ function visitContentPayloads(slide, slidePath, visit) {
66
+ const walk = (payload, path, depth, ancestors) => {
67
+ visit(payload, path);
68
+ if (!Array.isArray(payload.blocks) || depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(payload)) return;
69
+ const nested = [...ancestors, payload];
70
+ const blocksPath = pathFor(path, "blocks");
71
+ payload.blocks.forEach((block, index) => {
72
+ if (isRecord(block)) walk(block, `${blocksPath}/${index}`, depth + 1, nested);
73
+ });
74
+ };
75
+ if (Array.isArray(slide.blocks)) {
76
+ const blocksPath = pathFor(slidePath, "blocks");
77
+ slide.blocks.forEach((block, index) => {
78
+ if (isRecord(block)) walk(block, `${blocksPath}/${index}`, 0, []);
79
+ });
80
+ }
81
+ for (const key of promotedRegionKeys) {
82
+ const region = slide[key];
83
+ if (isRecord(region)) walk(region, pathFor(slidePath, key), 0, []);
84
+ }
85
+ }
86
+
7
87
  // src/generated/catalog-ids.ts
8
88
  var catalogIds = {
9
89
  audiences: [
@@ -67,10 +147,12 @@ var catalogIds = {
67
147
  "number-4x",
68
148
  "number-5x",
69
149
  "number-6x",
150
+ "quote-1x",
70
151
  "table-1x",
71
152
  "text-1x",
72
153
  "text-2x",
73
154
  "text-3x",
155
+ "timeline-1x",
74
156
  "title",
75
157
  "title-subtitle"
76
158
  ],
@@ -464,56 +546,6 @@ var OPFValidationError = class extends Error {
464
546
  var schemaNameByCatalogKind = catalogSchemaNames;
465
547
  var ajv;
466
548
  var dynamicSchemaCache = /* @__PURE__ */ new WeakMap();
467
- var promotedRegionKeys = [
468
- "left",
469
- "center",
470
- "right",
471
- "left+center",
472
- "center+right",
473
- "left+center+right",
474
- "top",
475
- "middle",
476
- "bottom",
477
- "top+middle",
478
- "middle+bottom",
479
- "top+middle+bottom",
480
- "top:left",
481
- "top:center",
482
- "top:right",
483
- "top:left+center",
484
- "top:center+right",
485
- "top:left+center+right",
486
- "middle:left",
487
- "middle:center",
488
- "middle:right",
489
- "middle:left+center",
490
- "middle:center+right",
491
- "middle:left+center+right",
492
- "bottom:left",
493
- "bottom:center",
494
- "bottom:right",
495
- "bottom:left+center",
496
- "bottom:center+right",
497
- "bottom:left+center+right",
498
- "top+middle:left",
499
- "top+middle:center",
500
- "top+middle:right",
501
- "top+middle:left+center",
502
- "top+middle:center+right",
503
- "top+middle:left+center+right",
504
- "middle+bottom:left",
505
- "middle+bottom:center",
506
- "middle+bottom:right",
507
- "middle+bottom:left+center",
508
- "middle+bottom:center+right",
509
- "middle+bottom:left+center+right",
510
- "top+middle+bottom:left",
511
- "top+middle+bottom:center",
512
- "top+middle+bottom:right",
513
- "top+middle+bottom:left+center",
514
- "top+middle+bottom:center+right",
515
- "top+middle+bottom:left+center+right"
516
- ];
517
549
  var promotedRegionKeySet = new Set(promotedRegionKeys);
518
550
  var rootPayloadFields = [
519
551
  "type",
@@ -653,18 +685,12 @@ function semanticIssue(path, message, params = {}) {
653
685
  params
654
686
  };
655
687
  }
656
- function isRecord(value) {
657
- return typeof value === "object" && value !== null && !Array.isArray(value);
658
- }
659
688
  function hasOwn(value, key) {
660
689
  return Object.hasOwn(value, key);
661
690
  }
662
691
  function isEnUkTag(value) {
663
692
  return typeof value === "string" && value.toLowerCase() === "en-uk";
664
693
  }
665
- function pathFor(parentPath, key) {
666
- return parentPath === "/" ? `/${key}` : `${parentPath}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`;
667
- }
668
694
  function presentFields(value, fields) {
669
695
  return fields.filter((field) => hasOwn(value, field));
670
696
  }
@@ -837,27 +863,32 @@ function validatePresentationSemantics(value) {
837
863
  replacement: "en-GB"
838
864
  }));
839
865
  }
840
- const slideIds = /* @__PURE__ */ new Set();
866
+ const documentIds = /* @__PURE__ */ new Map();
841
867
  value.slides.forEach((slide, index) => {
842
868
  if (isRecord(slide)) {
869
+ const slidePath = `/slides/${index}`;
843
870
  if (typeof slide.id === "string") {
844
- if (slideIds.has(slide.id)) issues.push(semanticIssue(`/slides/${index}/id`, "slide ids must be unique within a presentation", { id: slide.id }));
845
- slideIds.add(slide.id);
871
+ const seen = documentIds.get(slide.id);
872
+ if (seen === "slide") issues.push(semanticIssue(`${slidePath}/id`, "slide ids must be unique within a presentation", { id: slide.id }));
873
+ else if (seen === "payload") issues.push(semanticIssue(`${slidePath}/id`, "slide id duplicates a content payload id; ids must be unique among slide and payload ids within a presentation", { id: slide.id }));
874
+ else documentIds.set(slide.id, "slide");
846
875
  }
847
- issues.push(...validateSlideRegions(slide, `/slides/${index}`));
848
- const visitTables = (node, path) => {
849
- if (isRecord(node.table)) for (const issue of tableGrid(node.table).issues) issues.push(semanticIssue(pathFor(path, "table") + issue.path.slice(5).replaceAll(".", "/"), issue.message));
850
- if (Array.isArray(node.blocks)) node.blocks.forEach((block, i) => {
851
- if (isRecord(block)) visitTables(block, `${path}/blocks/${i}`);
852
- });
853
- for (const key of promotedRegionKeys) if (isRecord(node[key])) visitTables(node[key], pathFor(path, key));
876
+ visitContentPayloads(slide, slidePath, (payload, path) => {
877
+ if (typeof payload.id === "string") {
878
+ if (documentIds.has(payload.id)) issues.push(semanticIssue(`${path}/id`, "content payload ids must be unique among slide and payload ids within a presentation", { id: payload.id }));
879
+ else documentIds.set(payload.id, "payload");
880
+ }
881
+ });
882
+ issues.push(...validateSlideRegions(slide, slidePath));
883
+ const payloadTableIssues = (payload, path) => {
884
+ if (isRecord(payload.table)) for (const issue of tableGrid(payload.table).issues) issues.push(semanticIssue(pathFor(path, "table") + issue.path.slice(5).replaceAll(".", "/"), issue.message));
854
885
  };
855
- visitTables(slide, `/slides/${index}`);
886
+ payloadTableIssues(slide, slidePath);
887
+ visitContentPayloads(slide, slidePath, payloadTableIssues);
856
888
  }
857
889
  });
858
890
  return issues;
859
891
  }
860
- var bareIdPattern = /^[a-z0-9][a-z0-9-]*$/;
861
892
  function inlineCatalogEntry(context, kind) {
862
893
  const catalogsField = context.document.catalogs;
863
894
  if (!isRecord(catalogsField)) {
@@ -876,7 +907,7 @@ function unknownIdWarning(kind, value, path, context) {
876
907
  if (Array.isArray(entry.records) && entry.records.some((record) => isRecord(record) && record.id === value)) {
877
908
  return void 0;
878
909
  }
879
- if (typeof entry.source === "string") {
910
+ if (typeof entry.source === "string" || Array.isArray(entry.source) && entry.source.length > 0) {
880
911
  return void 0;
881
912
  }
882
913
  }
@@ -930,6 +961,126 @@ function chartTypeWarnings(payload, path, context) {
930
961
  }
931
962
  return issues;
932
963
  }
964
+ var cellBorderEdges = ["top", "right", "bottom", "left"];
965
+ var variableIdPattern = /^[a-z][a-z0-9-]*$/;
966
+ var hexColorPattern = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
967
+ var schemeColorNames = /* @__PURE__ */ new Set([
968
+ "accent1",
969
+ "accent2",
970
+ "accent3",
971
+ "accent4",
972
+ "accent5",
973
+ "accent6",
974
+ "dark1",
975
+ "dark2",
976
+ "light1",
977
+ "light2",
978
+ "hyperlink",
979
+ "followedHyperlink",
980
+ "primary",
981
+ "secondary",
982
+ "accent",
983
+ "background",
984
+ "surface",
985
+ "text",
986
+ "textSecondary"
987
+ ]);
988
+ function variableReferenceWarnings(value) {
989
+ if (!Array.isArray(value.slides)) {
990
+ return [];
991
+ }
992
+ const variables = isRecord(value.variables) ? value.variables : {};
993
+ const issues = [];
994
+ const colorRef = (entry, path) => {
995
+ if (typeof entry !== "string" || !entry.startsWith("var:")) return;
996
+ const id = entry.slice("var:".length);
997
+ if (!variableIdPattern.test(id) || hasOwn(variables, id)) return;
998
+ issues.push(semanticIssue(path, `unknown variable '${id}'; declare it in the top-level variables map`, { id }));
999
+ };
1000
+ const runColor = (entry, path) => {
1001
+ if (typeof entry !== "string") return;
1002
+ if (hexColorPattern.test(entry) || schemeColorNames.has(entry)) return;
1003
+ if (entry.startsWith("var:") && variableIdPattern.test(entry.slice("var:".length))) {
1004
+ colorRef(entry, path);
1005
+ return;
1006
+ }
1007
+ issues.push(semanticIssue(
1008
+ path,
1009
+ `run color '${entry}' is not a hex color, a color-scheme name, or a 'var:' reference; renderers fall back to the theme color`,
1010
+ { color: entry }
1011
+ ));
1012
+ };
1013
+ const richText = (entry, path) => {
1014
+ if (!Array.isArray(entry)) return;
1015
+ entry.forEach((run, index) => {
1016
+ if (isRecord(run)) runColor(run.color, `${path}/${index}/color`);
1017
+ });
1018
+ };
1019
+ const cellStyle = (style, path) => {
1020
+ if (!isRecord(style)) return;
1021
+ colorRef(style.fill, pathFor(path, "fill"));
1022
+ colorRef(style.color, pathFor(path, "color"));
1023
+ if (!isRecord(style.borders)) return;
1024
+ const bordersPath = pathFor(path, "borders");
1025
+ for (const edge of cellBorderEdges) {
1026
+ const border = style.borders[edge];
1027
+ if (isRecord(border)) colorRef(border.color, `${pathFor(bordersPath, edge)}/color`);
1028
+ }
1029
+ };
1030
+ const tableCell = (cell, path) => {
1031
+ if (Array.isArray(cell)) {
1032
+ richText(cell, path);
1033
+ return;
1034
+ }
1035
+ if (!isRecord(cell)) return;
1036
+ richText(cell.value, pathFor(path, "value"));
1037
+ cellStyle(cell.style, pathFor(path, "style"));
1038
+ };
1039
+ const table = (node, path) => {
1040
+ if (!isRecord(node)) return;
1041
+ if (Array.isArray(node.columns)) {
1042
+ const columnsPath = pathFor(path, "columns");
1043
+ node.columns.forEach((column, index) => {
1044
+ tableCell(column, `${columnsPath}/${index}`);
1045
+ });
1046
+ }
1047
+ if (Array.isArray(node.rows)) {
1048
+ const rowsPath = pathFor(path, "rows");
1049
+ node.rows.forEach((row, rowIndex) => {
1050
+ if (!Array.isArray(row)) return;
1051
+ row.forEach((cell, cellIndex) => {
1052
+ tableCell(cell, `${rowsPath}/${rowIndex}/${cellIndex}`);
1053
+ });
1054
+ });
1055
+ }
1056
+ };
1057
+ const textEntries = (entries, path) => {
1058
+ if (!Array.isArray(entries)) return;
1059
+ entries.forEach((entry, index) => {
1060
+ const entryPath = `${path}/${index}`;
1061
+ if (Array.isArray(entry)) {
1062
+ richText(entry, entryPath);
1063
+ return;
1064
+ }
1065
+ if (!isRecord(entry)) return;
1066
+ richText(entry.text, pathFor(entryPath, "text"));
1067
+ richText(entry.description, pathFor(entryPath, "description"));
1068
+ });
1069
+ };
1070
+ const payload = (node, path) => {
1071
+ richText(node.text, pathFor(path, "text"));
1072
+ textEntries(node.items, pathFor(path, "items"));
1073
+ textEntries(node.bullets, pathFor(path, "bullets"));
1074
+ table(node.table, pathFor(path, "table"));
1075
+ };
1076
+ value.slides.forEach((slide, index) => {
1077
+ if (!isRecord(slide)) return;
1078
+ const slidePath = `/slides/${index}`;
1079
+ payload(slide, slidePath);
1080
+ visitContentPayloads(slide, slidePath, payload);
1081
+ });
1082
+ return issues;
1083
+ }
933
1084
  function presentationReferenceWarnings(value) {
934
1085
  if (!isRecord(value) || !Array.isArray(value.slides)) {
935
1086
  return [];
@@ -940,6 +1091,7 @@ function presentationReferenceWarnings(value) {
940
1091
  pushIfDefined(issues, unknownIdWarning("narratives", value.narrative, "/narrative", context));
941
1092
  }
942
1093
  issues.push(...designReferenceWarnings(value.design, "/design", context));
1094
+ issues.push(...variableReferenceWarnings(value));
943
1095
  value.slides.forEach((slide, index) => {
944
1096
  if (!isRecord(slide)) {
945
1097
  return;
@@ -1055,4 +1207,4 @@ function assertValidCatalogRecord(kind, value) {
1055
1207
  assertValid(value, kind);
1056
1208
  }
1057
1209
 
1058
- export { OPFValidationError, assertValid, assertValidCatalogRecord, assertValidPresentation, validate, validateCatalogRecord, validatePresentation, validationDefinition };
1210
+ export { OPFValidationError, assertValid, assertValidCatalogRecord, assertValidPresentation, bareIdPattern, isRecord, promotedRegionKeys, validate, validateCatalogRecord, validatePresentation, validationDefinition, visitContentPayloads };
@@ -1,6 +1,6 @@
1
- import { validatePresentation, validationDefinition, validateCatalogRecord } from './chunk-GCB6LGJM.js';
2
- import { schemas, schemaEntries } from './chunk-GQSK3T7X.js';
3
- import { catalogKinds, catalogEntries, catalogSchemaNames } from './chunk-TWMRZ43O.js';
1
+ import { validatePresentation, validationDefinition, validateCatalogRecord } from './chunk-CHKJ4QHQ.js';
2
+ import { schemas, schemaEntries } from './chunk-GN52EE3P.js';
3
+ import { catalogKinds, catalogEntries, catalogSchemaNames } from './chunk-6FU2KICU.js';
4
4
  import { parseTree, printParseErrorCode, getNodePath } from 'jsonc-parser';
5
5
 
6
6
  var own = (value, key) => Object.hasOwn(value, key);
@@ -36,6 +36,77 @@ function tableRowBoundaries(value) {
36
36
  }
37
37
 
38
38
  // src/color.ts
39
+ var schemeColorSlots = /* @__PURE__ */ new Set([
40
+ "accent1",
41
+ "accent2",
42
+ "accent3",
43
+ "accent4",
44
+ "accent5",
45
+ "accent6",
46
+ "dark1",
47
+ "dark2",
48
+ "light1",
49
+ "light2",
50
+ "hyperlink",
51
+ "followedHyperlink"
52
+ ]);
53
+ var schemeColorRoles = /* @__PURE__ */ new Set([
54
+ "primary",
55
+ "secondary",
56
+ "accent",
57
+ "background",
58
+ "surface",
59
+ "text",
60
+ "textSecondary"
61
+ ]);
62
+ var defaultRoleSlots = {
63
+ primary: "accent1",
64
+ secondary: "accent2",
65
+ accent: "accent3",
66
+ background: "light1",
67
+ surface: "light2",
68
+ text: "dark1",
69
+ textSecondary: "dark2"
70
+ };
71
+ function normalizeHexColor(value) {
72
+ if (typeof value !== "string") return void 0;
73
+ let hex = value.trim();
74
+ if (/^#[0-9a-f]{3}$/i.test(hex)) hex = `#${[...hex.slice(1)].map((char) => char + char).join("")}`;
75
+ if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(hex)) return hex.length === 9 ? `#${hex.slice(1, 7).toUpperCase()}` : hex.toUpperCase();
76
+ return void 0;
77
+ }
78
+ function schemeSlot(colorScheme, slot, fallback) {
79
+ return normalizeHexColor(colorScheme[slot]) ?? fallback;
80
+ }
81
+ function resolveVariable(variables, id, fallback) {
82
+ const entry = variables[id];
83
+ if (typeof entry === "string") return normalizeHexColor(entry) ?? fallback;
84
+ if (entry && typeof entry === "object" && entry.type === "color") {
85
+ return normalizeHexColor(entry.value) ?? fallback;
86
+ }
87
+ return fallback;
88
+ }
89
+ function resolveColorRef(reference, options) {
90
+ const { colorScheme, roles, variables = {}, fallback } = options;
91
+ const literal = normalizeHexColor(reference);
92
+ if (literal) return literal;
93
+ if (reference.startsWith("var:")) {
94
+ const id = reference.slice("var:".length);
95
+ if (/^[a-z][a-z0-9-]*$/.test(id)) return resolveVariable(variables, id, fallback);
96
+ return fallback;
97
+ }
98
+ if (schemeColorRoles.has(reference)) {
99
+ const roleKey = reference;
100
+ const fromRoles = roles?.[roleKey];
101
+ if (fromRoles) return normalizeHexColor(fromRoles) ?? fallback;
102
+ const fromScheme = normalizeHexColor(colorScheme[reference]);
103
+ if (fromScheme) return fromScheme;
104
+ const slot = defaultRoleSlots[reference];
105
+ return slot ? schemeSlot(colorScheme, slot, fallback) : fallback;
106
+ }
107
+ if (schemeColorSlots.has(reference)) return schemeSlot(colorScheme, reference, fallback);
108
+ return fallback;
109
+ }
39
110
  function luminance(value) {
40
111
  if (typeof value !== "string") return void 0;
41
112
  let hex = value.trim();
@@ -146,8 +217,105 @@ var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
146
217
  var rows = ["top", "middle", "bottom"];
147
218
  var columns = ["left", "center", "right"];
148
219
  var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
149
- var kind = (field) => field === "items" || field === "bullets" ? "list" : field;
220
+ var kind = (field) => field === "items" ? "list" : field === "bullets" ? "text" : field;
150
221
  var round = (value) => Math.round(value * 1e6) / 1e6 || value;
222
+ function layoutFurniture(input, options = {}) {
223
+ const slide = record2(input), width = options.width ?? 1280, height = options.height ?? 720, scale = Math.min(width, height) / 720;
224
+ const settings = { ...record2(record2(options.layout).composition), ...record2(slide.composition) };
225
+ assertComposition(settings);
226
+ const minimum = (settings.minFontSize ?? 16) * scale, size = Math.max(13 * scale, minimum), padding = (options.textRasterPadding ?? 2) * scale;
227
+ if (![width, height, scale, minimum, padding].every(Number.isFinite) || width <= 0 || height <= 0 || minimum <= 0 || padding < 0) throw new RangeError("Furniture requires finite positive dimensions and nonnegative raster padding.");
228
+ const number = options.slideNumber ?? (options.slideIndex ?? 0) + 1;
229
+ if (!Number.isSafeInteger(number) || number < 1) throw new RangeError("Displayed slide number must be a positive safe integer.");
230
+ const outlines = options.textMeasurement?.outlineBounds !== void 0, parts = [], diagnostics = [];
231
+ const sourceRoot = `slides.${options.slideIndex ?? 0}`, organizations = Array.isArray(options.presentation?.organization) ? options.presentation.organization : [options.presentation?.organization];
232
+ const primaryIndex = organizations.findIndex((item) => record2(item).role === "primary"), organizationIndex = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
233
+ const organization = record2(organizations[organizationIndex]), organizationPath = Array.isArray(options.presentation?.organization) ? `organization.${organizationIndex}.name` : "organization.name";
234
+ const fontFamily = options.fonts?.body ?? "sans-serif";
235
+ let headerBottom = 0, footerTop = height, configured = false;
236
+ const error = (path, message, code = "text-overflow") => diagnostics.push({ code, path, message });
237
+ for (const kind2 of ["header", "footer"]) {
238
+ const local = record2(slide.design)[kind2] !== void 0, value = local ? record2(slide.design)[kind2] : options.presentation?.design?.[kind2];
239
+ if (value === void 0 || value === false) continue;
240
+ const root = local ? `${sourceRoot}.design.${kind2}` : `design.${kind2}`;
241
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Header/footer content must be an object or false.");
242
+ configured = true;
243
+ const zones = [];
244
+ for (const [index, zone] of ["left", "center", "right"].entries()) {
245
+ const content = record2(record2(value)[zone]), path = `${root}.${zone}`, x = width * (0.07 + index * 0.3), zoneWidth = width * 0.26, zoneParts = [];
246
+ let y = 0;
247
+ const add = (field, text, generated = false, sourcePath) => {
248
+ if (text === void 0) return;
249
+ if (typeof text !== "string") throw new TypeError(`Furniture field ${path}.${field} requires string content.`);
250
+ const partPath = `${path}.${field}`, style = resolveTextStyle({ fontFamily, fontWeight: 400, italic: false, path: partPath }, options.textMeasurement);
251
+ const fit = fitText(text, { width: Math.max(Number.MIN_VALUE, zoneWidth - (outlines ? 2 * padding : 0)), height: Number.MAX_VALUE }, size, size, textWidthMeasurer(style, options.textMeasurement));
252
+ const ink = fit.sourceLines.map((line, index2) => {
253
+ let outline = null;
254
+ if (outlines) {
255
+ for (const segment of line.segments) if (segment.kind === "text") {
256
+ const bounds = measureTextOutline(text.slice(segment.start, segment.end), size, style, options.textMeasurement);
257
+ if (bounds) {
258
+ const next = { ...bounds, x: bounds.x + segment.x };
259
+ if (!outline) outline = next;
260
+ else {
261
+ const right = Math.max(outline.x + outline.width, next.x + next.width), bottom = Math.max(outline.y + outline.height, next.y + next.height);
262
+ outline.x = Math.min(outline.x, next.x);
263
+ outline.y = Math.min(outline.y, next.y);
264
+ outline.width = right - outline.x;
265
+ outline.height = bottom - outline.y;
266
+ }
267
+ }
268
+ }
269
+ }
270
+ return { width: line.width, y: index2 * fit.lineHeight, baseline: size + index2 * fit.lineHeight, height: fit.lineHeight, outline };
271
+ });
272
+ const natural = outlines ? placeTextLines(ink, { x, y, width: zoneWidth, height: Number.MAX_VALUE }, zone, padding) : void 0;
273
+ const partHeight = natural?.height ?? fit.sourceLines.length * fit.lineHeight;
274
+ if (!Number.isFinite(partHeight)) throw new RangeError("Furniture exceeds finite layout coordinates.");
275
+ const box = { x, y, width: zoneWidth, height: Math.max(scale, partHeight) }, placement = outlines ? placeTextLines(ink, box, zone, padding) : void 0;
276
+ const accepted = { ...fit, ...placement ? { placement } : {}, overflow: fit.overflow || !!placement?.overflow };
277
+ zoneParts.push({ type: "text", kind: kind2, zone, field, path: partPath, sourcePath: sourcePath ?? (!generated ? partPath : void 0), generated, text, style, requestedFontSize: 13 * scale, minFontSize: minimum, box, alignment: zone, fit: accepted });
278
+ if (accepted.overflow) error(partPath, "Repeated text exceeds its zone at the selected readability floor; change the furniture or slide design.");
279
+ y += box.height;
280
+ };
281
+ if (content.image !== void 0) {
282
+ const box = { x, y, width: zoneWidth, height: Math.max(32 * scale, Math.min(height * 0.05, 72 * scale)) };
283
+ zoneParts.push({ type: "image", kind: kind2, zone, field: "image", path: `${path}.image`, sourcePath: `${path}.image`, generated: false, image: content.image, box, alignment: zone });
284
+ y += box.height;
285
+ }
286
+ add("text", content.text);
287
+ if (content.organization === true) {
288
+ if (typeof organization.name === "string") add("organization", organization.name, true, organizationPath);
289
+ else error(`${path}.organization`, "Generated organization name needs a named organization in the presentation.", "unresolved-content");
290
+ }
291
+ if (content.section === true) {
292
+ if (typeof slide.section === "string") add("section", slide.section, true, `${sourceRoot}.section`);
293
+ else error(`${path}.section`, "Generated section needs a literal slide section.", "unresolved-content");
294
+ }
295
+ if (content.slideNumber === true) add("slideNumber", String(number), true);
296
+ if (content.date === true) error(`${path}.date`, "Use a literal date string for deterministic header/footer content.", "unresolved-content");
297
+ else if (typeof content.date === "string") add("date", content.date);
298
+ zones.push(zoneParts);
299
+ }
300
+ const tallest = Math.max(0, ...zones.map((zone) => zone.reduce((sum, part) => sum + part.box.height, 0)));
301
+ if (!tallest) continue;
302
+ const top = kind2 === "header" ? height * 0.025 : Math.max(height * 0.025, height - height * 0.04 - tallest);
303
+ if (kind2 === "header") headerBottom = top + tallest;
304
+ else footerTop = top;
305
+ for (const zone of zones) for (const part of zone) {
306
+ part.box.y += top;
307
+ if (part.type === "text" && part.fit.placement) for (const line of part.fit.placement.lines) {
308
+ line.y += top;
309
+ line.baseline += top;
310
+ if (line.outline) line.outline.y += top;
311
+ }
312
+ if (part.box.y + part.box.height > height - height * 0.04 + 0.01) error(part.path, "Repeated header/footer content extends beyond the usable slide height. Change its content or design before pagination.");
313
+ parts.push(part);
314
+ }
315
+ }
316
+ if (headerBottom > footerTop + 0.01) error(parts.find((part) => part.kind === "footer")?.path ?? sourceRoot, "Header and footer content overlap; repeated content cannot be repaired by splitting body content.");
317
+ return { algorithm: "furniture-flow-v2", configured, textMeasurement: options.textMeasurement ? "provided" : "estimated", textOutlines: outlines ? "provided" : "unavailable", parts, headerBottom, footerTop, diagnostics, overflow: diagnostics.length > 0 };
318
+ }
151
319
  var textSegments = new Intl.Segmenter("und", { granularity: "grapheme" });
152
320
  function measureText(text, fontSize) {
153
321
  let units = 0;
@@ -1078,7 +1246,11 @@ function composeSlide(input, options = {}) {
1078
1246
  const fitContent = (field, value, text, box, size, minimum, path2) => field === "text" ? fitPlacedText(field, value, text, box, size, minimum, path2) : field === "items" || field === "bullets" ? fitList(value, box, size, minimum, { style: styleFor(field, path2), textMeasurement: options.textMeasurement }) : fitText(text, box, size, minimum, widthFor(field, path2));
1079
1247
  const path = `slides.${options.slideIndex ?? 0}`;
1080
1248
  const items = [], diagnostics = [];
1081
- let y = padding;
1249
+ const measuredFurniture = layoutFurniture(slide, options);
1250
+ const furniture = measuredFurniture.configured || measuredFurniture.diagnostics.length ? measuredFurniture : void 0;
1251
+ if (furniture) diagnostics.push(...furniture.diagnostics);
1252
+ const bodyBottom = Math.min(height - padding, furniture && furniture.footerTop < height ? furniture.footerTop - gap * 0.5 : height - padding);
1253
+ let y = Math.max(padding, furniture?.headerBottom ? furniture.headerBottom + gap * 0.5 : padding);
1082
1254
  for (const field of ["tag", "title", "subtitle"]) {
1083
1255
  if (!slide[field]) continue;
1084
1256
  const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale;
@@ -1086,11 +1258,12 @@ function composeSlide(input, options = {}) {
1086
1258
  const box = { x: padding, y, width: width - padding * 2, height: maxHeight };
1087
1259
  const text = fitPlacedText(field, slide[field], String(slide[field]), box, requested, minSize, `${path}.${field}`);
1088
1260
  box.height = Math.min(maxHeight, Math.max(text.lines.length * text.lineHeight, text.placement?.height ?? 0));
1261
+ if (furniture && box.y + box.height > bodyBottom + 0.01) diagnostics.push({ code: "text-overflow", path: `${path}.${field}`, message: "Repeated furniture leaves too little room for this heading. Change the header/footer or slide design." });
1089
1262
  items.push({ path: `${path}.${field}`, field, type: "text", value: slide[field], payload: { text: slide[field] }, box, text, textStyle: styleFor(field, `${path}.${field}`), composition });
1090
1263
  y += box.height + gap * 0.5;
1091
1264
  }
1092
1265
  if (items.length) y += gap * 0.5;
1093
- const contentBox = { x: padding, y, width: width - padding * 2, height: Math.max(scale, height - padding - y) };
1266
+ const contentBox = { x: padding, y, width: width - padding * 2, height: Math.max(scale, bodyBottom - y) };
1094
1267
  const collect = (host, basePath, depth = 0, ancestors = []) => {
1095
1268
  if (Array.isArray(host.blocks)) {
1096
1269
  if (depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(host)) throw new RangeError(`Content groups must be acyclic and nest at most ${MAX_COMPOSITION_DEPTH} levels.`);
@@ -1285,6 +1458,7 @@ function composeSlide(input, options = {}) {
1285
1458
  for (const group of groups) for (const box of [group.box, group.contentBox]) for (const key of ["x", "y", "width", "height"]) box[key] = round(box[key]);
1286
1459
  const strictPaths = new Set(items.filter((item) => item.composition.overflow === "error").map((item) => item.path));
1287
1460
  const failures = diagnostics.filter((diagnostic) => {
1461
+ if (composition.overflow === "error") return true;
1288
1462
  let path2 = diagnostic.path;
1289
1463
  while (path2) {
1290
1464
  if (strictPaths.has(path2)) return true;
@@ -1295,7 +1469,7 @@ function composeSlide(input, options = {}) {
1295
1469
  return false;
1296
1470
  });
1297
1471
  const explanation = decisions ? {
1298
- algorithm: "grid-score-v8",
1472
+ algorithm: "grid-score-v9",
1299
1473
  textMeasurement: options.textMeasurement ? "provided" : "estimated",
1300
1474
  textOutlines: options.textMeasurement?.outlineBounds ? "provided" : "unavailable",
1301
1475
  textRasterPadding: rasterPadding,
@@ -1303,7 +1477,7 @@ function composeSlide(input, options = {}) {
1303
1477
  unmeasuredPayloads: items.filter((item) => !headings.has(item.field) && !item.quoteLayout && !item.codeLayout && !item.metricLayout && !item.timelineLayout && !["text", "items", "bullets", "table"].includes(item.field)).map((item) => item.path)
1304
1478
  } : void 0;
1305
1479
  if (failures.length) throw new OPFCompositionError(failures, explanation);
1306
- return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...explanation ? { explanation } : {} };
1480
+ return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...furniture ? { furniture } : {}, ...explanation ? { explanation } : {} };
1307
1481
  }
1308
1482
  function resolveCanvasDimensions(input) {
1309
1483
  const presets = {
@@ -1323,4 +1497,4 @@ function resolveCanvasDimensions(input) {
1323
1497
  return { width, height };
1324
1498
  }
1325
1499
 
1326
- export { MAX_COMPOSITION_DEPTH, OPFCompositionError, chartColorForFill, colorContrast, composeSlide, fitList, fitRichText, fitText, layoutCode, layoutMetric, layoutQuote, layoutTable, layoutTimeline, measureText, measureTextOutline, placeTextLines, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, wrapText };
1500
+ export { MAX_COMPOSITION_DEPTH, OPFCompositionError, chartColorForFill, colorContrast, composeSlide, fitList, fitRichText, fitText, layoutCode, layoutFurniture, layoutMetric, layoutQuote, layoutTable, layoutTimeline, measureText, measureTextOutline, normalizeHexColor, placeTextLines, resolveCanvasDimensions, resolveColorRef, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, wrapText };