@openpresentation/opf 0.16.0 → 0.18.1

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 (63) hide show
  1. package/README.md +16 -2
  2. package/dist/browser.js +38 -0
  3. package/dist/{catalog-refs-_anZs-Qz.d.ts → catalog-refs-BSfGELpt.d.ts} +1 -1
  4. package/dist/{catalog-schemas-BZCjgAxu.d.ts → catalog-schemas-CinPh5WA.d.ts} +1 -1
  5. package/dist/catalog.d.ts +2 -2
  6. package/dist/catalog.js +3 -23712
  7. package/dist/{chunk-BSYQDWT4.js → chunk-64YSJZDH.js} +28 -15
  8. package/dist/{chunk-WEDGVC5W.js → chunk-6WW2EDUW.js} +108 -108
  9. package/dist/{chunk-5UEUAW57.js → chunk-CJPDTLGD.js} +1 -1
  10. package/dist/{chunk-FQSIIADJ.js → chunk-DBYZI75U.js} +30 -5
  11. package/dist/{chunk-PX5AWL7Y.js → chunk-JT3QXA22.js} +99 -29
  12. package/dist/{chunk-6OR326EA.js → chunk-JXVS7BJK.js} +1 -1
  13. package/dist/chunk-KXBZ6ZSB.js +991 -0
  14. package/dist/{chunk-5PDN7BN6.js → chunk-MBU42S5Y.js} +427 -167
  15. package/dist/{chunk-K4DQQIT4.js → chunk-NS3VYB57.js} +5 -5
  16. package/dist/chunk-QLFWWHRF.js +919 -0
  17. package/dist/{chunk-5TO6I3GB.js → chunk-RAKEVM52.js} +1 -1
  18. package/dist/{chunk-NAWQR7MN.js → chunk-VCGRMNY2.js} +1 -1
  19. package/dist/chunk-WCG3THTD.js +23712 -0
  20. package/dist/{chunk-UM24JFUE.js → chunk-YWXB4XAT.js} +2 -2
  21. package/dist/{chunk-3XDRQYWZ.js → chunk-YZXTXX2D.js} +1 -1
  22. package/dist/{chunk-SMLMI6IZ.js → chunk-ZCFSDSUX.js} +3 -3
  23. package/dist/{chunk-KINQFU6X.js → chunk-ZOHFLP3O.js} +19 -12
  24. package/dist/{composition-D4ADF5c-.d.ts → composition-C-OoqvAr.d.ts} +227 -167
  25. package/dist/composition.d.ts +2 -2
  26. package/dist/composition.js +1 -1
  27. package/dist/conversion-C5foedh6.d.ts +502 -0
  28. package/dist/conversion-KGDVJM75.js +21 -0
  29. package/dist/convert.d.ts +3 -3
  30. package/dist/convert.js +7 -7
  31. package/dist/data.d.ts +6 -6
  32. package/dist/data.js +2 -2
  33. package/dist/deck-report-wszX7uo8.d.ts +40 -0
  34. package/dist/deck.d.ts +21 -51
  35. package/dist/deck.js +10 -10
  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 +418 -451
  40. package/dist/format.js +2 -2
  41. package/dist/index.d.ts +179 -16
  42. package/dist/index.js +51 -796
  43. package/dist/markdown.d.ts +10 -6
  44. package/dist/markdown.js +7 -7
  45. package/dist/node-engine.d.ts +114 -0
  46. package/dist/node-engine.js +44 -0
  47. package/dist/{pagination-DzZPFheM.d.ts → pagination-BaUjDrTz.d.ts} +25 -8
  48. package/dist/pagination.d.ts +3 -3
  49. package/dist/pagination.js +4 -4
  50. package/dist/patch.js +4 -4
  51. package/dist/{presentation-COrFuj2g.d.ts → presentation-HondgJ3l.d.ts} +278 -454
  52. package/dist/repo-readme.js +1 -1
  53. package/dist/schemas.d.ts +4 -2
  54. package/dist/schemas.js +1 -1
  55. package/dist/spec/catalogs/manifest.json +1 -1
  56. package/dist/spec/schemas/opf.schema.json +74 -100
  57. package/dist/types.d.ts +2 -2
  58. package/dist/{validation-types-HxIgJfKx.d.ts → validation-types-PbGDrNUr.d.ts} +23 -7
  59. package/dist/validator.d.ts +6 -6
  60. package/dist/validator.js +3 -3
  61. package/dist/yaml.d.ts +4 -4
  62. package/dist/yaml.js +7 -7
  63. package/package.json +37 -2
@@ -1,5 +1,5 @@
1
- import { assertValid, resolveSlideContext } from './chunk-PX5AWL7Y.js';
2
- import { composeSlide, codeHighlightSlice, tableRowBoundaries, visitContentPayloads, sliceNumberedItems } from './chunk-5PDN7BN6.js';
1
+ import { assertValid, resolveSlideScriptMeasurement, resolveSlideContext } from './chunk-JT3QXA22.js';
2
+ import { resolveSlideVariables, usesSlideBuiltin, composeSlide, codeHighlightSlice, tableRowBoundaries, visitContentPayloads, slideTokenSpans, sliceNumberedItems } from './chunk-MBU42S5Y.js';
3
3
  import { checkCatalogsOption } from './chunk-SPFV27O5.js';
4
4
 
5
5
  // src/pagination.ts
@@ -39,6 +39,10 @@ var deckContext = (presentation) => {
39
39
  const deck = isRecord(presentation) ? presentation : {};
40
40
  return { ...Array.isArray(deck.references) ? { references: deck.references } : {}, ...isRecord(deck.datasets) ? { datasets: deck.datasets } : {} };
41
41
  };
42
+ function keepTokensWhole(boundaries, text) {
43
+ const spans = slideTokenSpans(text);
44
+ return spans.length ? boundaries.filter((boundary) => !spans.some(([start, end]) => boundary > start && boundary < end)) : boundaries;
45
+ }
42
46
  function leafFor(path, field, value) {
43
47
  const leaf = { path, field, value };
44
48
  let text;
@@ -62,7 +66,7 @@ function leafFor(path, field, value) {
62
66
  }
63
67
  if (text !== void 0) {
64
68
  leaf.unit = "utf16";
65
- leaf.boundaries = [0, ...Array.from(graphemes.segment(text), (segment) => segment.index + segment.segment.length)];
69
+ leaf.boundaries = keepTokensWhole([0, ...Array.from(graphemes.segment(text), (segment) => segment.index + segment.segment.length)], text);
66
70
  } else if (field === "items" || field === "bullets") {
67
71
  leaf.unit = "items";
68
72
  leaf.boundaries = Array.from({ length: value.length + 1 }, (_, i) => i);
@@ -103,8 +107,9 @@ function paginateSlide(input, options = {}) {
103
107
  };
104
108
  source = withReadability(source);
105
109
  let evaluations = 0;
106
- const { fonts, minFontSize: _minFontSize, maxSlides: _maxSlides, reservedIds: _reservedIds, ...engineOptions } = options;
107
- const composeOptions = { ...engineOptions, ...fonts?.textMeasurement ? { textMeasurement: fonts.textMeasurement } : {} };
110
+ const { fonts, catalogs, minFontSize: _minFontSize, maxSlides: _maxSlides, reservedIds: _reservedIds, ...engineOptions } = options;
111
+ const textMeasurement = fonts?.textMeasurement && options.presentation !== void 0 ? resolveSlideScriptMeasurement(options.presentation, sourceIndex, fonts.textMeasurement, { ...catalogs !== void 0 ? { catalogs } : {}, ...options.fontFamilies ? { fontFamilies: options.fontFamilies } : {} }) : fonts?.textMeasurement;
112
+ const composeOptions = { ...engineOptions, ...textMeasurement ? { textMeasurement } : {} };
108
113
  const readableLayout = (layout) => {
109
114
  if (!isRecord(layout) || !Array.isArray(layout.placeholders) || !layout.placeholders.some((entry) => isRecord(entry) && entry.type === "group")) return layout;
110
115
  const visit = (entries) => entries.map((entry) => {
@@ -115,9 +120,13 @@ function paginateSlide(input, options = {}) {
115
120
  return { ...layout, placeholders: visit(layout.placeholders) };
116
121
  };
117
122
  if (options.layout !== void 0) composeOptions.layout = readableLayout(options.layout);
123
+ const presentationSlides = options.presentation?.slides;
124
+ const slideCount = options.slideCount ?? (Array.isArray(presentationSlides) && presentationSlides.length ? presentationSlides.length : void 0);
125
+ const logoContext = options.presentation !== void 0 ? { presentation: options.presentation, darkBackground: options.darkBackground === true } : {};
118
126
  const geometry = (slide, pageIndex = 0) => {
119
127
  if (++evaluations > 2e4) throw new OPFPaginationError("Pagination exceeded its layout evaluation limit. Split the input into smaller sections.");
120
- return composeSlide(withReadability(slide, true), { ...composeOptions, slideNumber: (options.slideNumber ?? sourceIndex + 1) + pageIndex });
128
+ const slideNumber = (options.slideNumber ?? sourceIndex + 1) + pageIndex;
129
+ return composeSlide(resolveSlideVariables(withReadability(slide, true), { slideNumber, ...slideCount !== void 0 ? { slideCount } : {}, ...logoContext }), { ...composeOptions, slideNumber });
121
130
  };
122
131
  const initial = geometry(source);
123
132
  const fit = (diagnostics) => diagnostics.filter((issue) => issue.code === "text-overflow" || issue.code === "small-cell" || !!initial.furniture?.diagnostics.includes(issue));
@@ -133,7 +142,14 @@ function paginateSlide(input, options = {}) {
133
142
  if (!fit(initial.diagnostics).length) return { slides: [source], pages: [{ slideIndex: sourceIndex, mappings: initial.items.map((item) => ({ sourcePath: item.path, outputPath: item.path })), ...initial.furniture ? { repeatedMappings: repeatedMappings(0) } : {} }] };
134
143
  const headerIssues = initial.diagnostics.filter((issue) => headingFields.has(issue.path.slice(sourceBase.length + 1)) || initial.furniture?.diagnostics.includes(issue));
135
144
  if (headerIssues.length) throw new OPFPaginationError("Repeated headings or header/footer content cannot fit or resolve. Change the repeated content or slide design before pagination.", headerIssues);
136
- const leaves = initial.items.filter((item) => !headingFields.has(item.field)).map((item) => leafFor(item.path, item.field, item.value));
145
+ const tokens = resolveSlideVariables(source, { slideNumber: 1, slideCount: 1, ...logoContext }) !== source;
146
+ const sourceValue = (path, fallback) => {
147
+ if (!tokens || !path.startsWith(`${sourceBase}.`)) return fallback;
148
+ let node = source;
149
+ for (const key of path.slice(sourceBase.length + 1).split(".")) node = isRecord(node) || Array.isArray(node) ? node[key] : void 0;
150
+ return node === void 0 ? fallback : node;
151
+ };
152
+ const leaves = initial.items.filter((item) => !headingFields.has(item.field)).map((item) => leafFor(item.path, item.field, sourceValue(item.path, item.value)));
137
153
  const leafPaths = new Set(leaves.map((leaf) => leaf.path));
138
154
  const slides = [], pages = [];
139
155
  let selected = /* @__PURE__ */ new Map();
@@ -256,10 +272,6 @@ function paginateSlide(input, options = {}) {
256
272
  if (!slides.length) throw new OPFPaginationError("No body content can be paginated.", initial.diagnostics);
257
273
  return { slides, pages };
258
274
  }
259
- var usesSlideTotal = (presentation) => [presentation, ...presentation.slides].some((owner) => ["header", "footer"].some((kind) => ["left", "center", "right"].some((zone) => {
260
- const content = owner?.design?.[kind]?.[zone];
261
- return content?.slideNumber === true && typeof content.slideNumberFormat === "string" && content.slideNumberFormat.includes("{total}");
262
- })));
263
275
  function paginate(input, options = {}) {
264
276
  checkCatalogsOption(options.catalogs, "paginate");
265
277
  assertValid(input, { only: ["format"] });
@@ -280,7 +292,7 @@ function paginate(input, options = {}) {
280
292
  }
281
293
  }
282
294
  if (output2.length >= maxSlides) throw new OPFPaginationError(`Pagination needs more than ${maxSlides} slides. No partial result was returned.`);
283
- const result2 = paginateSlide(slide, { ...context.options, textRasterPadding: options.textRasterPadding, fonts: options.fonts, presentation, slideIndex: index, slideNumber: output2.length + 1, slideCount: slideCount2, date: options.date, maxSlides: maxSlides - output2.length, minFontSize: options.minFontSize, reservedIds });
295
+ const result2 = paginateSlide(slide, { ...context.options, textRasterPadding: options.textRasterPadding, fonts: options.fonts, ...options.catalogs ? { catalogs: options.catalogs } : {}, presentation, slideIndex: index, slideNumber: output2.length + 1, slideCount: slideCount2, date: options.date, maxSlides: maxSlides - output2.length, minFontSize: options.minFontSize, reservedIds });
284
296
  const outputStart = output2.length;
285
297
  result2.pages.forEach((page, pageIndex) => {
286
298
  const remap = (mapping) => ({ ...mapping, outputPath: mapping.outputPath.replace(/^slides\.\d+/, `slides.${outputStart + pageIndex}`) });
@@ -291,16 +303,17 @@ function paginate(input, options = {}) {
291
303
  });
292
304
  return { output: output2, pages: pages2 };
293
305
  };
306
+ const usesSlideCount = usesSlideBuiltin(presentation, "deck.slideCount");
294
307
  let slideCount = presentation.slides.length, result = run(slideCount);
295
- for (let attempt = 0; usesSlideTotal(presentation) && result.output.length !== slideCount; attempt++) {
296
- if (attempt >= 3) throw new OPFPaginationError("Slide-number totals did not converge. Change the slide-number format or split the input.");
308
+ for (let attempt = 0; usesSlideCount && result.output.length !== slideCount; attempt++) {
309
+ if (attempt >= 3) throw new OPFPaginationError("The slide count did not converge. Change the text around {{deck.slideCount}} or split the input.");
297
310
  slideCount = result.output.length;
298
311
  result = run(slideCount);
299
312
  }
300
313
  const { output, pages } = result;
301
314
  presentation.slides = output;
302
315
  assertValid(presentation, { only: ["format"] });
303
- return { presentation, pages };
316
+ return { presentation, pages, layout: options.fonts ? "measured" : "estimated" };
304
317
  }
305
318
 
306
319
  export { OPFPaginationError, paginate, paginateSlide };
@@ -52,7 +52,7 @@ var presentation = {
52
52
  "minItems": 1
53
53
  }
54
54
  ],
55
- "description": "Organization associated with the presentation, usually the presenting company. Array form supports hosts, partners, clients, and sponsors. The primary organization (Organization.role 'primary', else the first item) supplies the default deck logo, the 'organization' and 'socials' header/footer fields and the 'organization.<field>' built-in variables; the others are available by id as 'organization.<id>.<field>' and are not drawn automatically."
55
+ "description": "Organization associated with the presentation, usually the presenting company. Array form supports hosts, partners, clients, and sponsors. The primary organization (Organization.role 'primary', else the first item) supplies the cover and section logo, the 'socials' header/footer field and the 'organization.<field>' built-in variables; the others are available by id as 'organization.<id>.<field>' and are not drawn automatically."
56
56
  },
57
57
  "speaker": {
58
58
  "oneOf": [
@@ -67,7 +67,7 @@ var presentation = {
67
67
  "minItems": 1
68
68
  }
69
69
  ],
70
- "description": "Person presenting the deck. Array form supports panels and multi-speaker decks. The first speaker is the primary speaker: the built-in variables 'speaker.<field>' and the 'speaker' header/footer field read it, 'speakers' lists every name, and 'speaker.<id>.<field>' addresses one by id. A speaker is drawn nowhere else."
70
+ "description": "Person presenting the deck. Array form supports panels and multi-speaker decks. The first speaker is the primary speaker: the built-in variables 'speaker.<field>' read it, 'speakers' lists every name, and 'speaker.<id>.<field>' addresses one by id. A speaker is drawn nowhere else."
71
71
  },
72
72
  "author": {
73
73
  "oneOf": [
@@ -775,7 +775,7 @@ var presentation = {
775
775
  },
776
776
  "Organization": {
777
777
  "type": "object",
778
- "description": "An organization associated with the presentation, typically the presenting company, but also hosts, partners, clients, or sponsors. The primary organization (role 'primary', else the first one) supplies the default deck logo (unless design.logo overrides it), the 'organization' header/footer field (its name) and the 'socials' field (its socials). Every field is also a built-in variable ('{{organization.name}}', 'var:organization.logo', 'organization.<id>.<field>'). Nothing else about an organization is drawn automatically. See docs/templates-and-variables.md.",
778
+ "description": "An organization associated with the presentation, typically the presenting company, but also hosts, partners, clients, or sponsors. The primary organization (role 'primary', else the first one) supplies the cover and section logo (unless design.logo overrides it) and the 'socials' header/footer field (its socials). Every field is also a built-in variable ('{{organization.name}}', 'organization.<id>.<field>'), and every logo a slide-scoped image ('var:organization.logo.icon', 'var:organization.<id>.logo'). Nothing else about an organization is drawn automatically. See docs/templates-and-variables.md.",
779
779
  "required": [
780
780
  "id",
781
781
  "name"
@@ -793,7 +793,7 @@ var presentation = {
793
793
  },
794
794
  "name": {
795
795
  "type": "string",
796
- "description": "Display name. Drawn by the 'organization' header/footer field for the primary organization; the built-in variable 'organization.name' (or 'organization.<id>.name') carries it into any string.",
796
+ "description": "Display name. The built-in variable 'organization.name' (primary organization) or 'organization.<id>.name' carries it into any string, such as a header or footer text of '{{organization.name}}'.",
797
797
  "examples": [
798
798
  "Acme Corp",
799
799
  "DevConf 2026",
@@ -809,12 +809,29 @@ var presentation = {
809
809
  ]
810
810
  },
811
811
  "logo": {
812
- "$ref": "#/$defs/Asset",
813
- "description": "Source for the organization's logo image. Accepts an HTTPS URL, data URI, relative path (resolved against the OPF file location), local path, or 'asset:<id>' reference. Common formats are SVG (preferred for vector logos), PNG (with transparency), or JPG. The primary organization's logo (role 'primary', else the first organization) is the fallback source wherever the deck logo is drawn: a slide's design.logo, then design.logo, then this value. It is therefore drawn on cover and section slides and by header/footer zones with logo: true, exactly as design.logo would be (see design.logo and docs/design-resolution.md). It is also the built-in image variable 'organization.logo' (or 'organization.<id>.logo'), placed with 'var:organization.logo' in an image field.",
812
+ "oneOf": [
813
+ {
814
+ "$ref": "#/$defs/Asset"
815
+ },
816
+ {
817
+ "$ref": "#/$defs/OrganizationLogo"
818
+ }
819
+ ],
820
+ "description": "The organization's logo. A path, URL or Asset is one logo for every shape; an object gives up to four shapes (full, stacked, icon, wordmark), each a path or Asset, or { onLight, onDark } for the two backgrounds. A path is resolved against the OPF file location; an HTTPS URL, data URI or 'asset:<id>' reference works too. SVG (preferred for vector logos) or PNG with transparency are typical. The primary organization's full logo (role 'primary', else the first organization) is drawn on cover and section slides unless design.logo names another or is false. Every logo is also a slide-scoped built-in image, placed with a whole-field reference in any image field: 'var:organization.logo' (the full logo), 'var:organization.logo.icon' (likewise stacked, wordmark, full), and 'var:organization.<id>.logo' or 'var:organization.<id>.logo.<shape>' for another organization. A missing shape falls back to full, and full to the first defined of wordmark, stacked and icon. The onLight or onDark value is chosen for each slide's background; a missing one uses the other. See docs/templates-and-variables.md and docs/design-resolution.md.",
814
821
  "examples": [
822
+ "./assets/acme-logo.svg",
823
+ {
824
+ "full": "./assets/acme-logo.svg",
825
+ "icon": "./assets/acme-mark.svg"
826
+ },
827
+ {
828
+ "full": {
829
+ "onLight": "./assets/acme-logo.svg",
830
+ "onDark": "./assets/acme-logo-white.svg"
831
+ },
832
+ "icon": "./assets/acme-mark.svg"
833
+ },
815
834
  "https://cdn.acme.com/logo.svg",
816
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9V3iWggAAAAASUVORK5CYII=",
817
- "./assets/logo.jpg",
818
835
  "asset:acme-logo"
819
836
  ]
820
837
  },
@@ -863,7 +880,7 @@ var presentation = {
863
880
  "sponsor",
864
881
  "host"
865
882
  ],
866
- "description": "Role of the organization relative to the presentation. Only 'primary' has behavior: it selects the primary organization (deck logo, 'organization' and 'socials' header/footer fields, and the 'organization.<field>' built-ins). When no organization has role 'primary', the first one is primary. 'partner', 'client', 'sponsor' and 'host' are descriptive labels that change nothing."
883
+ "description": "Role of the organization relative to the presentation. Only 'primary' has behavior: it selects the primary organization (cover and section logo, the 'socials' header/footer field, and the 'organization.<field>' built-ins). When no organization has role 'primary', the first one is primary. 'partner', 'client', 'sponsor' and 'host' are descriptive labels that change nothing."
867
884
  },
868
885
  "socials": {
869
886
  "$ref": "#/$defs/Socials",
@@ -873,7 +890,7 @@ var presentation = {
873
890
  },
874
891
  "Speaker": {
875
892
  "type": "object",
876
- "description": "A person presenting the deck. A speaker is drawn only through built-in variables ('{{speaker.name}}' inside any string, 'var:speaker.photo' as a whole image field) and the 'speaker' header/footer field (the first speaker's name and title). No layout, cover or bio slide places a speaker on its own. See docs/templates-and-variables.md.",
893
+ "description": "A person presenting the deck. A speaker is drawn only through built-in variables ('{{speaker.name}}' inside any string, a header or footer text included, and 'var:speaker.photo' as a whole image field). No layout, cover or bio slide places a speaker on its own. See docs/templates-and-variables.md.",
877
894
  "required": [
878
895
  "id",
879
896
  "name"
@@ -891,7 +908,7 @@ var presentation = {
891
908
  },
892
909
  "name": {
893
910
  "type": "string",
894
- "description": "Display name. Built-in variable 'speaker.name' (first speaker) or 'speaker.<id>.name'; the first speaker's name is also drawn by the 'speaker' header/footer field and every name is listed by 'speakers'.",
911
+ "description": "Display name. Built-in variable 'speaker.name' (first speaker) or 'speaker.<id>.name'; every name is listed by 'speakers'.",
895
912
  "examples": [
896
913
  "Alice Chen",
897
914
  "Bob Lee",
@@ -900,7 +917,7 @@ var presentation = {
900
917
  },
901
918
  "title": {
902
919
  "type": "string",
903
- "description": "Role or title. Built-in variable 'speaker.title'; the 'speaker' header/footer field draws it after the name ('Alice Chen, VP of Engineering').",
920
+ "description": "Role or title. Built-in variable 'speaker.title' (or 'speaker.<id>.title'), for example in a footer text of '{{speaker.name}}, {{speaker.title}}'.",
904
921
  "examples": [
905
922
  "VP of Engineering",
906
923
  "Founder & CEO",
@@ -1086,20 +1103,19 @@ var presentation = {
1086
1103
  "logo": {
1087
1104
  "oneOf": [
1088
1105
  {
1089
- "$ref": "#/$defs/Asset"
1106
+ "type": "string",
1107
+ "pattern": "^var:organization(\\.[A-Za-z0-9_-]+)?\\.logo(\\.(full|stacked|icon|wordmark))?$"
1090
1108
  },
1091
1109
  {
1092
- "$ref": "#/$defs/LogoSet"
1110
+ "type": "boolean",
1111
+ "const": false
1093
1112
  }
1094
1113
  ],
1095
- "description": "Deck logo assets used by covers, section dividers, headers, footers and picture bullets. A string or Asset object is the default logo source; the LogoSet object form provides light/dark, stacked, icon, and wordmark variants (see LogoSet for the selection order). Source precedence: a slide's design.logo, then this value, then the primary organization's logo; absence inherits. Placement (reference engines, vetoable): on a cover or section slide (no body payload on a heading-only layout such as title, title-subtitle or section-divider, or no layout) the lockup variant is drawn at the top-left of the free area, inside the slide padding and below any header furniture, in a box 56 reference pixels tall and at most four times as wide; the tag/title/subtitle group then centers in the remaining span. Content slides never get an automatic logo. Header and footer zones draw the icon variant when they set logo: true, and design.listBullet 'image' draws it as the picture bullet. composeSlide returns the cover box as geometry.logo.",
1114
+ "description": "Which logo covers, section dividers and picture bullets draw. Logos live on the organization (Organization.logo); without this field covers and sections draw the primary organization's full logo and picture bullets its icon. A logo reference overrides that: 'var:organization.beta.logo' draws organization beta's logo, and a reference that names a shape ('var:organization.logo.wordmark') draws that shape everywhere this field applies. false draws no logo. A slide's design.logo overrides the deck's; absence inherits. An override naming an organization without a logo draws nothing (validation warns). Placement (reference engines, vetoable): on a cover or section slide (no body payload on a heading-only layout such as title, title-subtitle or section-divider, or no layout) the logo is drawn at the top-left of the free area, inside the slide padding and below any header furniture, in a box 56 reference pixels tall and at most four times as wide; the tag/title/subtitle group then centers in the remaining span. Content slides never get an automatic logo; a header or footer zone places one with image: 'var:organization.logo.icon'. composeSlide returns the cover box as geometry.logo.",
1096
1115
  "examples": [
1097
- "asset:acme-logo",
1098
- {
1099
- "default": "asset:acme-logo",
1100
- "light": "asset:acme-logo-white",
1101
- "icon": "asset:acme-icon"
1102
- }
1116
+ "var:organization.logo.wordmark",
1117
+ "var:organization.beta.logo",
1118
+ false
1103
1119
  ]
1104
1120
  },
1105
1121
  "watermark": {
@@ -1144,10 +1160,10 @@ var presentation = {
1144
1160
  false,
1145
1161
  {
1146
1162
  "left": {
1147
- "section": true
1163
+ "text": "{{slide.section}}"
1148
1164
  },
1149
1165
  "right": {
1150
- "image": "asset:acme-logo"
1166
+ "image": "var:organization.logo.icon"
1151
1167
  }
1152
1168
  }
1153
1169
  ]
@@ -1167,13 +1183,13 @@ var presentation = {
1167
1183
  false,
1168
1184
  {
1169
1185
  "left": {
1170
- "image": "asset:acme-logo"
1186
+ "image": "var:organization.logo.icon"
1171
1187
  },
1172
1188
  "center": {
1173
1189
  "text": "Confidential"
1174
1190
  },
1175
1191
  "right": {
1176
- "slideNumber": true
1192
+ "text": "{{slide.number}}"
1177
1193
  }
1178
1194
  }
1179
1195
  ]
@@ -1234,7 +1250,7 @@ var presentation = {
1234
1250
  "character",
1235
1251
  "image"
1236
1252
  ],
1237
- "description": "Marker style for items and bullets lists. Effective value: the slide's design, then the deck's design, then the layout record's design.listBullet, then 'character'. 'character' (the default) draws the glyph marker. 'image' draws the deck's icon logo (a slide's design.logo, then design.logo, then the primary organization's logo; light variants on dark backgrounds) as a picture bullet: a square of the marker's font size whose bottom sits on the marker baseline, so list geometry does not change. Without a logo the glyph is drawn and the engine reports unresolved-content at this field."
1253
+ "description": "Marker style for items and bullets lists. Effective value: the slide's design, then the deck's design, then the layout record's design.listBullet, then 'character'. 'character' (the default) draws the glyph marker. 'image' draws the primary organization's icon logo (or the organization and shape design.logo names; the onDark value on dark backgrounds) as a picture bullet: a square of the marker's font size whose bottom sits on the marker baseline, so list geometry does not change. Without a logo the glyph is drawn and the engine reports unresolved-content at this field."
1238
1254
  }
1239
1255
  }
1240
1256
  },
@@ -1666,7 +1682,7 @@ var presentation = {
1666
1682
  "additionalProperties": {
1667
1683
  "$ref": "#/$defs/Variable"
1668
1684
  },
1669
- "description": "Named variables, keyed by stable kebab-case id. A variable is a typed, named value the deck declares once and uses in many places: a color ('var:<id>' in color fields, the original use), or content that fills a template (text, number, date, image, url, list). Content is referenced inline as '{{<id>}}' inside any string, or whole as 'var:<id>' in a field of the matching type; '\\{{' writes a literal '{{'. A hex string is shorthand for a color variable. A variable with no 'value' is unfilled: expected in a template (root 'template': true), an error in a normal deck. Engines resolve variables with resolveVariables() before composing, rendering or exporting, so a template plus its values previews and exports exactly like the equivalent hand-written deck. Read-only built-in variables come from the deck's own metadata and use dotted names that a user id can never take: 'deck.name', 'deck.description', 'deck.author', 'speaker.name|title|email|phone|bio|photo' (first speaker), 'speakers' (every speaker's name, a list), 'organization.name|legalName|tagline|domain|email|phone|logo' (primary organization), and 'speaker.<id>.<field>' / 'organization.<id>.<field>' by id. They are written the same two ways ('{{speaker.name}}', 'var:speaker.photo'), are never declared, and an unknown built-in path is a validation error; a known one with no source value resolves to nothing with a warning. The id 'speakers' is reserved. See docs/templates-and-variables.md.",
1685
+ "description": "Named variables, keyed by stable kebab-case id. A variable is a typed, named value the deck declares once and uses in many places: a color ('var:<id>' in color fields, the original use), or content that fills a template (text, number, date, image, url, list). Content is referenced inline as '{{<id>}}' inside any string, or whole as 'var:<id>' in a field of the matching type; '\\{{' writes a literal '{{'. A hex string is shorthand for a color variable. A variable with no 'value' is unfilled: expected in a template (root 'template': true), an error in a normal deck. Engines resolve variables with resolveVariables() before composing, rendering or exporting, so a template plus its values previews and exports exactly like the equivalent hand-written deck. Read-only built-in variables come from the deck's own metadata and use dotted names that a user id can never take: 'deck.name', 'deck.description', 'deck.author', 'speaker.name|title|email|phone|bio|photo' (first speaker), 'speakers' (every speaker's name, a list), 'organization.name|legalName|tagline|domain|email|phone' (primary organization), and 'speaker.<id>.<field>' / 'organization.<id>.<field>' by id. They are written the same two ways ('{{speaker.name}}', 'var:speaker.photo'), are never declared, and an unknown built-in path is a validation error; a known one with no source value resolves to nothing with a warning. The id 'speakers' is reserved. Three built-ins are slide-scoped and written only as inline tokens: '{{slide.number}}' (the displayed slide number after pagination), '{{slide.section}}' (the slide's section) and '{{deck.slideCount}}' (the number of slides after pagination). resolveVariables() leaves them as written; they resolve per slide as it is composed (resolveSlideVariables(), and header/footer text in layoutFurniture()), so every 'slide.' name is reserved and 'var:slide.*' or 'var:deck.slideCount' is a validation error. Organization logos are slide-scoped whole-field images: 'var:organization.logo', 'var:organization.logo.<shape>' (full, stacked, icon, wordmark), 'var:organization.<id>.logo' and 'var:organization.<id>.logo.<shape>' resolve per slide to the onLight or onDark asset for its background. See docs/templates-and-variables.md.",
1670
1686
  "examples": [
1671
1687
  {
1672
1688
  "risk": "#B42318",
@@ -2509,60 +2525,71 @@ var presentation = {
2509
2525
  }
2510
2526
  }
2511
2527
  },
2512
- "LogoSet": {
2528
+ "OrganizationLogo": {
2513
2529
  "type": "object",
2514
- "description": "Deck logo variants surfaced by covers, section dividers, headers, footers and picture bullets. Organization identity lives in organization; this object only controls visual rendering assets. Engines select one variant per slot and background tone (resolveLogo in @openpresentation/opf): same-tone variants first, neutral ones next, the opposite tone last. Lockup on a dark background: light, default, stackedLight, stacked, wordmarkLight, wordmark, iconLight, icon, then dark, stackedDark, wordmarkDark, iconDark; on a light background: dark, default, stackedDark, stacked, wordmarkDark, wordmark, iconDark, icon, then light, stackedLight, wordmarkLight, iconLight. The icon slot (headers, footers, picture bullets) tries iconLight or iconDark for the tone, then icon, then the lockup chain; the stacked slot tries stackedLight or stackedDark, then stacked, then the lockup chain.",
2530
+ "description": "An organization's logo in up to four shapes. Each shape is a path or Asset, or { onLight, onDark } for the two backgrounds. A missing shape falls back to full, and full to the first defined of wordmark, stacked and icon; within a shape a missing onLight or onDark uses the other. Shapes are placed with slide-scoped references such as 'var:organization.logo.icon'.",
2515
2531
  "additionalProperties": false,
2532
+ "minProperties": 1,
2516
2533
  "properties": {
2517
- "default": {
2518
- "$ref": "#/$defs/Asset",
2519
- "description": "Default full-lockup logo. Used as fallback when no more specific variant is set."
2520
- },
2521
- "light": {
2522
- "$ref": "#/$defs/Asset",
2523
- "description": "Light-colored full-lockup logo intended for rendering on dark backgrounds."
2524
- },
2525
- "dark": {
2526
- "$ref": "#/$defs/Asset",
2527
- "description": "Dark-colored full-lockup logo intended for rendering on light backgrounds."
2534
+ "full": {
2535
+ "$ref": "#/$defs/LogoSource",
2536
+ "description": "The full logo (lockup): mark and name side by side. Drawn on cover and section slides, and placed by 'var:organization.logo'."
2528
2537
  },
2529
2538
  "stacked": {
2530
- "$ref": "#/$defs/Asset",
2531
- "description": "Stacked vertical logo lockup, suited to portrait or square brand-mark slots."
2532
- },
2533
- "stackedLight": {
2534
- "$ref": "#/$defs/Asset",
2535
- "description": "Light-colored stacked logo variant intended for rendering on dark backgrounds."
2536
- },
2537
- "stackedDark": {
2538
- "$ref": "#/$defs/Asset",
2539
- "description": "Dark-colored stacked logo variant intended for rendering on light backgrounds."
2539
+ "$ref": "#/$defs/LogoSource",
2540
+ "description": "The stacked lockup: mark above the name, for portrait or square slots. Placed by 'var:organization.logo.stacked'."
2540
2541
  },
2541
2542
  "icon": {
2542
- "$ref": "#/$defs/Asset",
2543
- "description": "Default icon, mark, or symbol without wordmark. Useful for tight spaces such as footers, badges, and slide-corner marks."
2544
- },
2545
- "iconLight": {
2546
- "$ref": "#/$defs/Asset",
2547
- "description": "Light-colored icon variant intended for rendering on dark backgrounds."
2548
- },
2549
- "iconDark": {
2550
- "$ref": "#/$defs/Asset",
2551
- "description": "Dark-colored icon variant intended for rendering on light backgrounds."
2543
+ "$ref": "#/$defs/LogoSource",
2544
+ "description": "The mark or symbol without the name, for tight spaces such as header and footer zones and picture bullets. Placed by 'var:organization.logo.icon'."
2552
2545
  },
2553
2546
  "wordmark": {
2554
- "$ref": "#/$defs/Asset",
2555
- "description": "Default wordmark: the organization name set in branded typography, without icon."
2547
+ "$ref": "#/$defs/LogoSource",
2548
+ "description": "The name set in the brand's lettering, without the mark. Placed by 'var:organization.logo.wordmark'."
2549
+ }
2550
+ },
2551
+ "examples": [
2552
+ {
2553
+ "full": "./assets/acme-logo.svg",
2554
+ "icon": "./assets/acme-mark.svg"
2556
2555
  },
2557
- "wordmarkLight": {
2558
- "$ref": "#/$defs/Asset",
2559
- "description": "Light-colored wordmark variant intended for rendering on dark backgrounds."
2556
+ {
2557
+ "full": {
2558
+ "onLight": "./assets/acme-logo.svg",
2559
+ "onDark": "./assets/acme-logo-white.svg"
2560
+ }
2561
+ }
2562
+ ]
2563
+ },
2564
+ "LogoSource": {
2565
+ "description": "One logo shape: a path or Asset for every background, or { onLight, onDark }. onLight is drawn on light backgrounds (usually dark artwork) and onDark on dark backgrounds (usually light artwork), by each slide's background; a missing one uses the other.",
2566
+ "oneOf": [
2567
+ {
2568
+ "$ref": "#/$defs/Asset"
2560
2569
  },
2561
- "wordmarkDark": {
2562
- "$ref": "#/$defs/Asset",
2563
- "description": "Dark-colored wordmark variant intended for rendering on light backgrounds."
2570
+ {
2571
+ "type": "object",
2572
+ "additionalProperties": false,
2573
+ "minProperties": 1,
2574
+ "properties": {
2575
+ "onLight": {
2576
+ "$ref": "#/$defs/Asset",
2577
+ "description": "The logo for light backgrounds, usually dark artwork."
2578
+ },
2579
+ "onDark": {
2580
+ "$ref": "#/$defs/Asset",
2581
+ "description": "The logo for dark backgrounds, usually light or white artwork."
2582
+ }
2583
+ }
2564
2584
  }
2565
- }
2585
+ ],
2586
+ "examples": [
2587
+ "./assets/acme-mark.svg",
2588
+ {
2589
+ "onLight": "./assets/acme-logo.svg",
2590
+ "onDark": "./assets/acme-logo-white.svg"
2591
+ }
2592
+ ]
2566
2593
  },
2567
2594
  "Watermark": {
2568
2595
  "type": "object",
@@ -2625,41 +2652,26 @@ var presentation = {
2625
2652
  },
2626
2653
  "HeaderFooterItem": {
2627
2654
  "type": "object",
2628
- "description": "One header/footer zone. Every configured field renders; fields in one zone stack top to bottom in the order logo, image, text, organization, speaker, socials, section, slide number, date. Put a date and a slide number in different zones to keep each on the zone's single line.",
2655
+ "description": "One header/footer zone. Every configured field renders, side by side in one row in the order image, text, socials, date (mirrored in a right-to-left deck), aligned to the zone's edge (left, center or right) and vertically centered on each other; a line break inside text or socials ('\\n') stacks lines within that part. When the parts are wider than the zone their text wraps, and a row that cannot fit even so is reported as overflow. Generated values such as the slide number, the slide count, the section, the organization or the speaker are variables inside text ('{{slide.number}} / {{deck.slideCount}}', '{{organization.name}}'), so one text writes several values in the author's order. An organization logo is image: 'var:organization.logo.icon'. See docs/templates-and-variables.md.",
2656
+ "additionalProperties": false,
2629
2657
  "properties": {
2630
- "logo": {
2631
- "type": "boolean",
2632
- "description": "Whether to render the deck's icon logo in this zone: a slide's design.logo, then design.logo, then the primary organization's logo (LogoSet icon variants first, light ones on dark backgrounds). It is a generated image part in the same box as image; without a logo the engine reports unresolved-content at this field. Use image for a literal picture that is not the deck logo."
2633
- },
2634
2658
  "text": {
2635
2659
  "type": "string",
2636
- "description": "Literal text rendered in this zone.",
2660
+ "description": "Text rendered in this zone. Variables resolve inside it like any string: deck-wide ones ('{{organization.name}}', '{{speaker.name}}, {{speaker.title}}', '{{customer}}') before composition, and the slide-scoped built-ins '{{slide.number}}', '{{slide.section}}' and '{{deck.slideCount}}' for each slide as it is laid out. Each '{{slide.number}}' is a live slide-number field: PPTX export writes it as a native field when its value fits within one accepted text line, and as static text with a diagnostic otherwise. '{{deck.slideCount}}' is fixed text, because PowerPoint has no slide-count field. Write '\\{{' for literal braces.",
2637
2661
  "examples": [
2638
2662
  "Confidential",
2639
- "Q4 Business Review"
2663
+ "Q4 Business Review",
2664
+ "{{slide.number}} / {{deck.slideCount}}",
2665
+ "{{organization.name}}\n{{slide.section}}"
2640
2666
  ]
2641
2667
  },
2642
2668
  "image": {
2643
2669
  "$ref": "#/$defs/Asset",
2644
- "description": "Generic image rendered in this zone, such as a logo, partner mark, certification badge, or icon.",
2670
+ "description": "Image rendered first in this zone's row, such as a logo, partner mark, certification badge, or icon: a path, URL or Asset, or an organization logo reference ('var:organization.logo.icon', 'var:organization.<id>.logo.wordmark'), which resolves for each slide's background (onLight or onDark).",
2645
2671
  "examples": [
2646
- "asset:acme-logo",
2647
- "./assets/footer-badge.svg"
2648
- ]
2649
- },
2650
- "slideNumber": {
2651
- "type": "boolean",
2652
- "description": "Whether to render the current slide number in this zone. PPTX export writes a native slide-number field when its value fits within one accepted text line; a value split across lines exports as static text with a diagnostic."
2653
- },
2654
- "slideNumberFormat": {
2655
- "type": "string",
2656
- "pattern": "[{]current[}]",
2657
- "description": 'Template for the slide number when slideNumber is true. {current} is the displayed slide number (a native PPTX field when its value fits within one accepted text line); {total} is the number of slides in the rendered or exported deck, written as fixed text because PowerPoint has no slide-count field. Other characters are literal. Defaults to "{current}".',
2658
- "examples": [
2659
- "{current}",
2660
- "{current} / {total}",
2661
- "A-{current}",
2662
- "Page {current} of {total}"
2672
+ "var:organization.logo.icon",
2673
+ "./assets/footer-badge.svg",
2674
+ "var:organization.beta.logo"
2663
2675
  ]
2664
2676
  },
2665
2677
  "date": {
@@ -2689,18 +2701,6 @@ var presentation = {
2689
2701
  "MMMM d, yyyy"
2690
2702
  ]
2691
2703
  },
2692
- "organization": {
2693
- "type": "boolean",
2694
- "description": "Whether to render the primary organization's name from organization (role 'primary', else the first organization)."
2695
- },
2696
- "speaker": {
2697
- "type": "boolean",
2698
- "description": "Whether to render the primary (first) speaker's name and title from speaker, joined as 'Ada Lovelace, CTO' (just the name when the speaker has no title). It is generated text: without a named speaker the engine reports unresolved-content at this field. PPTX export writes it as static text and import restores the flag from the file's furniture provenance."
2699
- },
2700
- "section": {
2701
- "type": "boolean",
2702
- "description": "Whether to render the current slide section label."
2703
- },
2704
2704
  "socials": {
2705
2705
  "type": "boolean",
2706
2706
  "description": "Whether to render the primary organization's social profiles from organization.socials, one line per platform in key order. A handle is formatted through the platform's socialPlatforms record (companyUrlPattern, else profileUrlPattern, else baseUrl, with handlePrefix stripped) and shown as that profile URL without its 'https://' scheme; a value that is already a URL is shown without an 'https://' scheme; an unknown platform key shows the raw value. Exporters link each line to its full URL when one exists. Speaker socials are not rendered by this field."
@@ -3100,7 +3100,7 @@ var presentation = {
3100
3100
  },
3101
3101
  "section": {
3102
3102
  "type": "string",
3103
- "description": "PowerPoint-style slide section label. Consecutive slides with the same value belong to the same section in presenter view, outlines, and PowerPoint section-aware exports.",
3103
+ "description": "PowerPoint-style slide section label. Consecutive slides with the same value belong to the same section in presenter view, outlines, and PowerPoint section-aware exports. Also the slide-scoped built-in variable '{{slide.section}}', for example in a header text.",
3104
3104
  "examples": [
3105
3105
  "Intro",
3106
3106
  "Problem",
@@ -3111,7 +3111,7 @@ var presentation = {
3111
3111
  },
3112
3112
  "hidden": {
3113
3113
  "type": "boolean",
3114
- "description": "Whether the slide is hidden from the presented sequence. The player skips it, the PPTX export writes it as a hidden slide, and per-slide image and PDF output skips it unless the caller asks to include hidden slides (opf CLI: --include-hidden; the deck-level renderSvg: the opt-in skipHidden). A slide named explicitly (opf render --slides, renderSvg slideIndex) is still rendered."
3114
+ "description": "Whether the slide is hidden from the presented sequence. The player skips it, the PPTX export writes it as a hidden slide, and per-slide image and PDF output skips it unless the caller asks to include hidden slides (opf CLI: --include-hidden; the deck-level toSvg: the opt-in skipHidden). A slide named explicitly (opf convert --slides, toSvg with a slide number) is still rendered."
3115
3115
  },
3116
3116
  "composition": {
3117
3117
  "$ref": "#/$defs/Composition"
@@ -1,4 +1,4 @@
1
- import { isRecord, same } from './chunk-5TO6I3GB.js';
1
+ import { isRecord, same } from './chunk-RAKEVM52.js';
2
2
  import { parseDocument, YAMLMap, Scalar, stringify } from 'yaml';
3
3
 
4
4
  // src/not-checked.ts
@@ -1,7 +1,7 @@
1
- import { Ctx, splitLines, lineRange, readYamlMapping, NOT_CHECKED, OPFMarkdownError, writeYaml, readCsv, DECIMAL, csvField, scalarText } from './chunk-5UEUAW57.js';
2
- import { parseQuoteLines, levelsFromIndents, parseCodeFence, splitWhen, fenceCode } from './chunk-NAWQR7MN.js';
3
- import { same, isRecord, runText, isPlainRun, FORMATTING } from './chunk-5TO6I3GB.js';
4
- import { validate } from './chunk-PX5AWL7Y.js';
1
+ import { Ctx, splitLines, lineRange, readYamlMapping, NOT_CHECKED, OPFMarkdownError, writeYaml, readCsv, DECIMAL, csvField, scalarText } from './chunk-CJPDTLGD.js';
2
+ import { parseQuoteLines, levelsFromIndents, parseCodeFence, splitWhen, fenceCode } from './chunk-VCGRMNY2.js';
3
+ import { same, isRecord, runText, isPlainRun, FORMATTING } from './chunk-RAKEVM52.js';
4
+ import { validate } from './chunk-JT3QXA22.js';
5
5
  import { checkCatalogsOption } from './chunk-SPFV27O5.js';
6
6
 
7
7
  // src/markdown/canon.ts
@@ -789,6 +789,30 @@ function splitSegments(lines, split) {
789
789
  }
790
790
  return segments;
791
791
  }
792
+ function isOutline(lines) {
793
+ let fence;
794
+ let comment = false;
795
+ let notes = false;
796
+ let headings = 0;
797
+ for (const { text } of lines) {
798
+ if (fence !== void 0) {
799
+ if (fenceClose(fence).test(text)) fence = void 0;
800
+ continue;
801
+ }
802
+ if (comment) {
803
+ if (text.includes("-->")) comment = false;
804
+ continue;
805
+ }
806
+ if (SEPARATOR.test(text)) return false;
807
+ if (notes) continue;
808
+ if (/^#(?:[ \t]|$)/.test(text)) headings++;
809
+ const open = fenceStart(text);
810
+ if (open) fence = open.fence;
811
+ else if (COMMENT.test(text) && !text.includes("-->", text.indexOf("<!--") + 4)) comment = true;
812
+ else if (NOTES.test(text)) notes = true;
813
+ }
814
+ return headings >= 2;
815
+ }
792
816
  function joinParagraph(lines) {
793
817
  let out = "";
794
818
  lines.forEach((raw, k) => {
@@ -1776,7 +1800,8 @@ function fromMarkdown(markdown, options = {}) {
1776
1800
  body = lines.slice(matter.end);
1777
1801
  }
1778
1802
  const document = { ...options.defaults ?? {}, ...front };
1779
- const segments = splitSegments(body, options.split ?? "rules");
1803
+ const split = options.split ?? "auto";
1804
+ const segments = splitSegments(body, split === "headings" || split === "auto" && isOutline(body) ? "headings" : "rules");
1780
1805
  const slides = [];
1781
1806
  segments.forEach((segment, index) => {
1782
1807
  if (emptySegment(segment)) {