@openpresentation/opf 0.11.4 → 0.12.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 (62) hide show
  1. package/README.md +16 -0
  2. package/dist/audit.d.ts +156 -0
  3. package/dist/audit.js +7 -0
  4. package/dist/{catalogs-CUClB3nl.d.ts → catalogs-u6TEeMcz.d.ts} +1 -1
  5. package/dist/catalogs.d.ts +2 -2
  6. package/dist/catalogs.js +2 -1
  7. package/dist/{chunk-NSVVQ52O.js → chunk-32DIB5BN.js} +8 -6
  8. package/dist/chunk-4FWCY4ZV.js +198 -0
  9. package/dist/{chunk-3H2U7FDR.js → chunk-6TSDIPSH.js} +1861 -247
  10. package/dist/chunk-FYLJVF7X.js +18448 -0
  11. package/dist/{chunk-HJL64ETN.js → chunk-GJKJL4YY.js} +14 -0
  12. package/dist/chunk-JENJZR3F.js +1690 -0
  13. package/dist/{chunk-3OJN7QAW.js → chunk-JVTN3HPP.js} +18 -4
  14. package/dist/{chunk-KJKASEPE.js → chunk-RVN5C2FX.js} +737 -26
  15. package/dist/chunk-S5W34SIJ.js +1 -0
  16. package/dist/chunk-S667QI3M.js +261 -0
  17. package/dist/chunk-UQKJWHSS.js +1332 -0
  18. package/dist/{chunk-RXNFDGPC.js → chunk-WXSPETQ6.js} +311 -40
  19. package/dist/{composition-Dl-d9FRj.d.ts → composition-DWbCiMwF.d.ts} +559 -10
  20. package/dist/composition.d.ts +1 -1
  21. package/dist/composition.js +2 -1
  22. package/dist/convert.d.ts +261 -0
  23. package/dist/convert.js +1107 -0
  24. package/dist/diff.d.ts +145 -0
  25. package/dist/diff.js +604 -0
  26. package/dist/docs.js +68 -14
  27. package/dist/examples.d.ts +1 -1
  28. package/dist/font-policy.d.ts +15 -0
  29. package/dist/font-policy.js +1 -1
  30. package/dist/format.d.ts +20 -0
  31. package/dist/format.js +83 -0
  32. package/dist/index.d.ts +173 -24
  33. package/dist/index.js +926 -262
  34. package/dist/lint.d.ts +4 -4
  35. package/dist/lint.js +6 -5
  36. package/dist/markdown.d.ts +74 -0
  37. package/dist/markdown.js +1941 -0
  38. package/dist/pagination.d.ts +1 -1
  39. package/dist/pagination.js +6 -5
  40. package/dist/patch.d.ts +99 -0
  41. package/dist/patch.js +6 -0
  42. package/dist/{presentation-j5PVrqfV.d.ts → presentation-DRx4hpNZ.d.ts} +451 -16
  43. package/dist/repo-readme.js +1 -1
  44. package/dist/{schemas-X_NniU4A.d.ts → schemas-BSjlG8-R.d.ts} +28 -0
  45. package/dist/schemas.d.ts +1 -1
  46. package/dist/schemas.js +1 -1
  47. package/dist/spec/reference/font-policy.json +311 -40
  48. package/dist/spec/reference/font-policy.schema.json +40 -0
  49. package/dist/spec/reference/symbol-font-encodings.json +18354 -0
  50. package/dist/spec/reference/symbol-font-encodings.schema.json +156 -0
  51. package/dist/spec/schemas/opf.schema.json +591 -22
  52. package/dist/spec-files.d.ts +1 -1
  53. package/dist/spec-files.js +1 -1
  54. package/dist/symbol-font-encodings.d.ts +128 -0
  55. package/dist/symbol-font-encodings.js +1 -0
  56. package/dist/types.d.ts +4 -4
  57. package/dist/validator-bu396ffT.d.ts +172 -0
  58. package/dist/validator.d.ts +4 -36
  59. package/dist/validator.js +5 -4
  60. package/package.json +31 -2
  61. package/dist/chunk-FDCWQHWO.js +0 -1366
  62. /package/dist/{chunk-AEUSVEUY.js → chunk-3ZGSWEHK.js} +0 -0
@@ -1,3 +1,5 @@
1
+ import { catalogs } from './chunk-3ZGSWEHK.js';
2
+
1
3
  // src/table.ts
2
4
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3
5
  function tableGrid(value, path = "table") {
@@ -49,8 +51,8 @@ function dataBytes(uri, limit) {
49
51
  const type = (match[1] ?? "").toLowerCase(), start = match[0].length;
50
52
  try {
51
53
  if (/;base64/i.test(match[2] ?? "")) {
52
- const chars = Math.ceil(limit / 3) * 4, text = uri.slice(start, start + chars + chars / 8).replace(/\s+/g, "").slice(0, chars);
53
- return { type, bytes: Uint8Array.from(atob(text.slice(0, text.length - text.length % 4)), (char) => char.charCodeAt(0)) };
54
+ const chars = Math.ceil(limit / 3) * 4, text2 = uri.slice(start, start + chars + chars / 8).replace(/\s+/g, "").slice(0, chars);
55
+ return { type, bytes: Uint8Array.from(atob(text2.slice(0, text2.length - text2.length % 4)), (char) => char.charCodeAt(0)) };
54
56
  }
55
57
  const prefix = uri.slice(start, start + limit).replace(/%[0-9a-f]?$/i, "");
56
58
  return { type, bytes: new TextEncoder().encode(decodeURIComponent(prefix)) };
@@ -99,8 +101,8 @@ function rasterDimensions(b) {
99
101
  }
100
102
  return void 0;
101
103
  }
102
- function svgDimensions(text) {
103
- const root = /<svg\b([^>]*)>/i.exec(text)?.[1];
104
+ function svgDimensions(text2) {
105
+ const root = /<svg\b([^>]*)>/i.exec(text2)?.[1];
104
106
  if (root === void 0) return void 0;
105
107
  const attribute = (name) => new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`, "i").exec(root)?.[1];
106
108
  const length = (value) => {
@@ -118,7 +120,7 @@ function svgDimensions(text) {
118
120
  }
119
121
  var cache = /* @__PURE__ */ new Map();
120
122
  var CACHE_ENTRIES = 64;
121
- function readAspect(source) {
123
+ function readDimensions(source) {
122
124
  const first = dataBytes(source, PREFIX_BYTES);
123
125
  if (!first) return void 0;
124
126
  let dimensions;
@@ -130,17 +132,540 @@ function readAspect(source) {
130
132
  if (more) dimensions = rasterDimensions(more.bytes);
131
133
  }
132
134
  }
133
- return dimensions && dimensions.width > 0 && dimensions.height > 0 ? dimensions.width / dimensions.height : void 0;
135
+ return dimensions && dimensions.width > 0 && dimensions.height > 0 ? dimensions : void 0;
134
136
  }
135
- function intrinsicImageAspect(value, assets) {
137
+ function intrinsicImageSize(value, assets) {
136
138
  let source = sourceOf(value);
137
139
  for (let hop = 0; source?.startsWith("asset:") && hop < 8; hop++) source = sourceOf(isRecord(assets) ? assets[source.slice(6)] : void 0);
138
140
  if (!source?.startsWith("data:")) return void 0;
139
- if (cache.has(source)) return cache.get(source);
140
- const aspect = readAspect(source);
141
- if (cache.size >= CACHE_ENTRIES) cache.delete(cache.keys().next().value);
142
- cache.set(source, aspect);
143
- return aspect;
141
+ if (!cache.has(source)) {
142
+ if (cache.size >= CACHE_ENTRIES) cache.delete(cache.keys().next().value);
143
+ cache.set(source, readDimensions(source));
144
+ }
145
+ const size = cache.get(source);
146
+ return size ? { ...size, svg: /^data:image\/svg/i.test(source) } : void 0;
147
+ }
148
+ function intrinsicImageAspect(value, assets) {
149
+ const size = intrinsicImageSize(value, assets);
150
+ return size ? size.width / size.height : void 0;
151
+ }
152
+
153
+ // src/reading-order.ts
154
+ function visualReadingOrder(items, direction = "ltr") {
155
+ const index = new Map(items.map((item, i) => [item, i]));
156
+ const along = direction === "rtl" ? (item) => -(item.box.x + item.box.width) : (item) => item.box.x;
157
+ return readingRows(items).flatMap((row) => [...row].sort((a, b) => along(a) - along(b) || a.box.y - b.box.y || index.get(a) - index.get(b)));
158
+ }
159
+ function readingRows(items) {
160
+ const index = new Map(items.map((item, i) => [item, i]));
161
+ const sorted = [...items].sort((a, b) => a.box.y - b.box.y || a.box.x - b.box.x || index.get(a) - index.get(b));
162
+ const rows2 = [];
163
+ for (const item of sorted) {
164
+ const centre = item.box.y + item.box.height / 2;
165
+ const row = rows2.find((candidate) => centre >= candidate.top && centre <= candidate.bottom);
166
+ if (row) {
167
+ row.items.push(item);
168
+ row.top = Math.min(row.top, item.box.y);
169
+ row.bottom = Math.max(row.bottom, item.box.y + item.box.height);
170
+ } else rows2.push({ top: item.box.y, bottom: item.box.y + item.box.height, items: [item] });
171
+ }
172
+ rows2.sort((a, b) => a.top - b.top);
173
+ return rows2.map((row) => row.items.sort((a, b) => index.get(a) - index.get(b)));
174
+ }
175
+
176
+ // src/direction.ts
177
+ 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;
178
+ var strongLtr = /[\u200E\p{L}\p{Nl}]/u;
179
+ function paragraphDirection(text2, deckDirection) {
180
+ if (deckDirection !== "rtl") return "ltr";
181
+ let isolates = 0;
182
+ for (const char of String(text2 ?? "")) {
183
+ if (char === "\u2066" || char === "\u2067" || char === "\u2068") isolates += 1;
184
+ else if (char === "\u2069") isolates = Math.max(0, isolates - 1);
185
+ else if (isolates === 0) {
186
+ if (strongRtl.test(char)) return "rtl";
187
+ if (strongLtr.test(char)) return "ltr";
188
+ }
189
+ }
190
+ return "rtl";
191
+ }
192
+ function physicalAlignment(alignment, direction) {
193
+ const value = alignment ?? "left";
194
+ if (direction !== "rtl" || value === "center") return value;
195
+ return value === "left" ? "right" : "left";
196
+ }
197
+ function paragraphDirectionAt(text2, deckDirection) {
198
+ if (deckDirection !== "rtl") return () => "ltr";
199
+ const source = String(text2 ?? ""), starts = [], directions = [];
200
+ let start = 0;
201
+ const close = (end) => {
202
+ starts.push(start);
203
+ directions.push(paragraphDirection(source.slice(start, end), "rtl"));
204
+ };
205
+ for (const match of source.matchAll(/\r\n|\r|\n/g)) {
206
+ close(match.index);
207
+ start = match.index + match[0].length;
208
+ }
209
+ close(source.length);
210
+ return (offset) => {
211
+ for (let index = starts.length - 1; index >= 0; index--) if (offset >= starts[index]) return directions[index];
212
+ return directions[0];
213
+ };
214
+ }
215
+
216
+ // src/content-walk.ts
217
+ function isRecord2(value) {
218
+ return typeof value === "object" && value !== null && !Array.isArray(value);
219
+ }
220
+ function pathFor(parentPath, key) {
221
+ return parentPath === "/" ? `/${key}` : `${parentPath}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`;
222
+ }
223
+ var bareIdPattern = /^[a-z0-9][a-z0-9-]*$/;
224
+ var promotedRegionKeys = [
225
+ "left",
226
+ "center",
227
+ "right",
228
+ "left+center",
229
+ "center+right",
230
+ "left+center+right",
231
+ "top",
232
+ "middle",
233
+ "bottom",
234
+ "top+middle",
235
+ "middle+bottom",
236
+ "top+middle+bottom",
237
+ "top:left",
238
+ "top:center",
239
+ "top:right",
240
+ "top:left+center",
241
+ "top:center+right",
242
+ "top:left+center+right",
243
+ "middle:left",
244
+ "middle:center",
245
+ "middle:right",
246
+ "middle:left+center",
247
+ "middle:center+right",
248
+ "middle:left+center+right",
249
+ "bottom:left",
250
+ "bottom:center",
251
+ "bottom:right",
252
+ "bottom:left+center",
253
+ "bottom:center+right",
254
+ "bottom:left+center+right",
255
+ "top+middle:left",
256
+ "top+middle:center",
257
+ "top+middle:right",
258
+ "top+middle:left+center",
259
+ "top+middle:center+right",
260
+ "top+middle:left+center+right",
261
+ "middle+bottom:left",
262
+ "middle+bottom:center",
263
+ "middle+bottom:right",
264
+ "middle+bottom:left+center",
265
+ "middle+bottom:center+right",
266
+ "middle+bottom:left+center+right",
267
+ "top+middle+bottom:left",
268
+ "top+middle+bottom:center",
269
+ "top+middle+bottom:right",
270
+ "top+middle+bottom:left+center",
271
+ "top+middle+bottom:center+right",
272
+ "top+middle+bottom:left+center+right"
273
+ ];
274
+ function visitContentPayloads(slide, slidePath, visit) {
275
+ const walk = (payload, path, depth, ancestors) => {
276
+ visit(payload, path);
277
+ if (!Array.isArray(payload.blocks) || depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(payload)) return;
278
+ const nested = [...ancestors, payload];
279
+ const blocksPath = pathFor(path, "blocks");
280
+ payload.blocks.forEach((block, index) => {
281
+ if (isRecord2(block)) walk(block, `${blocksPath}/${index}`, depth + 1, nested);
282
+ });
283
+ };
284
+ if (Array.isArray(slide.blocks)) {
285
+ const blocksPath = pathFor(slidePath, "blocks");
286
+ slide.blocks.forEach((block, index) => {
287
+ if (isRecord2(block)) walk(block, `${blocksPath}/${index}`, 0, []);
288
+ });
289
+ }
290
+ for (const key of promotedRegionKeys) {
291
+ const region = slide[key];
292
+ if (isRecord2(region)) walk(region, pathFor(slidePath, key), 0, []);
293
+ }
294
+ }
295
+
296
+ // src/script-fonts.ts
297
+ var eastAsianScripts = /* @__PURE__ */ new Set(["Hani", "Hans", "Hant", "Hanb", "Jpan", "Kore", "Hang", "Jamo", "Hira", "Kana", "Hrkt", "Bopo", "Yiii"]);
298
+ var complexScripts = /* @__PURE__ */ new Set([
299
+ "Arab",
300
+ "Hebr",
301
+ "Syrc",
302
+ "Thaa",
303
+ "Nkoo",
304
+ "Adlm",
305
+ "Rohg",
306
+ "Mand",
307
+ "Samr",
308
+ "Deva",
309
+ "Beng",
310
+ "Guru",
311
+ "Gujr",
312
+ "Orya",
313
+ "Taml",
314
+ "Telu",
315
+ "Knda",
316
+ "Mlym",
317
+ "Sinh",
318
+ "Thai",
319
+ "Laoo",
320
+ "Tibt",
321
+ "Mymr",
322
+ "Khmr",
323
+ "Mong",
324
+ "Bali",
325
+ "Java",
326
+ "Lana",
327
+ "Tale",
328
+ "Talu",
329
+ "Cakm",
330
+ "Olck"
331
+ ]);
332
+ var rtlScripts = /* @__PURE__ */ new Set(["Arab", "Hebr", "Syrc", "Thaa", "Nkoo", "Adlm", "Rohg", "Mand", "Samr"]);
333
+ var latinSlotScripts = /* @__PURE__ */ new Set(["Latn", "Cyrl", "Grek", "Zyyy", "Zzzz"]);
334
+ var deprecatedLanguages = { iw: "he", in: "id", ji: "yi" };
335
+ var matchingLanguages = { ...deprecatedLanguages, no: "nb", zsm: "ms", ku: "kmr" };
336
+ var likelyScripts = {
337
+ zh: { script: "Hans", regions: { TW: "Hant", HK: "Hant", MO: "Hant" } },
338
+ sr: { script: "Cyrl", regions: { ME: "Latn" } },
339
+ pa: { script: "Guru", regions: { PK: "Arab" } },
340
+ az: { script: "Latn", regions: { IR: "Arab" } },
341
+ uz: { script: "Latn", regions: { AF: "Arab" } },
342
+ bs: { script: "Latn" },
343
+ mn: { script: "Cyrl", regions: { CN: "Mong" } },
344
+ ms: { script: "Latn" }
345
+ };
346
+ var bareLanguageId = /^[a-z][a-z0-9-]*$/;
347
+ function scriptFontRole(script) {
348
+ const code = normalizeScript(script);
349
+ if (code && eastAsianScripts.has(code)) return "eastAsian";
350
+ if (code && complexScripts.has(code)) return "complexScript";
351
+ return "latin";
352
+ }
353
+ function normalizeScript(value) {
354
+ if (typeof value !== "string" || !/^[A-Za-z]{4}$/.test(value)) return void 0;
355
+ return value.slice(0, 1).toUpperCase() + value.slice(1).toLowerCase();
356
+ }
357
+ function text(value) {
358
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
359
+ }
360
+ var tagSyntax = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/;
361
+ function splitTag(tag) {
362
+ if (!tagSyntax.test(tag)) return void 0;
363
+ const [first = "", ...rest] = tag.split("-");
364
+ const language = first.toLowerCase();
365
+ if (language === "und") return void 0;
366
+ let script;
367
+ let region;
368
+ for (const subtag of rest) {
369
+ if (subtag.length === 1) break;
370
+ if (!script && !region && /^[A-Za-z]{4}$/.test(subtag)) script = normalizeScript(subtag);
371
+ else if (!region && /^(?:[A-Za-z]{2}|\d{3})$/.test(subtag)) region = subtag.toUpperCase();
372
+ }
373
+ return { language, script, region };
374
+ }
375
+ function canonicalTag(tag) {
376
+ let extension = false;
377
+ return tag.split("-").map((subtag, index) => {
378
+ const lower = subtag.toLowerCase();
379
+ if (index === 0) return deprecatedLanguages[lower] ?? lower;
380
+ if (extension) return lower;
381
+ if (subtag.length === 1) {
382
+ extension = true;
383
+ return lower;
384
+ }
385
+ if (/^[A-Za-z]{4}$/.test(subtag)) return normalizeScript(subtag);
386
+ if (/^[A-Za-z]{2}$/.test(subtag)) return subtag.toUpperCase();
387
+ return lower;
388
+ }).join("-");
389
+ }
390
+ function vendoredScript(language, region) {
391
+ const entry = likelyScripts[language];
392
+ return entry ? (region && entry.regions?.[region]) ?? entry.script : void 0;
393
+ }
394
+ function inferScript(tag) {
395
+ const parts = splitTag(tag);
396
+ if (!parts) return void 0;
397
+ const known = parts.script ?? vendoredScript(matchingLanguages[parts.language] ?? parts.language, parts.region);
398
+ if (known) return known;
399
+ try {
400
+ return normalizeScript(new Intl.Locale(tag).maximize().script);
401
+ } catch {
402
+ return void 0;
403
+ }
404
+ }
405
+ function inlineRecords(document, kind2) {
406
+ const kinds = isRecord2(document.catalogs) ? document.catalogs : {};
407
+ const entry = kinds[kind2];
408
+ return isRecord2(entry) && Array.isArray(entry.records) ? entry.records.filter(isRecord2) : [];
409
+ }
410
+ function allRecords(document, kind2) {
411
+ return [...inlineRecords(document, kind2), ...catalogs[kind2]];
412
+ }
413
+ function resolveReference(lookup, kind2, reference) {
414
+ if (typeof reference === "string") return lookup(kind2, reference);
415
+ if (!isRecord2(reference)) return void 0;
416
+ const base = typeof reference.id === "string" ? lookup(kind2, reference.id) : void 0;
417
+ return { ...base, ...reference };
418
+ }
419
+ function matchLanguageTag(document, tag) {
420
+ const wanted = splitTag(tag);
421
+ if (!wanted) return void 0;
422
+ const records = allRecords(document, "languages").filter((record5) => typeof record5.bcp47 === "string");
423
+ const key = canonicalTag(tag).toLowerCase();
424
+ const exact = records.find((record5) => canonicalTag(record5.bcp47).toLowerCase() === key);
425
+ if (exact) return exact;
426
+ const language = matchingLanguages[wanted.language] ?? wanted.language;
427
+ const script = wanted.script ?? vendoredScript(language, wanted.region);
428
+ const candidates = records.flatMap((record5) => {
429
+ const parts = splitTag(record5.bcp47);
430
+ if (!parts || (matchingLanguages[parts.language] ?? parts.language) !== language) return [];
431
+ const recordScript = normalizeScript(record5.script) ?? parts.script ?? vendoredScript(language, parts.region);
432
+ if (script && recordScript && script !== recordScript) return [];
433
+ return [{ record: record5, region: parts.region }];
434
+ });
435
+ return candidates.find((candidate) => wanted.region && candidate.region === wanted.region)?.record ?? candidates.find((candidate) => !candidate.region)?.record ?? candidates[0]?.record;
436
+ }
437
+ function resolveLanguage(document, lookup, reference) {
438
+ if (typeof reference === "string") {
439
+ const value = reference.trim();
440
+ const byId = bareLanguageId.test(value) ? lookup("languages", value) : void 0;
441
+ if (byId) return { record: byId, languageId: value };
442
+ if (/^[a-z][a-z0-9+.-]*:/i.test(value) || !splitTag(value)) return void 0;
443
+ const matched = matchLanguageTag(document, value);
444
+ if (matched) return { record: matched, authoredTag: value, languageId: text(matched.id) };
445
+ return inferScript(value) ? { record: {}, authoredTag: value } : void 0;
446
+ }
447
+ if (!isRecord2(reference)) return void 0;
448
+ const authoredTag = text(reference.bcp47);
449
+ if (authoredTag !== void 0 && !splitTag(authoredTag)) return void 0;
450
+ const base = typeof reference.id === "string" ? lookup("languages", reference.id) : authoredTag ? matchLanguageTag(document, authoredTag) : void 0;
451
+ const record5 = { ...base, ...reference };
452
+ const tag = text(record5.bcp47);
453
+ if (!tag || !splitTag(tag)) return void 0;
454
+ return { record: record5, authoredTag, authoredOoxmlLang: text(reference.ooxmlLang), languageId: text(base?.id) };
455
+ }
456
+ function pairFamilies(value) {
457
+ if (!isRecord2(value)) return void 0;
458
+ const major = text(value.major);
459
+ const minor = text(value.minor);
460
+ const heading = major ?? minor;
461
+ const body = minor ?? major;
462
+ return heading && body ? { heading, body } : void 0;
463
+ }
464
+ function schemeServesLanguage(scheme, language) {
465
+ const entries = Array.isArray(scheme.languages) ? scheme.languages.filter((entry) => typeof entry === "string") : [];
466
+ if (entries.length === 0) return true;
467
+ const name = text(language.name)?.toLowerCase();
468
+ if (!name) return false;
469
+ const base = name.replace(/\s*\([^)]*\)\s*$/, "");
470
+ return entries.some((entry) => {
471
+ const value = entry.trim().toLowerCase();
472
+ return value === name || value === base;
473
+ });
474
+ }
475
+ function resolveScriptFonts(input, options = {}) {
476
+ const document = isRecord2(input) ? input : {};
477
+ const lookup = (kind2, id) => inlineRecords(document, kind2).find((record6) => record6.id === id) ?? catalogs[kind2].find((record6) => record6.id === id);
478
+ let design = isRecord2(document.design) ? document.design : {};
479
+ if (options.slideIndex !== void 0) {
480
+ const slides = Array.isArray(document.slides) ? document.slides : [];
481
+ if (!Number.isInteger(options.slideIndex) || options.slideIndex < 0 || options.slideIndex >= slides.length) {
482
+ throw new RangeError(`slideIndex must be an integer between 0 and ${slides.length - 1}.`);
483
+ }
484
+ const slide = slides[options.slideIndex];
485
+ if (isRecord2(slide) && isRecord2(slide.design)) design = { ...design, ...slide.design };
486
+ }
487
+ const theme = resolveReference(lookup, "themes", design.theme ?? "minimal") ?? {};
488
+ const scheme = resolveFontSchemeReference(design.fontScheme ?? theme.fontScheme ?? DEFAULT_FONT_SCHEME, (id) => lookup("fontSchemes", id)).scheme;
489
+ const latin = resolveFontFamilies(scheme);
490
+ const fromOption = options.language !== void 0 ? resolveLanguage(document, lookup, options.language) : void 0;
491
+ const fromDocument = fromOption ? void 0 : resolveLanguage(document, lookup, document.language);
492
+ const requestedDefault = text(options.defaultLanguage);
493
+ const fallbackTag = requestedDefault && splitTag(requestedDefault) ? requestedDefault : "en-US";
494
+ const language = fromOption ?? fromDocument ?? resolveLanguage(document, lookup, fallbackTag) ?? { record: {}, authoredTag: fallbackTag };
495
+ const languageSource = fromOption ? "option" : fromDocument ? "document" : "default";
496
+ const record5 = language.record;
497
+ const bcp47 = canonicalTag(language.authoredTag ?? text(record5.bcp47) ?? fallbackTag);
498
+ const curated = text(record5.ooxmlLang);
499
+ const lang = language.authoredOoxmlLang ? canonicalTag(language.authoredOoxmlLang) : language.authoredTag && splitTag(language.authoredTag)?.region ? canonicalTag(language.authoredTag) : curated ? canonicalTag(curated) : bcp47;
500
+ const parts = splitTag(bcp47);
501
+ const script = normalizeScript(record5.script) ?? (language.languageId ? parts?.script ?? (parts ? vendoredScript(matchingLanguages[parts.language] ?? parts.language, parts.region) : void 0) ?? normalizeScript(matchLanguageTag({}, bcp47)?.script) : inferScript(bcp47)) ?? "Zzzz";
502
+ const scriptRole = scriptFontRole(script);
503
+ const declaredDirection = record5.direction;
504
+ const direction = declaredDirection === "rtl" || declaredDirection === "ltr" ? declaredDirection : rtlScripts.has(script) ? "rtl" : "ltr";
505
+ const app = options.app ?? "PowerPoint";
506
+ const preferred = app === "Google Slides" ? record5.googleFontScheme : record5.fontScheme;
507
+ const alternate = app === "Google Slides" ? record5.fontScheme : record5.googleFontScheme;
508
+ const languageScheme = resolveReference(lookup, "fontSchemes", preferred) ?? resolveReference(lookup, "fontSchemes", alternate);
509
+ const languageFamilies = pairFamilies(languageScheme);
510
+ const schemeAdmitsLanguage = schemeServesLanguage(scheme, record5);
511
+ const slot = (role, family) => {
512
+ const explicit = pairFamilies(scheme[role]);
513
+ if (explicit) return { ...explicit, source: "fontScheme" };
514
+ if (scheme.languageFamily === family && schemeAdmitsLanguage) return { ...latin, source: "schemeFamily" };
515
+ if (scriptRole === role && languageFamilies) return { ...languageFamilies, source: "language" };
516
+ return { heading: latin.heading, body: latin.body, source: "latin" };
517
+ };
518
+ const eastAsian = slot("eastAsian", "ea");
519
+ const complexScript = slot("complexScript", "cs");
520
+ const heading = { latin: latin.heading, eastAsian: eastAsian.heading, complexScript: complexScript.heading };
521
+ const body = { latin: latin.body, eastAsian: eastAsian.body, complexScript: complexScript.body };
522
+ let supplement;
523
+ if (!latinSlotScripts.has(script)) {
524
+ const families = scriptRole === "latin" ? languageFamilies : (scriptRole === "eastAsian" ? eastAsian : complexScript).source === "latin" ? void 0 : { heading: heading[scriptRole], body: body[scriptRole] };
525
+ if (families) supplement = { script: script === "Kore" ? "Hang" : script, heading: families.heading, body: families.body };
526
+ }
527
+ return {
528
+ ...body,
529
+ lang,
530
+ bcp47,
531
+ ...language.languageId ? { languageId: language.languageId } : {},
532
+ languageSource,
533
+ script,
534
+ scriptRole,
535
+ direction,
536
+ rtl: direction === "rtl",
537
+ heading,
538
+ body,
539
+ ...supplement ? { supplement } : {},
540
+ sources: { eastAsian: eastAsian.source, complexScript: complexScript.source }
541
+ };
542
+ }
543
+ function resolveSlideDirection(presentation, slideIndex) {
544
+ if (!isRecord2(presentation) || presentation.language === void 0) return "ltr";
545
+ try {
546
+ return resolveScriptFonts(presentation, Number.isInteger(slideIndex) ? { slideIndex } : {}).direction;
547
+ } catch {
548
+ try {
549
+ return resolveScriptFonts(presentation).direction;
550
+ } catch {
551
+ return "ltr";
552
+ }
553
+ }
554
+ }
555
+
556
+ // src/numbering.ts
557
+ var NUMBERING_STYLES = ["arabic", "roman-upper", "roman-lower", "alpha-upper", "alpha-lower"];
558
+ var NUMBERING_SUFFIXES = ["period", "paren", "paren-both"];
559
+ var MAX_NUMBERING_VALUE = 32767;
560
+ var MAX_ROMAN_VALUE = 3999;
561
+ var MAX_NUMBERING_LEVELS = 9;
562
+ var styleSet = new Set(NUMBERING_STYLES);
563
+ var suffixSet = new Set(NUMBERING_SUFFIXES);
564
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
565
+ function resolveEntry(entry, label) {
566
+ if (typeof entry === "string") {
567
+ if (!styleSet.has(entry)) throw new RangeError(`${label} must be one of ${NUMBERING_STYLES.join(", ")}.`);
568
+ return { style: entry, start: 1, suffix: "period" };
569
+ }
570
+ if (!isObject(entry)) throw new TypeError(`${label} must be a style name or a numbering object.`);
571
+ const { style = "arabic", start = 1, suffix = "period" } = entry;
572
+ if (typeof style !== "string" || !styleSet.has(style)) throw new RangeError(`${label}.style must be one of ${NUMBERING_STYLES.join(", ")}.`);
573
+ if (typeof suffix !== "string" || !suffixSet.has(suffix)) throw new RangeError(`${label}.suffix must be one of ${NUMBERING_SUFFIXES.join(", ")}.`);
574
+ if (typeof start !== "number" || !Number.isInteger(start) || start < 1 || start > MAX_NUMBERING_VALUE) throw new RangeError(`${label}.start must be an integer from 1 to ${MAX_NUMBERING_VALUE}.`);
575
+ return { style, start, suffix };
576
+ }
577
+ function resolveNumbering(input) {
578
+ if (Array.isArray(input)) {
579
+ if (!input.length || input.length > MAX_NUMBERING_LEVELS) throw new RangeError(`numbering must list from 1 to ${MAX_NUMBERING_LEVELS} levels.`);
580
+ return input.map((entry, index) => resolveEntry(entry, `numbering[${index}]`));
581
+ }
582
+ return [resolveEntry(input, "numbering")];
583
+ }
584
+ function numberingAtLevel(levels, level) {
585
+ return levels[Math.min(Math.max(0, level), levels.length - 1)];
586
+ }
587
+ var ROMAN = [[1e3, "m"], [900, "cm"], [500, "d"], [400, "cd"], [100, "c"], [90, "xc"], [50, "l"], [40, "xl"], [10, "x"], [9, "ix"], [5, "v"], [4, "iv"], [1, "i"]];
588
+ function bareNumber(value, style) {
589
+ if (style === "arabic") return String(value);
590
+ if (style === "roman-upper" || style === "roman-lower") {
591
+ if (value > MAX_ROMAN_VALUE) return null;
592
+ let rest = value, out = "";
593
+ for (const [amount, glyphs] of ROMAN) while (rest >= amount) {
594
+ out += glyphs;
595
+ rest -= amount;
596
+ }
597
+ return style === "roman-upper" ? out.toUpperCase() : out;
598
+ }
599
+ const letter = String.fromCharCode(97 + (value - 1) % 26).repeat(Math.floor((value - 1) / 26) + 1);
600
+ return style === "alpha-upper" ? letter.toUpperCase() : letter;
601
+ }
602
+ function numberingStyleDraws(style, value) {
603
+ return bareNumber(value, style) !== null;
604
+ }
605
+ function formatListNumber(value, style = "arabic", suffix = "period") {
606
+ if (!Number.isInteger(value) || value < 1) throw new RangeError("A list number must be an integer of at least 1.");
607
+ if (!styleSet.has(style)) throw new RangeError(`Unknown numbering style '${String(style)}'.`);
608
+ if (!suffixSet.has(suffix)) throw new RangeError(`Unknown numbering suffix '${String(suffix)}'.`);
609
+ const bare = bareNumber(value, style) ?? String(value);
610
+ return suffix === "period" ? `${bare}.` : suffix === "paren" ? `${bare})` : `(${bare})`;
611
+ }
612
+ function listNumbers(items, numbering) {
613
+ const levels = resolveNumbering(numbering);
614
+ const counters = [];
615
+ return items.map((item, index) => {
616
+ const object = isObject(item) ? item : void 0;
617
+ const level = object?.level === void 0 ? 0 : object.level;
618
+ if (typeof level !== "number" || !Number.isInteger(level) || level < 0) throw new RangeError("List levels must be nonnegative integers.");
619
+ const own = object?.start;
620
+ if (own !== void 0 && (typeof own !== "number" || !Number.isInteger(own) || own < 1 || own > MAX_NUMBERING_VALUE)) throw new RangeError(`A list item start must be an integer from 1 to ${MAX_NUMBERING_VALUE}.`);
621
+ const { style: authored, start, suffix } = numberingAtLevel(levels, level);
622
+ const previous = counters[level];
623
+ const value = own ?? (previous === void 0 ? start : previous + 1);
624
+ counters[level] = value;
625
+ counters.length = level + 1;
626
+ const adapted = !numberingStyleDraws(authored, value);
627
+ const style = adapted ? "arabic" : authored;
628
+ return { index, level, value, style, suffix, text: formatListNumber(value, style, suffix), ...adapted ? { adapted: "roman-range" } : {} };
629
+ });
630
+ }
631
+ function sliceNumberedItems(items, numbering, from, to = items.length) {
632
+ const whole = listNumbers(items, numbering);
633
+ const result = items.slice(from, to);
634
+ for (let index = 0; index < result.length; index++) {
635
+ const wanted = whole[from + index].value;
636
+ if (listNumbers(result.slice(0, index + 1), numbering)[index].value === wanted) continue;
637
+ const item = result[index];
638
+ const object = isObject(item) ? item : { text: item };
639
+ result[index] = { ...object, start: wanted };
640
+ }
641
+ return result;
642
+ }
643
+ function numberingFindings(payload) {
644
+ const errors = [], warnings = [];
645
+ const lists = ["items", "bullets"].filter((key) => Array.isArray(payload[key]));
646
+ if (Object.hasOwn(payload, "numbering")) {
647
+ if (!Object.hasOwn(payload, "items") && !Object.hasOwn(payload, "bullets")) {
648
+ errors.push({ key: "numbering", message: "numbering applies to 'items' or 'bullets'; this payload has neither", params: {} });
649
+ } else {
650
+ for (const key of lists) {
651
+ let numbers;
652
+ try {
653
+ numbers = listNumbers(payload[key], payload.numbering);
654
+ } catch {
655
+ continue;
656
+ }
657
+ const largest = numbers.reduce((max, number) => Math.max(max, number.value), 0);
658
+ if (largest > MAX_NUMBERING_VALUE) errors.push({ key, message: `numbering counts up to ${largest}, past the native limit of ${MAX_NUMBERING_VALUE}`, params: { largest, limit: MAX_NUMBERING_VALUE } });
659
+ }
660
+ }
661
+ } else {
662
+ for (const key of lists) {
663
+ if (payload[key].some((item) => isObject(item) && item.start !== void 0)) {
664
+ warnings.push({ key, message: `'start' on ${key} entries has no effect without a 'numbering' field on the payload`, params: {} });
665
+ }
666
+ }
667
+ }
668
+ return { errors, warnings };
144
669
  }
145
670
 
146
671
  // src/furniture-fields.ts
@@ -152,10 +677,10 @@ var tokenWidths = { y: [1, 2, 4], M: [1, 2, 3, 4], d: [1, 2], E: [1, 2, 3, 4] };
152
677
  function parseFurnitureDateFormat(pattern) {
153
678
  if (!pattern) return "dateFormat must not be empty.";
154
679
  const tokens = [];
155
- const literal = (text) => {
680
+ const literal = (text2) => {
156
681
  const last = tokens[tokens.length - 1];
157
- if (last?.kind === "literal") last.text += text;
158
- else tokens.push({ kind: "literal", text });
682
+ if (last?.kind === "literal") last.text += text2;
683
+ else tokens.push({ kind: "literal", text: text2 });
159
684
  };
160
685
  for (let index = 0; index < pattern.length; ) {
161
686
  const character = pattern.charAt(index);
@@ -165,21 +690,21 @@ function parseFurnitureDateFormat(pattern) {
165
690
  index += 2;
166
691
  continue;
167
692
  }
168
- let end = index + 1, text = "";
693
+ let end = index + 1, text2 = "";
169
694
  for (; ; ) {
170
695
  if (end >= pattern.length) return "dateFormat has an unterminated quoted literal.";
171
696
  if (pattern[end] === "'") {
172
697
  if (pattern[end + 1] === "'") {
173
- text += "'";
698
+ text2 += "'";
174
699
  end += 2;
175
700
  continue;
176
701
  }
177
702
  break;
178
703
  }
179
- text += pattern[end];
704
+ text2 += pattern[end];
180
705
  end++;
181
706
  }
182
- literal(text);
707
+ literal(text2);
183
708
  index = end + 1;
184
709
  continue;
185
710
  }
@@ -214,39 +739,39 @@ function formatFurnitureDate(iso, pattern) {
214
739
  const utc = new Date(Date.UTC(2e3, date.month - 1, date.day));
215
740
  utc.setUTCFullYear(date.year);
216
741
  const pad = (value, width) => String(value).padStart(width, "0");
217
- let text = "";
742
+ let text2 = "";
218
743
  for (const token of tokens) {
219
744
  if (token.kind === "literal") {
220
- text += token.text;
745
+ text2 += token.text;
221
746
  continue;
222
747
  }
223
748
  const { letter, width } = token;
224
- if (letter === "y") text += width === 2 ? pad(date.year % 100, 2) : width === 4 ? pad(date.year, 4) : String(date.year);
749
+ if (letter === "y") text2 += width === 2 ? pad(date.year % 100, 2) : width === 4 ? pad(date.year, 4) : String(date.year);
225
750
  else if (letter === "M") {
226
751
  const name = months[date.month - 1] ?? "";
227
- text += width >= 3 ? width === 4 ? name : name.slice(0, 3) : pad(date.month, width);
228
- } else if (letter === "d") text += pad(date.day, width);
752
+ text2 += width >= 3 ? width === 4 ? name : name.slice(0, 3) : pad(date.month, width);
753
+ } else if (letter === "d") text2 += pad(date.day, width);
229
754
  else {
230
755
  const name = weekdays[utc.getUTCDay()] ?? "";
231
- text += width === 4 ? name : name.slice(0, 3);
756
+ text2 += width === 4 ? name : name.slice(0, 3);
232
757
  }
233
758
  }
234
- return { text };
759
+ return { text: text2 };
235
760
  }
236
761
  function formatSlideNumber(format, current, total) {
237
762
  if (!format.includes("{current}")) return { error: "slideNumberFormat must contain {current}." };
238
763
  if (format.includes("{total}") && total === void 0) return { error: "slideNumberFormat uses {total}, which needs the rendered slide count." };
239
- let text = "";
764
+ let text2 = "";
240
765
  const fields2 = [];
241
766
  for (const piece of format.split(/(\{current\}|\{total\})/)) {
242
767
  if (piece === "{current}") {
243
768
  const value = String(current);
244
- fields2.push({ type: "slideNumber", start: text.length, end: text.length + value.length });
245
- text += value;
246
- } else if (piece === "{total}") text += String(total);
247
- else text += piece;
769
+ fields2.push({ type: "slideNumber", start: text2.length, end: text2.length + value.length });
770
+ text2 += value;
771
+ } else if (piece === "{total}") text2 += String(total);
772
+ else text2 += piece;
248
773
  }
249
- return { text, fields: fields2 };
774
+ return { text: text2, fields: fields2 };
250
775
  }
251
776
 
252
777
  // src/color.ts
@@ -358,6 +883,982 @@ function chartColorForFill(fill, preferred) {
358
883
  }
359
884
  return textColorForFill(fill, preferred);
360
885
  }
886
+ var CHART_SERIES_MIN_LIGHTNESS_STEP = 11;
887
+ var CHART_SERIES_MIN_DIFFERENCE = 12;
888
+ var D65 = [0.95047, 1, 1.08883];
889
+ function opaqueChannels(value) {
890
+ if (luminance(value) === void 0) return void 0;
891
+ let hex = value.trim();
892
+ if (hex.length === 4) hex = `#${[...hex.slice(1)].map((c) => c + c).join("")}`;
893
+ return [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16));
894
+ }
895
+ var toLinear = (channel) => {
896
+ const n = channel / 255;
897
+ return n <= 0.04045 ? n / 12.92 : ((n + 0.055) / 1.055) ** 2.4;
898
+ };
899
+ var fromLinear = (value) => 255 * (value <= 31308e-7 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055);
900
+ function labOf(channels) {
901
+ const [r, g, b] = channels.map(toLinear);
902
+ const f = (t) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
903
+ const fx = f((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / D65[0]), fy = f(0.2126729 * r + 0.7151522 * g + 0.072175 * b), fz = f((0.0193339 * r + 0.119192 * g + 0.9503041 * b) / D65[2]);
904
+ return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
905
+ }
906
+ function labToLinear([l, a, b]) {
907
+ const fy = (l + 16) / 116, fx = fy + a / 500, fz = fy - b / 200;
908
+ const inverse = (t) => t ** 3 > 216 / 24389 ? t ** 3 : (116 * t - 16) / (24389 / 27);
909
+ const x = inverse(fx) * D65[0], y = l > 8 ? fy ** 3 : l / (24389 / 27), z = inverse(fz) * D65[2];
910
+ const rgb = [
911
+ 3.2404542 * x - 1.5371385 * y - 0.4985314 * z,
912
+ -0.969266 * x + 1.8760108 * y + 0.041556 * z,
913
+ 0.0556434 * x - 0.2040259 * y + 1.0572252 * z
914
+ ];
915
+ return rgb.every((channel) => channel >= -5e-4 && channel <= 1.0005) ? rgb : void 0;
916
+ }
917
+ var hexOfChannels = (channels) => `#${channels.map((channel) => Math.max(0, Math.min(255, Math.round(channel))).toString(16).padStart(2, "0")).join("").toUpperCase()}`;
918
+ function labWithLightness(l, a, b) {
919
+ const chroma = Math.hypot(a, b), hue = Math.atan2(b, a);
920
+ const at2 = (scale) => labToLinear([l, Math.cos(hue) * chroma * scale, Math.sin(hue) * chroma * scale]);
921
+ let linear = at2(1);
922
+ if (!linear) {
923
+ let low = 0, high = 1;
924
+ for (let step = 0; step < 24; step++) {
925
+ const mid = (low + high) / 2;
926
+ if (at2(mid)) low = mid;
927
+ else high = mid;
928
+ }
929
+ linear = at2(low) ?? [0, 0, 0];
930
+ }
931
+ return hexOfChannels(linear.map((channel) => fromLinear(Math.max(0, Math.min(1, channel)))));
932
+ }
933
+ function labDifference(a, b) {
934
+ return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
935
+ }
936
+ var CHART_SERIES_SHORTFALL_WEIGHT = 8;
937
+ function chartPaletteForFill(fill, palette) {
938
+ const opaqueFill = colorContrast(fill, fill) !== void 0;
939
+ const preferredLab = palette.map((color) => {
940
+ const channels = opaqueChannels(color);
941
+ return channels ? labOf(channels) : void 0;
942
+ });
943
+ const adjusted = palette.map((color) => chartColorForFill(fill, color));
944
+ const kept = palette.map((color, index) => opaqueFill && preferredLab[index] !== void 0 && adjusted[index] === color);
945
+ const placed = palette.map((_, index) => kept[index] || !opaqueFill || !preferredLab[index] ? adjusted[index] : void 0);
946
+ const placedLab = placed.map((color) => {
947
+ const channels = color === void 0 ? void 0 : opaqueChannels(color);
948
+ return channels ? labOf(channels) : void 0;
949
+ });
950
+ palette.forEach((_, index) => {
951
+ const wanted = preferredLab[index], base = adjusted[index];
952
+ if (placed[index] !== void 0 || !wanted) return;
953
+ const baseLab = labOf(opaqueChannels(base));
954
+ const needs = palette.flatMap((_2, other) => {
955
+ const before = preferredLab[other], lab = placedLab[other];
956
+ return other !== index && before && lab && placed[other] !== void 0 ? [{ lab, adjacent: Math.abs(other - index) === 1, wide: Math.min(labDifference(before, wanted), CHART_SERIES_MIN_DIFFERENCE), step: Math.min(Math.abs(before[0] - wanted[0]), CHART_SERIES_MIN_LIGHTNESS_STEP) }] : [];
957
+ });
958
+ const shortfall = (candidate) => needs.reduce((worst, need) => {
959
+ const lightness = Math.abs(need.lab[0] - candidate[0]) - need.step, difference = labDifference(need.lab, candidate) - need.wide;
960
+ return need.adjacent ? { near: Math.min(worst.near, difference, lightness), far: worst.far } : { near: Math.min(worst.near, difference), far: Math.min(worst.far, lightness) };
961
+ }, { near: Infinity, far: Infinity });
962
+ const deficit = (value) => Math.max(0, -value - 1e-9);
963
+ const cost = (candidate) => candidate.moved + CHART_SERIES_SHORTFALL_WEIGHT * deficit(candidate.near) + deficit(candidate.far);
964
+ const start = shortfall(baseLab);
965
+ if (deficit(start.near) === 0 && deficit(start.far) === 0) {
966
+ placed[index] = base;
967
+ placedLab[index] = baseLab;
968
+ return;
969
+ }
970
+ let best = { color: base, lab: baseLab, cost: cost({ moved: 0, ...start }), moved: 0 };
971
+ for (let step = 0; step <= 400; step++) {
972
+ const color = labWithLightness(step / 4, wanted[1], wanted[2]), contrast = colorContrast(color, fill);
973
+ if (contrast === void 0 || contrast < 3) continue;
974
+ const lab = labOf(opaqueChannels(color)), moved = Math.abs(lab[0] - baseLab[0]), total = cost({ moved, ...shortfall(lab) });
975
+ if (total < best.cost - 1e-9 || Math.abs(total - best.cost) <= 1e-9 && moved < best.moved) best = { color, lab, cost: total, moved };
976
+ }
977
+ placed[index] = best.color;
978
+ placedLab[index] = best.lab;
979
+ });
980
+ return placed;
981
+ }
982
+
983
+ // src/annotations.ts
984
+ var CITATION_MARKER_SCALE = 2 / 3;
985
+ var CITATION_MARKER_RAISE = 0.3;
986
+ var CAPTION_FONT_RATIO = 0.6;
987
+ var CAPTION_MAX_RATIO = 0.35;
988
+ var FOOTNOTE_MAX_RATIO = 0.35;
989
+ var CAPTIONABLE_FIELDS = Object.freeze(["image", "chart", "table", "video"]);
990
+ var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
991
+ var isRecord3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
992
+ var round = (value) => Math.round(value * 1e6) / 1e6 || value;
993
+ var regionKey = /^(top|middle|bottom|left|center|right)([+:]|$)/;
994
+ var TEXT_FIELDS = ["text", "items", "bullets"];
995
+ function annotationText(value) {
996
+ if (typeof value === "string") return value;
997
+ if (Array.isArray(value)) return value.map((run) => typeof run === "string" ? run : record2(run).text ?? "").join("");
998
+ if (isRecord3(value) && value.text !== void 0) return annotationText(value.text);
999
+ return value === void 0 || value === null ? "" : String(value);
1000
+ }
1001
+ function captionSettings(value) {
1002
+ if (value === void 0 || value === null) return void 0;
1003
+ if (typeof value === "string" || Array.isArray(value)) return { text: value, position: "below", align: "left" };
1004
+ if (!isRecord3(value) || value.text === void 0) return void 0;
1005
+ return { text: value.text, position: value.position === "above" ? "above" : "below", align: value.align === "center" || value.align === "right" ? value.align : "left" };
1006
+ }
1007
+ function walkCitationRuns(slide, basePath, visit) {
1008
+ const runs = (value2, path) => {
1009
+ if (!Array.isArray(value2)) return;
1010
+ value2.forEach((run, index) => {
1011
+ if (!isRecord3(run) || typeof run.text !== "string") return;
1012
+ const cite = run.cite === void 0 ? [] : Array.isArray(run.cite) ? run.cite.filter((id) => typeof id === "string") : typeof run.cite === "string" ? [run.cite] : [];
1013
+ const footnote = typeof run.footnote === "string" || Array.isArray(run.footnote) ? run.footnote : void 0;
1014
+ if (!cite.length && footnote === void 0) return;
1015
+ visit({ path: `${path}.${index}`, run, cite, footnote });
1016
+ });
1017
+ };
1018
+ const list = (items, path) => {
1019
+ if (!Array.isArray(items)) return;
1020
+ items.forEach((item, index) => {
1021
+ if (Array.isArray(item)) runs(item, `${path}.${index}`);
1022
+ else if (isRecord3(item)) {
1023
+ runs(item.text, `${path}.${index}.text`);
1024
+ runs(item.description, `${path}.${index}.description`);
1025
+ }
1026
+ });
1027
+ };
1028
+ const host = (node, path, depth) => {
1029
+ const payload = record2(node);
1030
+ if (Array.isArray(payload.blocks)) {
1031
+ if (depth > 32) return;
1032
+ payload.blocks.forEach((block, index) => {
1033
+ host(block, `${path}.blocks.${index}`, depth + 1);
1034
+ });
1035
+ return;
1036
+ }
1037
+ for (const field of TEXT_FIELDS) {
1038
+ if (payload[field] === void 0) continue;
1039
+ if (field === "text") runs(payload.text, `${path}.text`);
1040
+ else list(payload[field], `${path}.${field}`);
1041
+ }
1042
+ };
1043
+ const value = record2(slide);
1044
+ const regions = Object.keys(value).filter((key) => regionKey.test(key)).sort();
1045
+ if (regions.length) {
1046
+ for (const key of regions) host(value[key], `${basePath}.${key}`, 0);
1047
+ return;
1048
+ }
1049
+ if (Array.isArray(value.blocks)) {
1050
+ value.blocks.forEach((block, index) => {
1051
+ host(block, `${basePath}.blocks.${index}`, 0);
1052
+ });
1053
+ return;
1054
+ }
1055
+ host(value, basePath, 0);
1056
+ }
1057
+ var citationMarkerText = (numbers) => numbers.join(",");
1058
+ function referenceList(presentation) {
1059
+ const list = record2(presentation).references;
1060
+ return Array.isArray(list) ? list.filter((item) => isRecord3(item) && typeof item.id === "string") : [];
1061
+ }
1062
+ function numberSlide(slide, slideIndex, references, numbering) {
1063
+ const markers = /* @__PURE__ */ new Map(), marked = [], used = /* @__PURE__ */ new Map();
1064
+ walkCitationRuns(slide, `slides.${slideIndex}`, (entry) => {
1065
+ const numbers = [];
1066
+ for (const id of entry.cite) {
1067
+ let note = numbering.byId.get(id);
1068
+ if (!note) {
1069
+ const index = references.findIndex((item) => item.id === id), reference = references[index];
1070
+ note = { number: numbering.notes.length + 1, kind: "reference", id, text: reference?.text ?? id, sourcePath: index >= 0 ? `references.${index}` : entry.path, resolved: !!reference, ...typeof reference?.url === "string" ? { url: reference.url } : {} };
1071
+ numbering.notes.push(note);
1072
+ numbering.byId.set(id, note);
1073
+ }
1074
+ if (!numbers.includes(note.number)) numbers.push(note.number);
1075
+ used.set(note.number, note);
1076
+ }
1077
+ if (entry.footnote !== void 0) {
1078
+ const note = { number: numbering.notes.length + 1, kind: "footnote", text: entry.footnote, sourcePath: entry.path, resolved: true };
1079
+ numbering.notes.push(note);
1080
+ numbers.push(note.number);
1081
+ used.set(note.number, note);
1082
+ }
1083
+ const text2 = citationMarkerText(numbers);
1084
+ markers.set(entry.path, text2);
1085
+ marked.push({ path: entry.path, numbers, text: text2 });
1086
+ });
1087
+ if (!marked.length) return void 0;
1088
+ return { markers, marked, notes: [...used.values()].sort((a, b) => a.number - b.number) };
1089
+ }
1090
+ function collectCitations(presentation) {
1091
+ const references = referenceList(presentation), numbering = { notes: [], byId: /* @__PURE__ */ new Map() }, slides = /* @__PURE__ */ new Map();
1092
+ const list = record2(presentation).slides;
1093
+ if (Array.isArray(list)) list.forEach((slide, index) => {
1094
+ const result = numberSlide(slide, index, references, numbering);
1095
+ if (result) slides.set(index, result);
1096
+ });
1097
+ const cited = new Set(numbering.notes.filter((note) => note.kind === "reference").map((note) => note.id));
1098
+ return { notes: numbering.notes, references: numbering.notes.filter((note) => note.kind === "reference"), slides, unused: references.map((item) => item.id).filter((id) => !cited.has(id)) };
1099
+ }
1100
+ function slideCitations(slide, slideIndex, presentation) {
1101
+ const references = referenceList(presentation), numbering = { notes: [], byId: /* @__PURE__ */ new Map() };
1102
+ const list = record2(presentation).slides;
1103
+ if (Array.isArray(list)) for (let index = 0; index < Math.min(slideIndex, list.length); index += 1) numberSlide(list[index], index, references, numbering);
1104
+ return numberSlide(slide, slideIndex, references, numbering);
1105
+ }
1106
+ var fitHeight = (fit) => Math.max(fit.placement?.height ?? 0, "richLines" in fit ? fit.height : fit.lines.length * fit.lineHeight);
1107
+ function layoutCaption(value, box, path, options) {
1108
+ const settings = captionSettings(value);
1109
+ if (!settings) return void 0;
1110
+ const fontSize = Math.max(options.minFontSize, CAPTION_FONT_RATIO * 25 * options.scale), gap = fontSize * 0.4;
1111
+ const maxHeight = Math.max(fontSize * 1.22, box.height * CAPTION_MAX_RATIO);
1112
+ const diagnostics = [];
1113
+ const probe = options.fit(settings.text, { x: box.x, y: box.y, width: box.width, height: maxHeight }, fontSize, fontSize, path, settings.align);
1114
+ const height = Math.min(maxHeight, Math.max(fontSize * 1.22, fitHeight(probe)));
1115
+ const captionBox = { x: box.x, y: settings.position === "above" ? box.y : box.y + box.height - height, width: box.width, height };
1116
+ const fit = options.fit(settings.text, captionBox, fontSize, fontSize, path, settings.align);
1117
+ const overflow = fit.overflow || fitHeight(fit) > height + 0.01;
1118
+ if (overflow) diagnostics.push({ code: "text-overflow", path, message: "The caption does not fit its band at the minimum font size; shorten it or give the block more space." });
1119
+ const mediaHeight = Math.max(options.scale, box.height - height - gap);
1120
+ const mediaBox = { x: box.x, y: settings.position === "above" ? box.y + height + gap : box.y, width: box.width, height: mediaHeight };
1121
+ for (const target of [captionBox, mediaBox]) for (const key of ["x", "y", "width", "height"]) target[key] = round(target[key]);
1122
+ return { path, value, text: settings.text, position: settings.position, alignment: settings.align, box: captionBox, mediaBox, fit, textStyle: options.textStyle(path), fontSize, overflow, diagnostics };
1123
+ }
1124
+ function layoutFootnotes(citations, options) {
1125
+ const fontSize = Math.max(options.minFontSize, 13 * options.scale), thickness = Math.max(1, round(options.scale));
1126
+ const gap = fontSize * 0.5, spacing = fontSize * 0.25, diagnostics = [];
1127
+ const width = Math.max(options.scale, options.width);
1128
+ const measured = citations.notes.map((note) => {
1129
+ const value = typeof note.text === "string" ? `${note.number} ${note.text}` : [`${note.number} `, ...note.text];
1130
+ const probe = options.fit(value, { x: options.x, y: 0, width, height: Number.MAX_SAFE_INTEGER }, fontSize, fontSize, note.sourcePath, "left");
1131
+ return { note, value, height: Math.max(fontSize * 1.22, fitHeight(probe)) };
1132
+ });
1133
+ const total = thickness + gap + measured.reduce((sum, item) => sum + item.height, 0) + spacing * Math.max(0, measured.length - 1);
1134
+ const height = round(Math.min(total, Math.max(fontSize * 1.22 + thickness + gap, options.maxHeight)));
1135
+ const top = round(options.bottom - height), box = { x: round(options.x), y: top, width: round(width), height };
1136
+ let cursor = top + thickness + gap, overflow = false;
1137
+ const entries = measured.map((item, index) => {
1138
+ const entryBox = { x: box.x, y: round(cursor), width: box.width, height: round(item.height) };
1139
+ const fit = options.fit(item.value, entryBox, fontSize, fontSize, item.note.sourcePath, "left");
1140
+ const exceeds = fit.overflow || entryBox.y + entryBox.height > options.bottom + 0.01;
1141
+ if (exceeds) {
1142
+ overflow = true;
1143
+ diagnostics.push({ code: "text-overflow", path: item.note.sourcePath, message: `Footnote ${item.note.number} does not fit the slide's footnote area; shorten the note or split the slide.` });
1144
+ }
1145
+ cursor += item.height + (index < measured.length - 1 ? spacing : 0);
1146
+ return { number: item.note.number, kind: item.note.kind, ...item.note.id !== void 0 ? { id: item.note.id } : {}, sourcePath: item.note.sourcePath, value: item.value, text: item.note.text, box: entryBox, fit, textStyle: options.textStyle(item.note.sourcePath), ...item.note.url !== void 0 ? { url: item.note.url } : {}, overflow: exceeds };
1147
+ });
1148
+ for (const note of citations.notes) if (!note.resolved) diagnostics.push({ code: "unresolved-content", path: note.sourcePath, message: `Reference '${note.id}' is not in the presentation's references list; its id is listed instead.` });
1149
+ return { algorithm: "footnote-area-v1", path: options.path, box, rule: { x: box.x, y: top, width: box.width, thickness }, entries, markers: citations.marked, fontSize, overflow, diagnostics };
1150
+ }
1151
+ function referencesSlide(presentation, options = {}) {
1152
+ const title = options.title ?? "References";
1153
+ const { references } = collectCitations(presentation);
1154
+ if (!references.length) return { title };
1155
+ const items = references.map((note) => {
1156
+ const label = `${note.number}. `;
1157
+ if (typeof note.text === "string" && note.url === void 0) return `${label}${note.text}`;
1158
+ const runs = [label, ...typeof note.text === "string" ? [note.text] : note.text];
1159
+ if (note.url !== void 0) runs.push(" ", { text: note.url, link: note.url });
1160
+ return runs;
1161
+ });
1162
+ return { title, items };
1163
+ }
1164
+
1165
+ // src/generated/catalog-ids.ts
1166
+ var catalogIds = {
1167
+ audiences: [
1168
+ "executives",
1169
+ "board",
1170
+ "engineering-team",
1171
+ "investors",
1172
+ "customers",
1173
+ "sales-team",
1174
+ "marketing-team",
1175
+ "all-hands",
1176
+ "candidates",
1177
+ "regulators",
1178
+ "executive",
1179
+ "investor",
1180
+ "technical",
1181
+ "sales",
1182
+ "marketing",
1183
+ "academic",
1184
+ "internal-team",
1185
+ "customer",
1186
+ "general-public",
1187
+ "media",
1188
+ "partner",
1189
+ "regulatory"
1190
+ ],
1191
+ purposes: [
1192
+ "inform",
1193
+ "decide",
1194
+ "align",
1195
+ "persuade",
1196
+ "educate",
1197
+ "report",
1198
+ "pitch",
1199
+ "sell",
1200
+ "plan"
1201
+ ],
1202
+ tones: [
1203
+ "formal",
1204
+ "casual",
1205
+ "inspirational",
1206
+ "technical",
1207
+ "persuasive",
1208
+ "authoritative",
1209
+ "conversational"
1210
+ ],
1211
+ themes: [
1212
+ "minimal",
1213
+ "classic",
1214
+ "dark",
1215
+ "bold"
1216
+ ],
1217
+ layouts: [
1218
+ "blank",
1219
+ "chart-1x",
1220
+ "chart-2x",
1221
+ "chart-3x",
1222
+ "code-1x",
1223
+ "image-1x",
1224
+ "image-2x",
1225
+ "image-3x",
1226
+ "image-bleed",
1227
+ "list-1x",
1228
+ "list-2x",
1229
+ "list-3x",
1230
+ "list-4x",
1231
+ "list-5x",
1232
+ "list-6x",
1233
+ "media-1x",
1234
+ "number-1x",
1235
+ "number-2x",
1236
+ "number-3x",
1237
+ "number-4x",
1238
+ "number-5x",
1239
+ "number-6x",
1240
+ "quote-1x",
1241
+ "table-1x",
1242
+ "text-1x",
1243
+ "text-2x",
1244
+ "text-3x",
1245
+ "timeline-1x",
1246
+ "title",
1247
+ "title-subtitle",
1248
+ "title-slide",
1249
+ "two-column",
1250
+ "image-focus",
1251
+ "data-visualization",
1252
+ "quote-highlight",
1253
+ "section-divider",
1254
+ "agenda",
1255
+ "thank-you-cta",
1256
+ "bullet-list",
1257
+ "three-column",
1258
+ "icon-grid",
1259
+ "faq",
1260
+ "stats-metrics",
1261
+ "comparison",
1262
+ "swot-analysis",
1263
+ "matrix-quadrant",
1264
+ "funnel-pyramid",
1265
+ "timeline",
1266
+ "process-flow",
1267
+ "photo-grid",
1268
+ "before-after",
1269
+ "testimonial-grid",
1270
+ "team-bio",
1271
+ "pricing-table",
1272
+ "roadmap",
1273
+ "product-feature",
1274
+ "executive-summary",
1275
+ "key-takeaway",
1276
+ "chapter-recap",
1277
+ "qa-discussion",
1278
+ "single-message-statement",
1279
+ "one-column-explainer",
1280
+ "case-study",
1281
+ "pros-cons",
1282
+ "feature-comparison-matrix",
1283
+ "venn-overlap",
1284
+ "decision-tree",
1285
+ "kpi-scorecard",
1286
+ "dashboard",
1287
+ "waterfall-bridge",
1288
+ "benchmark-target-table",
1289
+ "heatmap-cohort",
1290
+ "risk-heatmap",
1291
+ "forecast-scenario",
1292
+ "swimlane-process",
1293
+ "gantt-project-plan",
1294
+ "circular-lifecycle",
1295
+ "customer-journey",
1296
+ "org-chart",
1297
+ "capability-map",
1298
+ "logic-tree",
1299
+ "hub-and-spoke",
1300
+ "architecture-stack",
1301
+ "ecosystem-map",
1302
+ "network-dependency-map",
1303
+ "wheel-radial",
1304
+ "geographic-map",
1305
+ "screenshot-walkthrough",
1306
+ "market-size",
1307
+ "persona-grid",
1308
+ "competitive-landscape",
1309
+ "business-model-canvas",
1310
+ "customer-logos",
1311
+ "executive-decision",
1312
+ "status-rag",
1313
+ "raci-matrix",
1314
+ "action-plan",
1315
+ "assumptions-dependencies",
1316
+ "appendix-index",
1317
+ "detailed-data-table"
1318
+ ],
1319
+ chartTypes: [
1320
+ "100pct-stacked-area-3x",
1321
+ "100pct-stacked-bar-3x",
1322
+ "100pct-stacked-column-3x",
1323
+ "area",
1324
+ "bar",
1325
+ "box-and-whisker",
1326
+ "column",
1327
+ "doughnut",
1328
+ "filled-radar",
1329
+ "funnel",
1330
+ "histogram",
1331
+ "line",
1332
+ "line-with-markers",
1333
+ "pareto",
1334
+ "pie",
1335
+ "radar",
1336
+ "radar-with-markers",
1337
+ "scatter",
1338
+ "stacked-area-3x",
1339
+ "stacked-bar-3x",
1340
+ "stacked-column-3x",
1341
+ "stacked-line-3x",
1342
+ "stacked-line-with-markers-3x",
1343
+ "treemap",
1344
+ "waterfall",
1345
+ "world",
1346
+ "100pct-bullet-bar",
1347
+ "100pct-bullet-bar-2x",
1348
+ "100pct-bullet-bar-3x",
1349
+ "100pct-bullet-column",
1350
+ "100pct-bullet-column-2x",
1351
+ "100pct-bullet-column-3x",
1352
+ "100pct-progress-bar",
1353
+ "100pct-stacked-area-2x",
1354
+ "100pct-stacked-bar-2x",
1355
+ "100pct-stacked-column-2x",
1356
+ "australia",
1357
+ "box-and-whisker-2x",
1358
+ "box-and-whisker-3x",
1359
+ "bullet-bar",
1360
+ "bullet-bar-2x",
1361
+ "bullet-bar-3x",
1362
+ "bullet-column",
1363
+ "bullet-column-2x",
1364
+ "bullet-column-3x",
1365
+ "canada",
1366
+ "clustered-bar-2x",
1367
+ "clustered-column",
1368
+ "dot-plot",
1369
+ "dot-plot-2x",
1370
+ "dot-plot-3x",
1371
+ "dot-plot-4x",
1372
+ "dot-plot-5x",
1373
+ "dot-plot-6x",
1374
+ "dumbbell",
1375
+ "line-2x",
1376
+ "line-3x",
1377
+ "line-with-high-low",
1378
+ "line-with-high-low-and-markers",
1379
+ "line-with-markers-2x",
1380
+ "line-with-markers-3x",
1381
+ "sparkline",
1382
+ "sparkline-2x",
1383
+ "sparkline-3x",
1384
+ "sparkline-4x",
1385
+ "sparkline-5x",
1386
+ "sparkline-6x",
1387
+ "stacked-area-2x",
1388
+ "stacked-bar-2x",
1389
+ "stacked-column-2x",
1390
+ "stacked-line-2x",
1391
+ "stacked-line-with-markers-2x",
1392
+ "treemap-2x",
1393
+ "treemap-3x",
1394
+ "united-kingdom",
1395
+ "united-states"
1396
+ ],
1397
+ narratives: [
1398
+ "problem-solution",
1399
+ "scqa",
1400
+ "pyramid-principle",
1401
+ "situation-complication-resolution",
1402
+ "what-so-what-now-what",
1403
+ "strategic-narrative",
1404
+ "golden-circle",
1405
+ "conference-talk",
1406
+ "transformation-arc",
1407
+ "pitch-deck",
1408
+ "early-startup-pitch",
1409
+ "venture-pitch",
1410
+ "persuasive-sales",
1411
+ "sparkline",
1412
+ "company-intro",
1413
+ "marketing-strategy",
1414
+ "product-launch",
1415
+ "strategic-advisory",
1416
+ "challenge-resolution",
1417
+ "project-proposal",
1418
+ "vision-roadmap",
1419
+ "failure-analysis",
1420
+ "star-method",
1421
+ "classic-story",
1422
+ "heros-journey",
1423
+ "business-narrative",
1424
+ "change-story",
1425
+ "rags-to-riches",
1426
+ "underdog-victory",
1427
+ "survival-story",
1428
+ "educate",
1429
+ "persuade",
1430
+ "reveal",
1431
+ "justice",
1432
+ "innovation",
1433
+ "focus",
1434
+ "qbr",
1435
+ "business-review",
1436
+ "board-meeting",
1437
+ "weekly-progress",
1438
+ "status-update",
1439
+ "performance-review",
1440
+ "survey-analysis",
1441
+ "trend-analysis",
1442
+ "data-story",
1443
+ "employee-review",
1444
+ "performance-improvement-plan",
1445
+ "capacity-planning"
1446
+ ],
1447
+ socialPlatforms: [
1448
+ "linkedin",
1449
+ "x",
1450
+ "github",
1451
+ "youtube",
1452
+ "instagram",
1453
+ "facebook",
1454
+ "tiktok",
1455
+ "threads",
1456
+ "mastodon",
1457
+ "bluesky"
1458
+ ],
1459
+ languages: [
1460
+ "afrikaans",
1461
+ "albanian",
1462
+ "amharic",
1463
+ "arabic",
1464
+ "armenian",
1465
+ "aymara",
1466
+ "azerbaijani",
1467
+ "bengali",
1468
+ "berber-latin",
1469
+ "bosnian-latin",
1470
+ "bulgarian",
1471
+ "catalan",
1472
+ "cebuano",
1473
+ "chinese-simplified",
1474
+ "chinese-traditional",
1475
+ "chittagonian",
1476
+ "croatian",
1477
+ "czech",
1478
+ "danish",
1479
+ "dutch",
1480
+ "english",
1481
+ "english-au",
1482
+ "english-ca",
1483
+ "english-gb",
1484
+ "english-in",
1485
+ "english-us",
1486
+ "estonian",
1487
+ "filipino",
1488
+ "finnish",
1489
+ "french",
1490
+ "fulfulde",
1491
+ "galician",
1492
+ "georgian",
1493
+ "german",
1494
+ "greek",
1495
+ "gujarati",
1496
+ "hausa",
1497
+ "hebrew",
1498
+ "hindi",
1499
+ "hungarian",
1500
+ "igbo",
1501
+ "indonesian",
1502
+ "italian",
1503
+ "japanese",
1504
+ "kannada",
1505
+ "kazakh",
1506
+ "khmer",
1507
+ "kinyarwanda",
1508
+ "korean",
1509
+ "kurmanji",
1510
+ "latvian",
1511
+ "lithuanian",
1512
+ "macedonian",
1513
+ "malagasy",
1514
+ "malay",
1515
+ "malayalam",
1516
+ "maori",
1517
+ "marathi",
1518
+ "mongolian",
1519
+ "nepali",
1520
+ "norwegian",
1521
+ "odia",
1522
+ "oromo",
1523
+ "pashto",
1524
+ "persian",
1525
+ "polish",
1526
+ "portuguese",
1527
+ "punjabi-gurmukhi",
1528
+ "punjabi-shahmukhi",
1529
+ "romanian",
1530
+ "russian",
1531
+ "serbian-cyrillic",
1532
+ "serbian-latin",
1533
+ "shona",
1534
+ "slovak",
1535
+ "slovenian",
1536
+ "somali",
1537
+ "spanish",
1538
+ "swahili",
1539
+ "swedish",
1540
+ "tagalog",
1541
+ "tajik",
1542
+ "tamil",
1543
+ "telugu",
1544
+ "thai",
1545
+ "turkish",
1546
+ "ukrainian",
1547
+ "urdu",
1548
+ "uzbek-latin",
1549
+ "vietnamese-quoc-ngu",
1550
+ "xhosa",
1551
+ "yoruba",
1552
+ "zulu"
1553
+ ],
1554
+ colorSchemes: [
1555
+ "black-and-white",
1556
+ "bold-red",
1557
+ "boost",
1558
+ "burnt-orange",
1559
+ "cool-horizon",
1560
+ "corporate-blue",
1561
+ "deep-purple",
1562
+ "forest-green",
1563
+ "golden-yellow",
1564
+ "luxury",
1565
+ "pastel-red",
1566
+ "slate-gray",
1567
+ "steel-blue",
1568
+ "vibes"
1569
+ ],
1570
+ fontSchemes: [
1571
+ "consolas",
1572
+ "courier-new",
1573
+ "arial",
1574
+ "calibri",
1575
+ "impact",
1576
+ "lucida-sans",
1577
+ "segoe-ui",
1578
+ "segoe-ui-light",
1579
+ "tahoma",
1580
+ "trebuchet-ms",
1581
+ "verdana",
1582
+ "bookman",
1583
+ "century-schoolbook",
1584
+ "constantia",
1585
+ "garamond",
1586
+ "rockwell",
1587
+ "times-new-roman",
1588
+ "aptos",
1589
+ "grandview",
1590
+ "seaford",
1591
+ "skeena",
1592
+ "tenorite",
1593
+ "georgia",
1594
+ "microsoft-yahei",
1595
+ "simsun",
1596
+ "fangsong",
1597
+ "yu-gothic",
1598
+ "pmingliu",
1599
+ "mingliu",
1600
+ "ms-mincho",
1601
+ "malgun-gothic",
1602
+ "batang",
1603
+ "gungsuh",
1604
+ "mangal",
1605
+ "nirmala-ui",
1606
+ "latha",
1607
+ "david",
1608
+ "miriam",
1609
+ "gisha",
1610
+ "vrinda",
1611
+ "shruti",
1612
+ "tunga",
1613
+ "kartika",
1614
+ "kalinga",
1615
+ "raavi",
1616
+ "gautami",
1617
+ "arabic-typesetting",
1618
+ "microsoft-jhenghei",
1619
+ "meiryo",
1620
+ "angsana-new",
1621
+ "aparajita",
1622
+ "dilleniaupc",
1623
+ "sakkal-majalla",
1624
+ "shonar-bangla",
1625
+ "sylfaen",
1626
+ "traditional-arabic",
1627
+ "montserrat",
1628
+ "open-sans",
1629
+ "poppins",
1630
+ "pt-serif",
1631
+ "raleway",
1632
+ "roboto",
1633
+ "noto-sans-arabic",
1634
+ "noto-sans-armenian",
1635
+ "noto-sans-bengali",
1636
+ "noto-sans-devangari",
1637
+ "noto-sans-georgian",
1638
+ "noto-sans-gujarati",
1639
+ "noto-sans-gurmukhi",
1640
+ "noto-sans-hebrew",
1641
+ "noto-sans-jp",
1642
+ "noto-sans-kannada",
1643
+ "noto-sans-kr",
1644
+ "noto-sans-malayalam",
1645
+ "noto-sans-mongolian",
1646
+ "noto-sans-oriya",
1647
+ "noto-sans-sc",
1648
+ "noto-sans-tamil",
1649
+ "noto-sans-tc",
1650
+ "noto-sans-telugu",
1651
+ "noto-sans",
1652
+ "noto-sans-thai",
1653
+ "noto-nastaliq-urdu",
1654
+ "noto-naksh-arabic",
1655
+ "noto-sans-ethiopic",
1656
+ "noto-sans-khmer",
1657
+ "nyala",
1658
+ "daunpenh",
1659
+ "khmer-ui"
1660
+ ]
1661
+ };
1662
+ var deprecatedCatalogIds = {
1663
+ "chartTypes/100pct-bullet-bar": "100pct-stacked-bar-3x",
1664
+ "chartTypes/100pct-bullet-bar-2x": "100pct-stacked-bar-3x",
1665
+ "chartTypes/100pct-bullet-bar-3x": "100pct-stacked-bar-3x",
1666
+ "chartTypes/100pct-bullet-column": "100pct-stacked-column-3x",
1667
+ "chartTypes/100pct-bullet-column-2x": "100pct-stacked-column-3x",
1668
+ "chartTypes/100pct-bullet-column-3x": "100pct-stacked-column-3x",
1669
+ "chartTypes/100pct-progress-bar": "100pct-stacked-bar-3x",
1670
+ "chartTypes/100pct-stacked-area-2x": "100pct-stacked-area-3x",
1671
+ "chartTypes/100pct-stacked-bar-2x": "100pct-stacked-bar-3x",
1672
+ "chartTypes/100pct-stacked-column-2x": "100pct-stacked-column-3x",
1673
+ "chartTypes/australia": "world",
1674
+ "chartTypes/box-and-whisker-2x": "box-and-whisker",
1675
+ "chartTypes/box-and-whisker-3x": "box-and-whisker",
1676
+ "chartTypes/bullet-bar": "bar",
1677
+ "chartTypes/bullet-bar-2x": "bar",
1678
+ "chartTypes/bullet-bar-3x": "bar",
1679
+ "chartTypes/bullet-column": "column",
1680
+ "chartTypes/bullet-column-2x": "column",
1681
+ "chartTypes/bullet-column-3x": "column",
1682
+ "chartTypes/canada": "world",
1683
+ "chartTypes/clustered-bar-2x": "bar",
1684
+ "chartTypes/clustered-column": "column",
1685
+ "chartTypes/dot-plot": "scatter",
1686
+ "chartTypes/dot-plot-2x": "scatter",
1687
+ "chartTypes/dot-plot-3x": "scatter",
1688
+ "chartTypes/dot-plot-4x": "scatter",
1689
+ "chartTypes/dot-plot-5x": "scatter",
1690
+ "chartTypes/dot-plot-6x": "scatter",
1691
+ "chartTypes/dumbbell": "scatter",
1692
+ "chartTypes/line-2x": "line",
1693
+ "chartTypes/line-3x": "line",
1694
+ "chartTypes/line-with-high-low": "line",
1695
+ "chartTypes/line-with-high-low-and-markers": "line-with-markers",
1696
+ "chartTypes/line-with-markers-2x": "line-with-markers",
1697
+ "chartTypes/line-with-markers-3x": "line-with-markers",
1698
+ "chartTypes/sparkline": "line",
1699
+ "chartTypes/sparkline-2x": "line",
1700
+ "chartTypes/sparkline-3x": "line",
1701
+ "chartTypes/sparkline-4x": "line",
1702
+ "chartTypes/sparkline-5x": "line",
1703
+ "chartTypes/sparkline-6x": "line",
1704
+ "chartTypes/stacked-area-2x": "stacked-area-3x",
1705
+ "chartTypes/stacked-bar-2x": "stacked-bar-3x",
1706
+ "chartTypes/stacked-column-2x": "stacked-column-3x",
1707
+ "chartTypes/stacked-line-2x": "stacked-line-3x",
1708
+ "chartTypes/stacked-line-with-markers-2x": "stacked-line-with-markers-3x",
1709
+ "chartTypes/treemap-2x": "treemap",
1710
+ "chartTypes/treemap-3x": "treemap",
1711
+ "chartTypes/united-kingdom": "world",
1712
+ "chartTypes/united-states": "world"
1713
+ };
1714
+
1715
+ // src/chart-options.ts
1716
+ var END_POSITIONS = ["center", "inside-end", "inside-base", "outside-end"];
1717
+ var STACKED_POSITIONS = ["center", "inside-end", "inside-base"];
1718
+ var POINT_POSITIONS = ["above", "below", "left", "right", "center"];
1719
+ var PIE_POSITIONS = ["center", "inside-end", "outside-end"];
1720
+ var NONE = [];
1721
+ var VALUE_CATEGORY = ["category", "value"];
1722
+ var ALL_CONTENT = ["category", "value", "percent"];
1723
+ var categoryKinds = /* @__PURE__ */ new Set(["bar", "line", "area", "scatter", "histogram", "pareto", "box", "waterfall", "funnel"]);
1724
+ var valueKinds = /* @__PURE__ */ new Set(["bar", "line", "area", "scatter", "histogram", "pareto", "box", "waterfall"]);
1725
+ var legendKinds = /* @__PURE__ */ new Set(["bar", "line", "area", "pie", "doughnut", "scatter", "radar", "box"]);
1726
+ function labelSupport(target) {
1727
+ const none = { supported: true, content: VALUE_CATEGORY, positions: NONE, defaultPosition: null, defaultOn: false };
1728
+ switch (target.kind) {
1729
+ case "bar":
1730
+ return target.stacked ? { ...none, positions: STACKED_POSITIONS, defaultPosition: "center" } : { ...none, positions: END_POSITIONS, defaultPosition: "outside-end" };
1731
+ case "line":
1732
+ case "scatter":
1733
+ return { ...none, positions: POINT_POSITIONS, defaultPosition: "above" };
1734
+ case "pie":
1735
+ return { ...none, content: ALL_CONTENT, positions: PIE_POSITIONS, defaultPosition: "outside-end" };
1736
+ case "doughnut":
1737
+ return { ...none, content: ALL_CONTENT };
1738
+ // The funnel labels its bars with values and the treemap its tiles with category names by default.
1739
+ case "funnel":
1740
+ case "treemap":
1741
+ return { ...none, defaultOn: true };
1742
+ case "area":
1743
+ case "radar":
1744
+ return none;
1745
+ case "histogram":
1746
+ case "pareto":
1747
+ case "waterfall":
1748
+ return { ...none, positions: END_POSITIONS, defaultPosition: "outside-end" };
1749
+ default:
1750
+ return { ...none, supported: false, content: [] };
1751
+ }
1752
+ }
1753
+ function chartOptionSupport(target) {
1754
+ return {
1755
+ axisTitles: { category: categoryKinds.has(target.kind), value: valueKinds.has(target.kind) },
1756
+ legend: legendKinds.has(target.kind),
1757
+ dataLabels: labelSupport(target)
1758
+ };
1759
+ }
1760
+ var KEPT = {
1761
+ column: { kind: "bar" },
1762
+ "stacked-column-3x": { kind: "bar", stacked: true },
1763
+ "100pct-stacked-column-3x": { kind: "bar", stacked: true },
1764
+ bar: { kind: "bar" },
1765
+ "stacked-bar-3x": { kind: "bar", stacked: true },
1766
+ "100pct-stacked-bar-3x": { kind: "bar", stacked: true },
1767
+ line: { kind: "line" },
1768
+ "line-with-markers": { kind: "line" },
1769
+ "stacked-line-3x": { kind: "line", stacked: true },
1770
+ "stacked-line-with-markers-3x": { kind: "line", stacked: true },
1771
+ area: { kind: "area" },
1772
+ "stacked-area-3x": { kind: "area", stacked: true },
1773
+ "100pct-stacked-area-3x": { kind: "area", stacked: true },
1774
+ pie: { kind: "pie" },
1775
+ doughnut: { kind: "doughnut" },
1776
+ scatter: { kind: "scatter" },
1777
+ radar: { kind: "radar" },
1778
+ "radar-with-markers": { kind: "radar" },
1779
+ "filled-radar": { kind: "radar" },
1780
+ treemap: { kind: "treemap" },
1781
+ histogram: { kind: "histogram" },
1782
+ pareto: { kind: "pareto" },
1783
+ "box-and-whisker": { kind: "box" },
1784
+ waterfall: { kind: "waterfall" },
1785
+ funnel: { kind: "funnel" },
1786
+ world: { kind: "map" }
1787
+ };
1788
+ function chartOptionTarget(typeId) {
1789
+ if (typeof typeId !== "string") return void 0;
1790
+ const raw = typeId.trim().toLowerCase();
1791
+ const id = raw === "donut" ? "doughnut" : raw;
1792
+ const kept = deprecatedCatalogIds[`chartTypes/${id}`] ?? id;
1793
+ const target = Object.hasOwn(KEPT, kept) ? KEPT[kept] : void 0;
1794
+ return target ? { ...target } : void 0;
1795
+ }
1796
+ var record3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1797
+ var LEGEND_POSITIONS = /* @__PURE__ */ new Set(["none", "top", "bottom", "left", "right"]);
1798
+ var CONTENT_ORDER = ["category", "value", "percent"];
1799
+ var POSITIONS = /* @__PURE__ */ new Set(["auto", "center", "inside-end", "inside-base", "outside-end", "above", "below", "left", "right"]);
1800
+ var DEFAULT_CHART_LABEL_SEPARATOR = ", ";
1801
+ function resolveChartOptions(chart, target) {
1802
+ const result = { active: false, axisTitles: {}, diagnostics: [] };
1803
+ if (!record3(chart)) return result;
1804
+ const support = target ? chartOptionSupport(target) : void 0;
1805
+ const adapt = (option, reason, message) => result.diagnostics.push({ code: "chart-option-adapted", option, reason, message });
1806
+ const kind2 = target?.kind ?? "unknown";
1807
+ if (record3(chart.axisTitles)) {
1808
+ for (const axis of ["category", "value"]) {
1809
+ const raw = chart.axisTitles[axis];
1810
+ if (typeof raw !== "string" || !raw.trim()) continue;
1811
+ if (support && !support.axisTitles[axis]) {
1812
+ adapt(`axisTitles.${axis}`, "unsupported-type", `A '${kind2}' chart has no ${axis} axis to title; the ${axis} axis title is not drawn or exported.`);
1813
+ continue;
1814
+ }
1815
+ result.axisTitles[axis] = raw.trim();
1816
+ }
1817
+ }
1818
+ if (typeof chart.legend === "string" && LEGEND_POSITIONS.has(chart.legend)) {
1819
+ if (support && !support.legend) adapt("legend", "unsupported-type", `A '${kind2}' chart has no legend option; the legend setting is ignored.`);
1820
+ else result.legend = chart.legend;
1821
+ }
1822
+ const labels = chart.dataLabels;
1823
+ if (labels === false && support?.dataLabels.defaultOn) result.dataLabelsOff = true;
1824
+ if (labels === true || record3(labels)) {
1825
+ const options = record3(labels) ? labels : {};
1826
+ if (support && !support.dataLabels.supported) {
1827
+ adapt("dataLabels", "unsupported-type", `A '${kind2}' chart has no data label option; data labels are not drawn or exported.`);
1828
+ } else {
1829
+ const allowed = support?.dataLabels.content ?? ALL_CONTENT;
1830
+ const requested = Array.isArray(options.content) ? options.content.filter((part) => typeof part === "string" && CONTENT_ORDER.includes(part)) : ["value"];
1831
+ const content = CONTENT_ORDER.filter((part) => requested.includes(part) && allowed.includes(part));
1832
+ for (const part of CONTENT_ORDER) {
1833
+ if (requested.includes(part) && !allowed.includes(part)) adapt("dataLabels.content", "unsupported-content", `A '${kind2}' chart cannot label '${part}'${part === "percent" ? " (percent exists only on pie and doughnut charts)" : ""}; it is dropped from the data labels.`);
1834
+ }
1835
+ if (!content.length) content.push("value");
1836
+ const positions = support?.dataLabels.positions ?? NONE;
1837
+ let position = support?.dataLabels.defaultPosition ?? null;
1838
+ if (typeof options.position === "string" && POSITIONS.has(options.position) && options.position !== "auto") {
1839
+ if (!support || positions.includes(options.position)) position = options.position;
1840
+ else adapt("dataLabels.position", "unsupported-position", `A '${kind2}'${target?.stacked ? " stacked" : ""} chart cannot place data labels '${options.position}'; the type's default position is used.`);
1841
+ }
1842
+ let separator = typeof options.separator === "string" ? options.separator : DEFAULT_CHART_LABEL_SEPARATOR;
1843
+ if (/[\r\n]/.test(separator)) {
1844
+ separator = separator.replace(/[\r\n]+/g, " ");
1845
+ adapt("dataLabels.separator", "unsupported-content", "A data label is one line; the line break in the separator is replaced with a space.");
1846
+ }
1847
+ result.dataLabels = { content, position, separator };
1848
+ }
1849
+ }
1850
+ result.active = result.legend !== void 0 || result.dataLabels !== void 0 || result.dataLabelsOff === true || result.axisTitles.category !== void 0 || result.axisTitles.value !== void 0;
1851
+ return result;
1852
+ }
1853
+ function formatChartLabelNumber(value) {
1854
+ return String(Number(value.toPrecision(12)));
1855
+ }
1856
+ function formatChartLabelPercent(share) {
1857
+ return `${Math.round(Number((share * 100).toPrecision(12)))}%`;
1858
+ }
1859
+ function chartLabelText(parts, content, separator = DEFAULT_CHART_LABEL_SEPARATOR) {
1860
+ return CONTENT_ORDER.filter((part) => content.includes(part) && parts[part] !== void 0).map((part) => parts[part]).join(separator);
1861
+ }
361
1862
 
362
1863
  // src/composition.ts
363
1864
  var MAX_COMPOSITION_DEPTH = 32;
@@ -368,14 +1869,14 @@ function resolveFontSchemeReference(reference, lookup, path = "design.fontScheme
368
1869
  const overrides = typeof reference === "object" && reference !== null && !Array.isArray(reference) ? reference : void 0;
369
1870
  const id = typeof reference === "string" ? reference : typeof overrides?.id === "string" ? overrides.id : void 0;
370
1871
  const found = id === void 0 ? void 0 : lookup(id);
371
- const base = record2(found ?? lookup(DEFAULT_FONT_SCHEME));
1872
+ const base = record4(found ?? lookup(DEFAULT_FONT_SCHEME));
372
1873
  const scheme = overrides ? { ...base, ...overrides } : { ...base };
373
1874
  if (id === void 0 || found !== void 0) return { scheme };
374
1875
  return { scheme, diagnostic: { code: "unresolved-font-scheme", path, id, fallback: DEFAULT_FONT_SCHEME, message: `Font scheme '${id}' is not in the inline or bundled catalogs; using the default font scheme '${DEFAULT_FONT_SCHEME}'.` } };
375
1876
  }
376
1877
  function resolveFontFamilies(input) {
377
- const scheme = record2(input);
378
- const family = (value) => typeof value === "string" ? value : record2(value).family;
1878
+ const scheme = record4(input);
1879
+ const family = (value) => typeof value === "string" ? value : record4(value).family;
379
1880
  const accent = family(scheme.accent);
380
1881
  return {
381
1882
  heading: family(scheme.heading) ?? scheme.major ?? scheme.minor ?? DEFAULT_FONT_FAMILIES.heading,
@@ -389,27 +1890,28 @@ function resolveTextStyle(style, measurement) {
389
1890
  }
390
1891
  function textWidthMeasurer(style, measurement) {
391
1892
  if (!measurement) return measureText;
392
- return (text, size) => {
393
- const width = measurement.measure(text, size, style);
1893
+ return (text2, size) => {
1894
+ const width = measurement.measure(text2, size, style);
394
1895
  if (!Number.isFinite(width) || width < 0) throw new RangeError("Text measurement must return a finite, nonnegative width.");
395
1896
  return width;
396
1897
  };
397
1898
  }
398
- function measureTextOutline(text, fontSize, style, measurement) {
1899
+ function measureTextOutline(text2, fontSize, style, measurement) {
399
1900
  if (measurement?.outlineBounds === void 0) return void 0;
400
1901
  if (typeof measurement.outlineBounds !== "function") throw new TypeError("Text outline provider must be a function.");
401
- const bounds = measurement.outlineBounds(text, fontSize, style);
1902
+ const bounds = measurement.outlineBounds(text2, fontSize, style);
402
1903
  if (bounds === null) return null;
403
1904
  if (!bounds || ![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite) || bounds.width < 0 || bounds.height < 0) {
404
1905
  throw new RangeError("Text outline measurement must return null or finite bounds with nonnegative dimensions.");
405
1906
  }
406
1907
  return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
407
1908
  }
408
- function placeTextLines(lines, box, alignment = "left", rasterPadding = 0) {
1909
+ function placeTextLines(lines, box, alignment = "left", rasterPadding = 0, directions) {
409
1910
  if (!Array.isArray(lines) || !box || ![box.x, box.y, box.width, box.height, rasterPadding].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || rasterPadding < 0 || !["left", "center", "right"].includes(alignment)) throw new RangeError("Text placement requires lines, finite positive dimensions, nonnegative padding and a valid alignment.");
410
- const factor = alignment === "right" ? 1 : alignment === "center" ? 0.5 : 0, placed = [];
1911
+ const placed = [];
411
1912
  let shift = 0, bottom = box.y, height = 0, overflow = false;
412
- for (const line of lines) {
1913
+ for (const [lineIndex, line] of lines.entries()) {
1914
+ const lineAlignment = physicalAlignment(alignment, directions?.[lineIndex]), factor = lineAlignment === "right" ? 1 : lineAlignment === "center" ? 0.5 : 0;
413
1915
  if (!line || ![line.width, line.y, line.baseline, line.height].every(Number.isFinite) || line.width < 0 || line.height <= 0 || line.y < 0 || line.baseline < line.y) throw new RangeError("Text lines require finite coordinates, nonnegative advances and a baseline at or below their top.");
414
1916
  const ink = line.outline;
415
1917
  if (ink !== null && (!ink || ![ink.x, ink.y, ink.width, ink.height].every(Number.isFinite) || ink.width < 0 || ink.height < 0)) throw new RangeError("Text outlines must be null or finite coordinates with nonnegative dimensions.");
@@ -425,7 +1927,7 @@ function placeTextLines(lines, box, alignment = "left", rasterPadding = 0) {
425
1927
  if (outline) bottom = outline.y + outline.height + rasterPadding;
426
1928
  height = Math.max(height, y + line.height - box.y, outline ? bottom - box.y : 0);
427
1929
  if (line.width > box.width + 0.01 || height > box.height + 0.01) overflow = true;
428
- placed.push({ x, y, baseline, height: line.height, width: line.width, outline });
1930
+ placed.push({ x, y, baseline, height: line.height, width: line.width, outline, ...directions ? { alignment: lineAlignment } : {} });
429
1931
  }
430
1932
  return { alignment, rasterPadding, lines: placed, height, overflow };
431
1933
  }
@@ -460,14 +1962,14 @@ var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
460
1962
  var COVER_LAYOUT_IDS = /* @__PURE__ */ new Set(["title", "title-subtitle"]);
461
1963
  var rows = ["top", "middle", "bottom"];
462
1964
  var columns = ["left", "center", "right"];
463
- var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
1965
+ var record4 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
464
1966
  var kind = (field) => field === "items" ? "list" : field === "bullets" ? "text" : field;
465
- var round = (value) => Math.round(value * 1e6) / 1e6 || value;
1967
+ var round2 = (value) => Math.round(value * 1e6) / 1e6 || value;
466
1968
  var SLIDE_IMAGE_POSITIONS = ["background", "top", "bottom", "left", "right"];
467
1969
  var SLIDE_IMAGE_BAND = 0.5;
468
- var assetSource = (value) => typeof value === "string" ? value : record2(value).src;
1970
+ var assetSource = (value) => typeof value === "string" ? value : record4(value).src;
469
1971
  var finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max ? value : void 0;
470
- var pathNumber = (value) => String(round(value));
1972
+ var pathNumber = (value) => String(round2(value));
471
1973
  var SLIDE_IMAGE_SHAPES = { rectangle: "rect", rounded: "roundRect", circle: "ellipse", hexagon: "hexagon" };
472
1974
  function slideImageShape(kind2, box, cornerRadius = 1 / 6) {
473
1975
  const { x, y, width: w, height: h } = box, r = x + w, b = y + h, ss = Math.min(w, h), n = pathNumber;
@@ -488,15 +1990,17 @@ function slideImageShape(kind2, box, cornerRadius = 1 / 6) {
488
1990
  }
489
1991
  return { kind: "rectangle", preset: "rect", adjust: {}, path: rect };
490
1992
  }
491
- function resolveSlideImage(slide, layout, presentation, width, height, path, padding, scale, diagnostics) {
492
- const own = record2(slide.design), deck = record2(record2(presentation).design);
1993
+ function resolveSlideImage(slide, layout, presentation, width, height, path, padding, scale, diagnostics, rtl = false) {
1994
+ const own = record4(slide.design), deck = record4(record4(presentation).design);
493
1995
  const local = own.slideImage !== void 0;
494
1996
  const configured = local ? own.slideImage : deck.slideImage;
495
1997
  if (!configured || typeof configured !== "object" && typeof configured !== "string") return void 0;
496
- const treatment = typeof configured === "object" && !Array.isArray(configured) && "position" in configured ? record2(configured) : void 0;
1998
+ const treatment = typeof configured === "object" && !Array.isArray(configured) && "position" in configured ? record4(configured) : void 0;
497
1999
  const alignment = typeof layout.slideImageAlignment === "string" ? layout.slideImageAlignment.toLowerCase() : void 0;
498
- const position = treatment ? treatment.position : SLIDE_IMAGE_POSITIONS.find((value2) => value2 === alignment) ?? "background";
499
- if (!SLIDE_IMAGE_POSITIONS.includes(position)) return void 0;
2000
+ const authoredPosition = treatment ? treatment.position : SLIDE_IMAGE_POSITIONS.find((value2) => value2 === alignment) ?? "background";
2001
+ if (!SLIDE_IMAGE_POSITIONS.includes(authoredPosition)) return void 0;
2002
+ const mirrorSide = (side) => !rtl ? side : side === "left" ? "right" : side === "right" ? "left" : side;
2003
+ const position = mirrorSide(authoredPosition);
500
2004
  const designSource = treatment ? treatment.src : configured;
501
2005
  const root = slide.image, sameSource = root !== void 0 && designSource !== void 0 && assetSource(root) === assetSource(designSource);
502
2006
  if (!local && layout.slideImage !== true && !sameSource) return void 0;
@@ -507,8 +2011,8 @@ function resolveSlideImage(slide, layout, presentation, width, height, path, pad
507
2011
  const t = treatment ?? {};
508
2012
  const fill = t.fill === "crop" || t.fill === "fit" ? t.fill : (own.imageFill ?? deck.imageFill) === "fit" ? "fit" : "crop";
509
2013
  const share = finite(t.size, 0.1, 0.9) ?? SLIDE_IMAGE_BAND;
510
- const band = { width: round(width * share), height: round(height * share) };
511
- const region = position === "left" ? { x: 0, y: 0, width: band.width, height } : position === "right" ? { x: round(width - band.width), y: 0, width: band.width, height } : position === "top" ? { x: 0, y: 0, width, height: band.height } : position === "bottom" ? { x: 0, y: round(height - band.height), width, height: band.height } : { x: 0, y: 0, width, height };
2014
+ const band = { width: round2(width * share), height: round2(height * share) };
2015
+ const region = position === "left" ? { x: 0, y: 0, width: band.width, height } : position === "right" ? { x: round2(width - band.width), y: 0, width: band.width, height } : position === "top" ? { x: 0, y: 0, width, height: band.height } : position === "bottom" ? { x: 0, y: round2(height - band.height), width, height: band.height } : { x: 0, y: 0, width, height };
512
2016
  const designPath = local ? `${path}.design.slideImage` : "design.slideImage";
513
2017
  const kind2 = Object.hasOwn(SLIDE_IMAGE_SHAPES, t.shape) ? t.shape : "rectangle";
514
2018
  let box = t.inset === true ? { x: region.x + padding, y: region.y + padding, width: Math.max(scale, region.width - 2 * padding), height: Math.max(scale, region.height - 2 * padding) } : { ...region };
@@ -517,7 +2021,7 @@ function resolveSlideImage(slide, layout, presentation, width, height, path, pad
517
2021
  const frameWidth = Math.min(box.width, box.height * aspect), frameHeight = frameWidth / aspect;
518
2022
  box = { x: box.x + (box.width - frameWidth) / 2, y: box.y + (box.height - frameHeight) / 2, width: frameWidth, height: frameHeight };
519
2023
  }
520
- box = { x: round(box.x), y: round(box.y), width: round(box.width), height: round(box.height) };
2024
+ box = { x: round2(box.x), y: round2(box.y), width: round2(box.width), height: round2(box.height) };
521
2025
  const result = {
522
2026
  path: designPath,
523
2027
  sourcePath: replacesContent ? `${path}.image` : designPath,
@@ -530,19 +2034,19 @@ function resolveSlideImage(slide, layout, presentation, width, height, path, pad
530
2034
  shape: slideImageShape(kind2, box, finite(t.cornerRadius, 0, 0.5))
531
2035
  };
532
2036
  if (typeof t.alt === "string") result.alt = t.alt;
533
- const border = record2(t.border), borderWidth = finite(border.width, 0, 64);
534
- if (border.color !== void 0 && borderWidth) result.border = { color: border.color, width: round(borderWidth * scale) };
2037
+ const border = record4(t.border), borderWidth = finite(border.width, 0, 64);
2038
+ if (border.color !== void 0 && borderWidth) result.border = { color: border.color, width: round2(borderWidth * scale) };
535
2039
  const opacity = finite(t.opacity, 0, 1);
536
2040
  if (opacity !== void 0 && opacity < 1) result.opacity = opacity;
537
2041
  if (t.recolor === "grayscale") result.recolor = { type: "grayscale" };
538
- else if (record2(t.recolor).dark !== void 0 && record2(t.recolor).light !== void 0) result.recolor = { type: "duotone", dark: t.recolor.dark, light: t.recolor.light };
539
- const overlay = record2(t.overlay), overlayOpacity = finite(overlay.opacity, 0, 1);
2042
+ else if (record4(t.recolor).dark !== void 0 && record4(t.recolor).light !== void 0) result.recolor = { type: "duotone", dark: t.recolor.dark, light: t.recolor.light };
2043
+ const overlay = record4(t.overlay), overlayOpacity = finite(overlay.opacity, 0, 1);
540
2044
  if (overlay.color !== void 0 && overlayOpacity !== void 0) {
541
- const edge = ["top", "bottom", "left", "right"].includes(overlay.edge) ? overlay.edge : void 0;
2045
+ const edge = ["top", "bottom", "left", "right"].includes(overlay.edge) ? mirrorSide(overlay.edge) : void 0;
542
2046
  if (edge && kind2 !== "rectangle") diagnostics.push({ code: "unsupported-image-treatment", path: `${designPath}.overlay.edge`, message: "An edge overlay needs a rectangle frame; a band cannot follow a rounded, circular or hexagonal mask as one native shape. Remove edge or use shape rectangle." });
543
2047
  else {
544
2048
  const part = finite(overlay.size, 0.05, 1) ?? 0.3;
545
- const bandBox = !edge ? box : edge === "top" ? { ...box, height: round(box.height * part) } : edge === "bottom" ? { ...box, y: round(box.y + box.height * (1 - part)), height: round(box.height * part) } : edge === "left" ? { ...box, width: round(box.width * part) } : { ...box, x: round(box.x + box.width * (1 - part)), width: round(box.width * part) };
2049
+ const bandBox = !edge ? box : edge === "top" ? { ...box, height: round2(box.height * part) } : edge === "bottom" ? { ...box, y: round2(box.y + box.height * (1 - part)), height: round2(box.height * part) } : edge === "left" ? { ...box, width: round2(box.width * part) } : { ...box, x: round2(box.x + box.width * (1 - part)), width: round2(box.width * part) };
546
2050
  result.overlay = { color: overlay.color, opacity: overlayOpacity, box: bandBox, shape: edge ? slideImageShape("rectangle", bandBox) : result.shape };
547
2051
  }
548
2052
  }
@@ -565,10 +2069,10 @@ function resolveLogo(presentation, slide, options = {}) {
565
2069
  };
566
2070
  const pick = (value, path) => {
567
2071
  if (value === void 0 || value === null || value === false) return null;
568
- if (typeof value === "string" || typeof value === "object" && !Array.isArray(value) && "src" in record2(value)) {
2072
+ if (typeof value === "string" || typeof value === "object" && !Array.isArray(value) && "src" in record4(value)) {
569
2073
  return usable(value) ? { source: value, path, variant: "default", slot } : null;
570
2074
  }
571
- const set = record2(value);
2075
+ const set = record4(value);
572
2076
  if (!Object.keys(set).some((key) => LOGO_SET_KEYS.has(key))) return null;
573
2077
  for (const key of chain) {
574
2078
  const variant = set[key];
@@ -576,16 +2080,16 @@ function resolveLogo(presentation, slide, options = {}) {
576
2080
  }
577
2081
  return null;
578
2082
  };
579
- const deck = record2(presentation), slideIndex = Number.isSafeInteger(options.slideIndex) && options.slideIndex >= 0 ? options.slideIndex : 0;
580
- const own = pick(record2(record2(slide).design).logo, `slides.${slideIndex}.design.logo`);
2083
+ const deck = record4(presentation), slideIndex = Number.isSafeInteger(options.slideIndex) && options.slideIndex >= 0 ? options.slideIndex : 0;
2084
+ const own = pick(record4(record4(slide).design).logo, `slides.${slideIndex}.design.logo`);
581
2085
  if (own) return own;
582
- const shared = pick(record2(deck.design).logo, "design.logo");
2086
+ const shared = pick(record4(deck.design).logo, "design.logo");
583
2087
  if (shared) return shared;
584
2088
  const organizations = Array.isArray(deck.organization) ? deck.organization : [deck.organization];
585
- const primaryIndex = organizations.findIndex((item) => record2(item).role === "primary");
2089
+ const primaryIndex = organizations.findIndex((item) => record4(item).role === "primary");
586
2090
  const index = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
587
2091
  if (index < 0) return null;
588
- return pick(record2(organizations[index]).logo, Array.isArray(deck.organization) ? `organization.${index}.logo` : "organization.logo");
2092
+ return pick(record4(organizations[index]).logo, Array.isArray(deck.organization) ? `organization.${index}.logo` : "organization.logo");
589
2093
  }
590
2094
  var webUrl = /^https?:\/\/\S+$/i;
591
2095
  function resolveSocialProfile(platform, value, records = [], owner = "organization") {
@@ -603,10 +2107,11 @@ function resolveSocialProfile(platform, value, records = [], owner = "organizati
603
2107
  return { text: display(url), href: encodeURI(url), resolved: true };
604
2108
  }
605
2109
  function layoutFurniture(input, options = {}) {
606
- const slide = record2(input), width = options.width ?? 1280, height = options.height ?? 720, scale = Math.min(width, height) / 720;
607
- const settings = { ...record2(record2(options.layout).composition), ...record2(slide.composition) };
2110
+ const slide = record4(input), width = options.width ?? 1280, height = options.height ?? 720, scale = Math.min(width, height) / 720;
2111
+ const settings = { ...record4(record4(options.layout).composition), ...record4(slide.composition) };
608
2112
  assertComposition(settings);
609
- const minimum = (settings.minFontSize ?? 16) * scale, size = Math.max(13 * scale, minimum), padding = (options.textRasterPadding ?? 2) * scale;
2113
+ const furnitureRtl = (options.direction ?? resolveSlideDirection(options.presentation, options.slideIndex)) === "rtl";
2114
+ const minimum = snapFontSizeUp((settings.minFontSize ?? 16) * scale), size = gridFontSize(13 * scale, minimum), padding = (options.textRasterPadding ?? 2) * scale;
610
2115
  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.");
611
2116
  const number = options.slideNumber ?? (options.slideIndex ?? 0) + 1;
612
2117
  if (!Number.isSafeInteger(number) || number < 1) throw new RangeError("Displayed slide number must be a positive safe integer.");
@@ -615,32 +2120,33 @@ function layoutFurniture(input, options = {}) {
615
2120
  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.");
616
2121
  const outlines = options.textMeasurement?.outlineBounds !== void 0, parts = [], diagnostics = [];
617
2122
  const sourceRoot = `slides.${options.slideIndex ?? 0}`, organizations = Array.isArray(options.presentation?.organization) ? options.presentation.organization : [options.presentation?.organization];
618
- const primaryIndex = organizations.findIndex((item) => record2(item).role === "primary"), organizationIndex = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
619
- const organization = record2(organizations[organizationIndex]), organizationRoot = Array.isArray(options.presentation?.organization) ? `organization.${organizationIndex}` : "organization", organizationPath = `${organizationRoot}.name`;
620
- const inlinePlatforms = record2(record2(options.presentation?.catalogs).socialPlatforms).records, platformRecords = [...Array.isArray(inlinePlatforms) ? inlinePlatforms : [], ...options.socialPlatforms ?? []];
2123
+ const primaryIndex = organizations.findIndex((item) => record4(item).role === "primary"), organizationIndex = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
2124
+ const organization = record4(organizations[organizationIndex]), organizationRoot = Array.isArray(options.presentation?.organization) ? `organization.${organizationIndex}` : "organization", organizationPath = `${organizationRoot}.name`;
2125
+ const inlinePlatforms = record4(record4(options.presentation?.catalogs).socialPlatforms).records, platformRecords = [...Array.isArray(inlinePlatforms) ? inlinePlatforms : [], ...options.socialPlatforms ?? []];
621
2126
  const fontFamily = options.fonts?.body ?? "sans-serif";
622
2127
  let headerBottom = 0, footerTop = height, configured = false;
623
2128
  const error = (path, message, code = "text-overflow") => diagnostics.push({ code, path, message });
624
2129
  for (const kind2 of ["header", "footer"]) {
625
- const local = record2(slide.design)[kind2] !== void 0, value = local ? record2(slide.design)[kind2] : options.presentation?.design?.[kind2];
2130
+ const local = record4(slide.design)[kind2] !== void 0, value = local ? record4(slide.design)[kind2] : options.presentation?.design?.[kind2];
626
2131
  if (value === void 0 || value === false) continue;
627
2132
  const root = local ? `${sourceRoot}.design.${kind2}` : `design.${kind2}`;
628
2133
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Header/footer content must be an object or false.");
629
2134
  configured = true;
630
2135
  const zones = [];
631
2136
  for (const [index, zone] of ["left", "center", "right"].entries()) {
632
- const content = record2(record2(value)[zone]), path = `${root}.${zone}`, x = width * (0.07 + index * 0.3), zoneWidth = width * 0.26, zoneParts = [];
2137
+ const side = furnitureRtl && zone !== "center" ? zone === "left" ? "right" : "left" : zone;
2138
+ const content = record4(record4(value)[zone]), path = `${root}.${zone}`, x = width * (0.07 + (furnitureRtl ? 2 - index : index) * 0.3), zoneWidth = width * 0.26, zoneParts = [];
633
2139
  let y = 0;
634
- const add = (field, text, generated = false, sourcePath, extras = {}) => {
635
- if (text === void 0) return;
636
- if (typeof text !== "string") throw new TypeError(`Furniture field ${path}.${field} requires string content.`);
2140
+ const add = (field, text2, generated = false, sourcePath, extras = {}) => {
2141
+ if (text2 === void 0) return;
2142
+ if (typeof text2 !== "string") throw new TypeError(`Furniture field ${path}.${field} requires string content.`);
637
2143
  const partPath = `${path}.${field}`, style = resolveTextStyle({ fontFamily, fontWeight: 400, italic: false, path: partPath }, options.textMeasurement);
638
- 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));
2144
+ const fit = fitText(text2, { width: Math.max(Number.MIN_VALUE, zoneWidth - (outlines ? 2 * padding : 0)), height: Number.MAX_VALUE }, size, size, textWidthMeasurer(style, options.textMeasurement));
639
2145
  const ink = fit.sourceLines.map((line, index2) => {
640
2146
  let outline = null;
641
2147
  if (outlines) {
642
2148
  for (const segment of line.segments) if (segment.kind === "text") {
643
- const bounds = measureTextOutline(text.slice(segment.start, segment.end), size, style, options.textMeasurement);
2149
+ const bounds = measureTextOutline(text2.slice(segment.start, segment.end), size, style, options.textMeasurement);
644
2150
  if (bounds) {
645
2151
  const next = { ...bounds, x: bounds.x + segment.x };
646
2152
  if (!outline) outline = next;
@@ -656,30 +2162,30 @@ function layoutFurniture(input, options = {}) {
656
2162
  }
657
2163
  return { width: line.width, y: index2 * fit.lineHeight, baseline: size + index2 * fit.lineHeight, height: fit.lineHeight, outline };
658
2164
  });
659
- const natural = outlines ? placeTextLines(ink, { x, y, width: zoneWidth, height: Number.MAX_VALUE }, zone, padding) : void 0;
2165
+ const natural = outlines ? placeTextLines(ink, { x, y, width: zoneWidth, height: Number.MAX_VALUE }, side, padding) : void 0;
660
2166
  const partHeight = natural?.height ?? fit.sourceLines.length * fit.lineHeight;
661
2167
  if (!Number.isFinite(partHeight)) throw new RangeError("Furniture exceeds finite layout coordinates.");
662
- const box = { x, y, width: zoneWidth, height: Math.max(scale, partHeight) }, placement = outlines ? placeTextLines(ink, box, zone, padding) : void 0;
2168
+ const box = { x, y, width: zoneWidth, height: Math.max(scale, partHeight) }, placement = outlines ? placeTextLines(ink, box, side, padding) : void 0;
663
2169
  const accepted = { ...fit, ...placement ? { placement } : {}, overflow: fit.overflow || !!placement?.overflow };
664
- 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, ...extras.fields?.length ? { fields: extras.fields } : {}, ...extras.links?.length ? { links: extras.links } : {} });
2170
+ zoneParts.push({ type: "text", kind: kind2, zone, field, path: partPath, sourcePath: sourcePath ?? (!generated ? partPath : void 0), generated, text: text2, style, requestedFontSize: size, minFontSize: minimum, box, alignment: side, fit: accepted, ...extras.fields?.length ? { fields: extras.fields } : {}, ...extras.links?.length ? { links: extras.links } : {} });
665
2171
  if (accepted.overflow) error(partPath, "Repeated text exceeds its zone at the selected readability floor; change the furniture or slide design.");
666
2172
  y += box.height;
667
2173
  };
668
2174
  const imageBox = (source) => {
669
- const imageHeight = Math.max(32 * scale, Math.min(height * 0.05, 72 * scale)), imageWidth = Math.min(zoneWidth, imageHeight * (intrinsicImageAspect(source, record2(options.presentation).assets) ?? 1));
670
- return { x: zone === "left" ? x : zone === "center" ? x + (zoneWidth - imageWidth) / 2 : x + zoneWidth - imageWidth, y, width: imageWidth, height: imageHeight };
2175
+ const imageHeight = Math.max(32 * scale, Math.min(height * 0.05, 72 * scale)), imageWidth = Math.min(zoneWidth, imageHeight * (intrinsicImageAspect(source, record4(options.presentation).assets) ?? 1));
2176
+ return { x: side === "left" ? x : side === "center" ? x + (zoneWidth - imageWidth) / 2 : x + zoneWidth - imageWidth, y, width: imageWidth, height: imageHeight };
671
2177
  };
672
2178
  if (content.logo === true) {
673
2179
  const resolved = resolveLogo(options.presentation, slide, { slot: "icon", onDark: options.darkBackground, slideIndex: options.slideIndex });
674
2180
  if (resolved) {
675
2181
  const box = imageBox(resolved.source);
676
- zoneParts.push({ type: "image", kind: kind2, zone, field: "logo", path: `${path}.logo`, sourcePath: resolved.path, generated: true, image: resolved.source, box, alignment: zone });
2182
+ zoneParts.push({ type: "image", kind: kind2, zone, field: "logo", path: `${path}.logo`, sourcePath: resolved.path, generated: true, image: resolved.source, box, alignment: side });
677
2183
  y += box.height;
678
2184
  } else error(`${path}.logo`, "Generated logo needs design.logo or a primary organization logo.", "unresolved-content");
679
2185
  }
680
2186
  if (content.image !== void 0) {
681
2187
  const box = imageBox(content.image);
682
- zoneParts.push({ type: "image", kind: kind2, zone, field: "image", path: `${path}.image`, sourcePath: `${path}.image`, generated: false, image: content.image, box, alignment: zone });
2188
+ zoneParts.push({ type: "image", kind: kind2, zone, field: "image", path: `${path}.image`, sourcePath: `${path}.image`, generated: false, image: content.image, box, alignment: side });
683
2189
  y += box.height;
684
2190
  }
685
2191
  add("text", content.text);
@@ -688,7 +2194,7 @@ function layoutFurniture(input, options = {}) {
688
2194
  else error(`${path}.organization`, "Generated organization name needs a named organization in the presentation.", "unresolved-content");
689
2195
  }
690
2196
  if (content.socials === true) {
691
- const links = Object.entries(record2(organization.socials)).filter(([, value2]) => typeof value2 === "string" && value2.trim()).map(([platform, value2]) => ({ platform, sourcePath: `${organizationRoot}.socials.${platform}`, ...resolveSocialProfile(platform, value2, platformRecords, "organization") }));
2197
+ const links = Object.entries(record4(organization.socials)).filter(([, value2]) => typeof value2 === "string" && value2.trim()).map(([platform, value2]) => ({ platform, sourcePath: `${organizationRoot}.socials.${platform}`, ...resolveSocialProfile(platform, value2, platformRecords, "organization") }));
692
2198
  if (links.length) add("socials", links.map((link) => link.text).join("\n"), true, `${organizationRoot}.socials`, { links });
693
2199
  else error(`${path}.socials`, "Generated social profiles need a primary organization with socials.", "unresolved-content");
694
2200
  }
@@ -740,29 +2246,43 @@ function layoutFurniture(input, options = {}) {
740
2246
  return { algorithm: "furniture-flow-v2", configured, textMeasurement: options.textMeasurement ? "provided" : "estimated", textOutlines: outlines ? "provided" : "unavailable", parts, headerBottom, footerTop, diagnostics, overflow: diagnostics.length > 0 };
741
2247
  }
742
2248
  var textSegments = new Intl.Segmenter("und", { granularity: "grapheme" });
743
- function measureText(text, fontSize) {
2249
+ var WRAP_TOLERANCE = 0.01;
2250
+ var HANGING_SPACE = /[^\S\r\n\u00a0\u202f\ufeff]/u;
2251
+ function measureText(text2, fontSize) {
744
2252
  let units = 0;
745
- for (const character of text) {
2253
+ for (const character of text2) {
746
2254
  if (/\p{Mark}|\u200d|\ufe0f/u.test(character)) continue;
747
2255
  units += character === " " ? 0.32 : /[A-Z0-9]/.test(character) ? 0.62 : character.codePointAt(0) > 11904 ? 1 : 0.54;
748
2256
  }
749
2257
  return units * fontSize;
750
2258
  }
751
- function wrapText(text, width, fontSize, measure = measureText) {
752
- return fitSourceText(text, { width, height: Number.MAX_VALUE }, fontSize, fontSize, 1, measure, true).lines;
2259
+ function wrapText(text2, width, fontSize, measure = measureText) {
2260
+ return fitSourceText(text2, { width, height: Number.MAX_VALUE }, fontSize, fontSize, 1, measure, true, false).lines;
753
2261
  }
754
- function fitText(text, box, requestedSize = 25, minFontSize = 16, measure = measureText) {
2262
+ function fitText(text2, box, requestedSize = 25, minFontSize = 16, measure = measureText, direction) {
755
2263
  if (![box.width, box.height, requestedSize, minFontSize].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || requestedSize <= 0 || minFontSize <= 0) {
756
2264
  throw new RangeError("Text dimensions and font sizes must be finite and positive.");
757
2265
  }
758
- return fitSourceText(text, box, Math.max(requestedSize, minFontSize), minFontSize, 1, measure, true);
2266
+ return fitSourceText(text2, box, Math.max(requestedSize, minFontSize), minFontSize, 1, measure, true, true, direction);
759
2267
  }
760
- function fitAtSizes(layout, requested, minimum, step = 1) {
2268
+ var FONT_SIZE_GRID_PER_PX = 75;
2269
+ var FONT_GRID_EPSILON = 1e-6;
2270
+ function snapFontSizeDown(px) {
2271
+ return Math.max(px > 0 ? 1 : -Infinity, Math.floor(px * FONT_SIZE_GRID_PER_PX + FONT_GRID_EPSILON)) / FONT_SIZE_GRID_PER_PX;
2272
+ }
2273
+ function snapFontSizeUp(px) {
2274
+ return Math.max(px > 0 ? 1 : -Infinity, Math.ceil(px * FONT_SIZE_GRID_PER_PX - FONT_GRID_EPSILON)) / FONT_SIZE_GRID_PER_PX;
2275
+ }
2276
+ function gridFontSize(raw, minimum) {
2277
+ return Math.max(snapFontSizeUp(minimum), snapFontSizeDown(raw));
2278
+ }
2279
+ function fitAtSizes(layout, requested, minimum, step = 1, snap = true) {
761
2280
  const start = Math.max(requested, minimum);
762
2281
  if (![start, minimum, step].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Readable font sizes and fitting steps must be finite and positive.");
763
2282
  for (let trial = 0; trial <= 64; trial++) {
764
- const size = trial === 64 ? minimum : Math.max(minimum, start - trial * step), result = layout(size);
765
- if (!result.overflow || size === minimum) return result;
2283
+ const raw = start - trial * step, last = trial === 64 || raw <= minimum;
2284
+ const size = !snap ? last ? minimum : Math.max(minimum, raw) : last ? snapFontSizeUp(minimum) : gridFontSize(raw, minimum), result = layout(size);
2285
+ if (!result.overflow || last) return result;
766
2286
  }
767
2287
  throw new Error("Text fitting did not evaluate its bounded floor trial.");
768
2288
  }
@@ -772,7 +2292,7 @@ function layoutQuote(value, box, options = {}) {
772
2292
  if (!quote || Array.isArray(quote) || typeof quote.text !== "string" || [quote.attribution, quote.source].some((field) => field !== void 0 && typeof field !== "string")) {
773
2293
  throw new TypeError("Quote content must be a string or a text object with optional string attribution/source.");
774
2294
  }
775
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
2295
+ const scale = options.scale ?? 1, minimum = snapFontSizeUp((options.minFontSize ?? 16) * scale);
776
2296
  if (![box.x, box.y, box.width, box.height, scale, minimum].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0) {
777
2297
  throw new RangeError("Quote dimensions, scale and minimum font size must be finite and positive.");
778
2298
  }
@@ -780,23 +2300,23 @@ function layoutQuote(value, box, options = {}) {
780
2300
  const path = options.path ?? "quote";
781
2301
  const diagnostics = [], parts = [];
782
2302
  const report = (reason, diagnosticPath, roles, message) => diagnostics.push({ code: "text-overflow", reason, path: diagnosticPath, parts: roles, message });
783
- const source = (sourcePath, text, outputStart) => ({ path: sourcePath, start: 0, end: text.length, outputStart, outputEnd: outputStart + text.length });
784
- const add = (role, text, sources, area, fontSize, fontFamily, fontWeight, partPath) => {
2303
+ const source = (sourcePath, text2, outputStart) => ({ path: sourcePath, start: 0, end: text2.length, outputStart, outputEnd: outputStart + text2.length });
2304
+ const add = (role, text2, sources, area, fontSize, fontFamily, fontWeight, partPath) => {
785
2305
  const requestedStyle = { fontFamily, fontWeight, italic: false, path: partPath };
786
2306
  const style = resolveTextStyle({ ...requestedStyle }, options.textMeasurement);
787
- const requestedFontSize = Math.max(fontSize * scale, minimum);
788
- const part = { role, path: partPath, text, sources, box: area, requestedFontSize, minFontSize: minimum, requestedStyle, style };
2307
+ const requestedFontSize = gridFontSize(fontSize * scale, minimum);
2308
+ const part = { role, path: partPath, text: text2, sources, box: area, requestedFontSize, minFontSize: minimum, requestedStyle, style };
789
2309
  parts.push(part);
790
2310
  return part;
791
2311
  };
792
2312
  let footer = "";
793
2313
  const footerSources = [];
794
2314
  for (const field of ["attribution", "source"]) {
795
- const text = quote[field];
796
- if (!text) continue;
2315
+ const text2 = quote[field];
2316
+ if (!text2) continue;
797
2317
  if (footer) footer += " - ";
798
- footerSources.push(source(`${path}.${field}`, text, footer.length));
799
- footer += text;
2318
+ footerSources.push(source(`${path}.${field}`, text2, footer.length));
2319
+ footer += text2;
800
2320
  }
801
2321
  const bodyPath = shorthand ? path : `${path}.text`;
802
2322
  const body = add(
@@ -820,7 +2340,7 @@ function layoutQuote(value, box, options = {}) {
820
2340
  path
821
2341
  ) : void 0;
822
2342
  const usable = (area) => [area.x, area.y, area.width, area.height].every(Number.isFinite) && area.width > 0 && area.height > 0;
823
- const fit = (part, area, size = part.requestedFontSize, floor = minimum) => usable(area) ? fitText(part.text, area, size, floor, textWidthMeasurer(part.style, options.textMeasurement)) : void 0;
2343
+ const fit = (part, area, size = part.requestedFontSize, floor = minimum) => usable(area) ? fitText(part.text, area, size, floor, textWidthMeasurer(part.style, options.textMeasurement), options.direction) : void 0;
824
2344
  const inner = { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - 36 };
825
2345
  if (!attribution) body.fit = fit(body, body.box);
826
2346
  else if (usable(inner) && inner.height > 18) {
@@ -877,31 +2397,31 @@ function layoutMetric(value, box, options = {}) {
877
2397
  if (!metric || Array.isArray(metric) || !isValue(metric.value) || [metric.label, metric.description, metric.unit].some((field) => field !== void 0 && typeof field !== "string") || metric.delta !== void 0 && !isValue(metric.delta) || metric.trend !== void 0 && !["up", "down", "flat"].includes(metric.trend)) {
878
2398
  throw new TypeError("Metric content requires a finite numeric or string value and schema-valid display metadata.");
879
2399
  }
880
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
2400
+ const scale = options.scale ?? 1, minimum = snapFontSizeUp((options.minFontSize ?? 16) * scale);
881
2401
  const rasterPadding = (options.textRasterPadding ?? 1) * scale, hasOutlines = options.textMeasurement?.outlineBounds !== void 0;
882
2402
  if (![box.x, box.y, box.width, box.height, box.x + box.width, box.y + box.height, scale, minimum, Math.max(76 * scale, minimum) * 1.22].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0) {
883
2403
  throw new RangeError("Metric dimensions, scale and minimum font size must be finite and positive.");
884
2404
  }
885
2405
  if (!Number.isFinite(rasterPadding) || rasterPadding < 0) throw new RangeError("Metric raster padding must be finite and nonnegative.");
886
2406
  if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid metric overflow policy.");
887
- const alignment = options.align ?? "left";
888
- if (!["left", "center", "right"].includes(alignment)) throw new RangeError("Invalid metric alignment.");
2407
+ const requestedAlignment = options.align ?? "left";
2408
+ if (!["left", "center", "right"].includes(requestedAlignment)) throw new RangeError("Invalid metric alignment.");
889
2409
  const sourcePath = options.path ?? "metric", parts = [], diagnostics = [];
890
2410
  for (const role of ["value", "unit", "label", "description", "delta", "trend"]) {
891
2411
  const sourceValue = metric[role];
892
2412
  if (sourceValue === void 0) continue;
893
- const text = String(sourceValue), path = scalar ? sourcePath : `${sourcePath}.${role}`;
2413
+ const text2 = String(sourceValue), path = scalar ? sourcePath : `${sourcePath}.${role}`;
894
2414
  const nominal = role === "value" ? Math.min(76 * scale, box.height * 0.28) : (role === "description" ? 20 : role === "trend" ? 18 : 23) * scale;
895
2415
  const requestedStyle = { fontFamily: (role === "value" ? options.fonts?.heading : options.fonts?.body) ?? "sans-serif", fontWeight: role === "value" ? 800 : role === "description" ? 400 : 500, italic: false, path };
896
2416
  const part = {
897
2417
  role,
898
2418
  path,
899
- text,
900
- sources: [{ path, value: sourceValue, start: 0, end: text.length }],
901
- visible: role === "value" || text.length > 0,
2419
+ text: text2,
2420
+ sources: [{ path, value: sourceValue, start: 0, end: text2.length }],
2421
+ visible: role === "value" || text2.length > 0,
902
2422
  linePositions: [],
903
2423
  box: { ...box, height: 0 },
904
- requestedFontSize: Math.max(nominal, minimum),
2424
+ requestedFontSize: gridFontSize(nominal, minimum),
905
2425
  minFontSize: minimum,
906
2426
  requestedStyle,
907
2427
  style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement)
@@ -910,6 +2430,8 @@ function layoutMetric(value, box, options = {}) {
910
2430
  parts.push(part);
911
2431
  }
912
2432
  const primary = parts[0], metadata = parts.filter((part) => part !== primary && part.visible), unit = metadata.find((part) => part.role === "unit");
2433
+ const metricDirection = options.direction === "rtl" ? paragraphDirection(["label", "description", "unit"].map((role) => parts.find((part) => part.role === role)?.text).find(Boolean) ?? primary.text, "rtl") : void 0;
2434
+ const alignment = physicalAlignment(requestedAlignment, metricDirection);
913
2435
  const usable = (area) => [area.x, area.y, area.width, area.height, area.x + area.width, area.y + area.height].every(Number.isFinite) && area.width > 0 && area.height > 0;
914
2436
  const occupied = (fit) => fit.placement?.height ?? fit.lines.length * fit.lineHeight;
915
2437
  const gap = 8 * scale, primaryGap = 12 * scale;
@@ -946,7 +2468,7 @@ function layoutMetric(value, box, options = {}) {
946
2468
  const key = JSON.stringify([part.role, size, width]);
947
2469
  let fit = cache2.get(key);
948
2470
  if (!fit) {
949
- fit = fitCodeText(part.text, { ...box, width: hasOutlines ? Math.max(Number.MIN_VALUE, width - 2 * rasterPadding) : width }, size, size, scale, textWidthMeasurer(part.style, options.textMeasurement));
2471
+ fit = fitCodeText(part.text, { ...box, width: hasOutlines ? Math.max(Number.MIN_VALUE, width - 2 * rasterPadding) : width }, size, size, scale, textWidthMeasurer(part.style, options.textMeasurement), options.direction);
950
2472
  fit = place(part, fit, width);
951
2473
  if (!Number.isFinite(occupied(fit))) throw new RangeError("Metric text layout exceeds finite coordinates.");
952
2474
  cache2.set(key, fit);
@@ -956,7 +2478,7 @@ function layoutMetric(value, box, options = {}) {
956
2478
  const fitValue = (area, singleLine) => {
957
2479
  if (!usable(area)) return void 0;
958
2480
  for (let step = 0; step <= 76; step++) {
959
- const size = Math.max(minimum, primary.requestedFontSize - step * scale);
2481
+ const size = gridFontSize(primary.requestedFontSize - step * scale, minimum);
960
2482
  const natural = measure(primary, size, area.width);
961
2483
  const fit = { ...natural, overflow: !!natural.placement?.overflow || singleLine && natural.lines.length !== 1 || occupied(natural) > area.height + 0.01 || natural.sourceLines.some((line) => line.width > area.width + 0.01) };
962
2484
  if (!fit.overflow || size === minimum) return fit;
@@ -965,7 +2487,7 @@ function layoutMetric(value, box, options = {}) {
965
2487
  let selected, attempts = 0;
966
2488
  const reductions = Math.min(23, Math.ceil(Math.max(0, ...metadata.map((part) => (part.requestedFontSize - minimum) / scale))));
967
2489
  for (let reduction = 0; reduction <= reductions; reduction++) {
968
- const measured = new Map(metadata.map((part) => [part, measure(part, Math.max(minimum, part.requestedFontSize - reduction * scale), box.width)]));
2490
+ const measured = new Map(metadata.map((part) => [part, measure(part, gridFontSize(part.requestedFontSize - reduction * scale, minimum), box.width)]));
969
2491
  const measuredUnit = unit ? measured.get(unit) : void 0;
970
2492
  const unitWidth = unit && measuredUnit ? naturalWidth(unit, measuredUnit) : 0;
971
2493
  const unitFit = unit && measuredUnit && unitWidth > 0 ? place(unit, measuredUnit, unitWidth) : measuredUnit;
@@ -1043,17 +2565,17 @@ function layoutTimeline(value, box, options = {}) {
1043
2565
  if (!timeline || !Array.isArray(timeline.events) || !timeline.events.length || [timeline.name, timeline.description].some((field) => field !== void 0 && typeof field !== "string") || timeline.events.some((event) => !event || typeof event.what !== "string" || [event.when, event.description].some((field) => field !== void 0 && typeof field !== "string"))) {
1044
2566
  throw new TypeError("Timeline content requires ordered events with string labels and optional string metadata.");
1045
2567
  }
1046
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale, padding = (options.textRasterPadding ?? 1) * scale;
2568
+ const scale = options.scale ?? 1, minimum = snapFontSizeUp((options.minFontSize ?? 16) * scale), padding = (options.textRasterPadding ?? 1) * scale;
1047
2569
  if (![box.x, box.y, box.width, box.height, scale, minimum, padding].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0 || padding < 0) throw new RangeError("Timeline dimensions, scale and minimum must be positive, with finite nonnegative raster padding.");
1048
2570
  if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid timeline overflow policy.");
1049
- const outlines = options.textMeasurement?.outlineBounds !== void 0;
2571
+ const outlines = options.textMeasurement?.outlineBounds !== void 0, rtl = options.direction === "rtl";
1050
2572
  if (outlines && typeof options.textMeasurement?.outlineBounds !== "function") throw new TypeError("Text outline provider must be a function.");
1051
2573
  const path = options.path ?? "timeline", eventPath = (index) => shorthand ? `${path}.${index}` : `${path}.events.${index}`;
1052
2574
  const fonts = resolveFontFamilies(options.fonts), source = [];
1053
- const add = (role, text, partPath, size, weight, eventIndex) => {
1054
- if (text === void 0) return;
2575
+ const add = (role, text2, partPath, size, weight, eventIndex) => {
2576
+ if (text2 === void 0) return;
1055
2577
  const requestedStyle = { fontFamily: fonts.body, fontWeight: weight, italic: false, path: partPath };
1056
- source.push({ role, eventIndex, path: partPath, text, sources: [{ path: partPath, start: 0, end: text.length }], box: { ...box }, alignment: "center", requestedFontSize: Math.max(size * scale, minimum), minFontSize: minimum, requestedStyle, style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement) });
2578
+ source.push({ role, eventIndex, path: partPath, text: text2, sources: [{ path: partPath, start: 0, end: text2.length }], box: { ...box }, alignment: "center", requestedFontSize: gridFontSize(size * scale, minimum), minFontSize: minimum, requestedStyle, style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement) });
1057
2579
  };
1058
2580
  add("name", timeline.name, `${path}.name`, 24, 700);
1059
2581
  add("description", timeline.description, `${path}.description`, 18, 400);
@@ -1070,7 +2592,7 @@ function layoutTimeline(value, box, options = {}) {
1070
2592
  const key = JSON.stringify([part.path, size, width, alignment]);
1071
2593
  let measured = cache2.get(key);
1072
2594
  if (measured) return measured;
1073
- const fit = fitText(part.text, { width: Math.max(Number.MIN_VALUE, width - (outlines ? 2 * padding : 0)), height: Number.MAX_VALUE }, size, size, textWidthMeasurer(part.style, options.textMeasurement));
2595
+ const fit = fitText(part.text, { width: Math.max(Number.MIN_VALUE, width - (outlines ? 2 * padding : 0)), height: Number.MAX_VALUE }, size, size, textWidthMeasurer(part.style, options.textMeasurement), options.direction);
1074
2596
  const ink = fit.sourceLines.map((line, index) => {
1075
2597
  let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity, hasInk = false;
1076
2598
  if (outlines) {
@@ -1087,7 +2609,7 @@ function layoutTimeline(value, box, options = {}) {
1087
2609
  }
1088
2610
  return { width: line.width, y: index * fit.lineHeight, baseline: size + index * fit.lineHeight, height: fit.lineHeight, outline: hasInk ? { x: left, y: top, width: right - left, height: bottom - top } : null };
1089
2611
  });
1090
- const placement = outlines ? placeTextLines(ink, { x: 0, y: 0, width, height: Number.MAX_VALUE }, alignment, padding) : void 0;
2612
+ const placement = outlines ? placeTextLines(ink, { x: 0, y: 0, width, height: Number.MAX_VALUE }, alignment, padding, fit.directions) : void 0;
1091
2613
  const height = placement?.height ?? fit.sourceLines.length * fit.lineHeight;
1092
2614
  if (!Number.isFinite(height)) throw new RangeError("Timeline text exceeds finite layout coordinates.");
1093
2615
  measured = { fit, ink, height };
@@ -1108,8 +2630,8 @@ function layoutTimeline(value, box, options = {}) {
1108
2630
  score2 += Math.max(1, excess);
1109
2631
  };
1110
2632
  const place = (part, x, y2, width, alignment) => {
1111
- const measured = measure(part, Math.max(minimum, part.requestedFontSize - reduction * scale), Math.max(scale, width), alignment);
1112
- const area = { x, y: y2, width: Math.max(scale, width), height: Math.max(scale, measured.height) }, placement = outlines ? placeTextLines(measured.ink, area, alignment, padding) : void 0;
2633
+ const measured = measure(part, gridFontSize(part.requestedFontSize - reduction * scale, minimum), Math.max(scale, width), alignment);
2634
+ const area = { x, y: y2, width: Math.max(scale, width), height: Math.max(scale, measured.height) }, placement = outlines ? placeTextLines(measured.ink, area, alignment, padding, measured.fit.directions) : void 0;
1113
2635
  const fit = { ...measured.fit, ...placement ? { placement } : {}, overflow: measured.fit.overflow || !!placement?.overflow };
1114
2636
  const accepted = { ...part, box: area, alignment, fit };
1115
2637
  parts.push(accepted);
@@ -1127,7 +2649,7 @@ function layoutTimeline(value, box, options = {}) {
1127
2649
  const width = box.width / Math.max(2, count), step = (box.width - width) / Math.max(1, count - 1), start = count === 1 ? box.x + box.width / 2 : box.x + width / 2;
1128
2650
  const lineY = y + Math.max(scale, available) * 0.46;
1129
2651
  events.forEach((fields2, index) => {
1130
- const x = start + index * step, top = index % 2 === 0 ? y : lineY + 24 * scale, bottom = index % 2 === 0 ? lineY - 24 * scale : box.y + box.height;
2652
+ const x = start + (rtl ? count - 1 - index : index) * step, top = index % 2 === 0 ? y : lineY + 24 * scale, bottom = index % 2 === 0 ? lineY - 24 * scale : box.y + box.height;
1131
2653
  markers.push({ path: eventPath(index), eventIndex: index, x, y: lineY, radius });
1132
2654
  let cursor = top;
1133
2655
  for (const part of fields2) {
@@ -1137,7 +2659,7 @@ function layoutTimeline(value, box, options = {}) {
1137
2659
  }
1138
2660
  });
1139
2661
  } else {
1140
- const x = box.x + radius, textX = box.x + 24 * scale, width = box.width - 24 * scale;
2662
+ const x = rtl ? box.x + box.width - radius : box.x + radius, textX = rtl ? box.x : box.x + 24 * scale, width = box.width - 24 * scale;
1141
2663
  events.forEach((fields2, index) => {
1142
2664
  const top = y;
1143
2665
  let first2;
@@ -1154,7 +2676,7 @@ function layoutTimeline(value, box, options = {}) {
1154
2676
  diagnostics.push({ code: "text-overflow", reason: "event-space", path: marker.path, message: "Timeline marker has no usable space; increase the cell or paginate events." });
1155
2677
  score2 += 1;
1156
2678
  }
1157
- const first = markers[0], last = markers.at(-1), connector = { x1: first.x, y1: first.y, x2: last.x, y2: last.y };
2679
+ const first = markers[0], last = markers.at(-1), connector = rtl ? { x1: Math.min(first.x, last.x), y1: first.y, x2: Math.max(first.x, last.x), y2: last.y } : { x1: first.x, y1: first.y, x2: last.x, y2: last.y };
1158
2680
  const candidate = { arrangement, parts, markers, connector, diagnostics, score: score2 };
1159
2681
  if (!selected || score2 < selected.score) selected = candidate;
1160
2682
  if (!diagnostics.length) break;
@@ -1165,26 +2687,27 @@ function layoutTimeline(value, box, options = {}) {
1165
2687
  if (result.diagnostics.length && options.overflow === "error") throw new OPFCompositionError(result.diagnostics);
1166
2688
  return { algorithm: "timeline-flow-v1", attempts, textMeasurement: options.textMeasurement ? "provided" : "estimated", textOutlines: outlines ? "provided" : "unavailable", ...result, overflow: result.diagnostics.length > 0 };
1167
2689
  }
1168
- function fitCodeText(text, box, size, minimum, step, measure) {
1169
- return fitSourceText(text, box, size, minimum, step, measure);
2690
+ function fitCodeText(text2, box, size, minimum, step, measure, direction) {
2691
+ return fitSourceText(text2, box, size, minimum, step, measure, false, true, direction);
1170
2692
  }
1171
- function fitSourceText(text, box, size, minimum, step, measure, prose = false) {
2693
+ function fitSourceText(text2, box, size, minimum, step, measure, prose = false, snap = true, direction) {
2694
+ const directionAt = direction === "rtl" ? paragraphDirectionAt(text2, "rtl") : void 0;
1172
2695
  const layout = (fontSize) => {
1173
2696
  const tabWidth = measure(" ", fontSize) * 4;
1174
- if (!Number.isFinite(tabWidth) || text.includes(" ") && tabWidth <= 0) throw new RangeError("Text tabs require a positive finite measured space advance.");
1175
- const measureLine = (start2, end2, record3 = false) => {
2697
+ if (!Number.isFinite(tabWidth) || text2.includes(" ") && tabWidth <= 0) throw new RangeError("Text tabs require a positive finite measured space advance.");
2698
+ const measureLine = (start2, end2, record5 = false) => {
1176
2699
  let x = 0, offset = start2;
1177
2700
  const segments = [];
1178
- for (const [index, segment] of text.slice(start2, end2).split(" ").entries()) {
2701
+ for (const [index, segment] of text2.slice(start2, end2).split(" ").entries()) {
1179
2702
  if (index) {
1180
2703
  const next = (Math.floor(x / tabWidth + 1e-9) + 1) * tabWidth;
1181
- if (record3) segments.push({ kind: "tab", start: offset, end: offset + 1, x, width: next - x });
2704
+ if (record5) segments.push({ kind: "tab", start: offset, end: offset + 1, x, width: next - x });
1182
2705
  x = next;
1183
2706
  offset++;
1184
2707
  }
1185
2708
  if (segment) {
1186
2709
  const advance = measure(segment, fontSize);
1187
- if (record3) segments.push({ kind: "text", start: offset, end: offset + segment.length, x, width: advance });
2710
+ if (record5) segments.push({ kind: "text", start: offset, end: offset + segment.length, x, width: advance });
1188
2711
  x += advance;
1189
2712
  offset += segment.length;
1190
2713
  }
@@ -1193,26 +2716,34 @@ function fitSourceText(text, box, size, minimum, step, measure, prose = false) {
1193
2716
  };
1194
2717
  const width = (start2, end2) => measureLine(start2, end2).width;
1195
2718
  const sourceLines = [];
1196
- let start = 0, end = 0;
2719
+ let start = 0, end = 0, hangFrom = -1;
1197
2720
  const push = (last, nextStart, boundary) => {
1198
- sourceLines.push({ start, end: last, nextStart, boundary, ...measureLine(start, last, true) });
2721
+ const measured = measureLine(start, last, true);
2722
+ if (hangFrom >= 0 && hangFrom < last) measured.width = measureLine(start, hangFrom).width;
2723
+ hangFrom = -1;
2724
+ sourceLines.push({ start, end: last, nextStart, boundary, ...measured });
1199
2725
  start = nextStart;
1200
2726
  end = nextStart;
1201
2727
  };
1202
2728
  const tokens = prose ? /\r\n|\r|\n|[^\S\r\n\u00a0\u202f\ufeff]+|(?:[^\s]|\u00a0|\u202f|\ufeff)+/gu : /\r\n|\r|\n|[^\S\r\n]+|[^\s]+/gu;
1203
- for (const token of text.matchAll(tokens)) {
2729
+ for (const token of text2.matchAll(tokens)) {
1204
2730
  if (token.index === void 0) throw new Error("Missing code token source offset.");
1205
2731
  const a = token.index, b = a + token[0].length;
1206
2732
  if (/^[\r\n]/.test(token[0])) {
1207
2733
  push(a, b, "hard");
1208
2734
  continue;
1209
2735
  }
1210
- if (width(start, b) <= box.width + 0.01) {
2736
+ if (width(start, b) <= box.width + WRAP_TOLERANCE) {
2737
+ end = b;
2738
+ continue;
2739
+ }
2740
+ if (prose && end > start && /\S/u.test(text2.slice(start, end)) && HANGING_SPACE.test(token[0][0])) {
2741
+ if (hangFrom < 0) hangFrom = end;
1211
2742
  end = b;
1212
2743
  continue;
1213
2744
  }
1214
- if (end > start && /\S/u.test(text.slice(start, end))) push(a, a, "soft");
1215
- if (width(start, b) <= box.width + 0.01) {
2745
+ if (end > start && /\S/u.test(text2.slice(start, end))) push(a, a, "soft");
2746
+ if (width(start, b) <= box.width + WRAP_TOLERANCE) {
1216
2747
  end = b;
1217
2748
  continue;
1218
2749
  }
@@ -1229,39 +2760,40 @@ function fitSourceText(text, box, size, minimum, step, measure, prose = false) {
1229
2760
  push(end, end, "end");
1230
2761
  const lineHeight = fontSize * 1.22;
1231
2762
  return {
1232
- lines: sourceLines.map((line) => text.slice(line.start, line.end)),
2763
+ lines: sourceLines.map((line) => text2.slice(line.start, line.end)),
1233
2764
  sourceLines,
1234
2765
  fontSize,
1235
2766
  lineHeight,
1236
2767
  tabSize: 4,
1237
2768
  tabWidth,
2769
+ ...directionAt ? { directions: sourceLines.map((line) => directionAt(line.start)) } : {},
1238
2770
  overflow: sourceLines.length * lineHeight > box.height + 0.01 || sourceLines.some((line) => line.width > box.width + 0.01)
1239
2771
  };
1240
2772
  };
1241
- return fitAtSizes(layout, size, minimum, step);
2773
+ return fitAtSizes(layout, size, minimum, step, snap);
1242
2774
  }
1243
2775
  function layoutCode(value, box, options = {}) {
1244
2776
  const shorthand = typeof value === "string", code = shorthand ? { source: value } : value;
1245
2777
  if (!code || Array.isArray(code) || typeof code.source !== "string" || [code.language, code.filename].some((field) => field !== void 0 && typeof field !== "string")) {
1246
2778
  throw new TypeError("Code content must be a string or source object with optional string language/filename.");
1247
2779
  }
1248
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
2780
+ const scale = options.scale ?? 1, minimum = snapFontSizeUp((options.minFontSize ?? 16) * scale);
1249
2781
  if (![box.x, box.y, box.width, box.height, box.x + box.width, box.y + box.height, scale, minimum, Math.max(18 * scale, minimum) * 1.22].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0) {
1250
2782
  throw new RangeError("Code dimensions, scale and minimum font size must be finite and positive.");
1251
2783
  }
1252
2784
  if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid code overflow policy.");
1253
2785
  const path = options.path ?? "code", inner = { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - 36 };
1254
2786
  const parts = [], diagnostics = [];
1255
- const add = (role, text, partPath, generated = false) => {
2787
+ const add = (role, text2, partPath, generated = false) => {
1256
2788
  const requestedStyle = { fontFamily: options.fonts?.code ?? "monospace", fontWeight: role === "body" ? 400 : 700, italic: false, path: partPath };
1257
2789
  const part = {
1258
2790
  role,
1259
2791
  path: partPath,
1260
- text,
1261
- sources: generated ? [] : [{ path: partPath, start: 0, end: text.length }],
2792
+ text: text2,
2793
+ sources: generated ? [] : [{ path: partPath, start: 0, end: text2.length }],
1262
2794
  generated,
1263
2795
  box: { ...inner },
1264
- requestedFontSize: Math.max((role === "body" ? 18 : 14) * scale, minimum),
2796
+ requestedFontSize: gridFontSize((role === "body" ? 18 : 14) * scale, minimum),
1265
2797
  minFontSize: minimum,
1266
2798
  requestedStyle,
1267
2799
  style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement)
@@ -1318,7 +2850,7 @@ function layoutCode(value, box, options = {}) {
1318
2850
  }
1319
2851
  function fitRichText(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
1320
2852
  if (![box.width, box.height, requestedSize, minFontSize].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Rich text dimensions and font sizes must be finite and positive.");
1321
- const layout = richTextLayouter(input, box, requestedSize, options);
2853
+ const layout = richTextLayouter(input, box, requestedSize, options, minFontSize);
1322
2854
  return fitAtSizes(layout, requestedSize, richMinimum(input, requestedSize, minFontSize));
1323
2855
  }
1324
2856
  function richMinimum(input, requested, minimum) {
@@ -1332,7 +2864,7 @@ function richMinimum(input, requested, minimum) {
1332
2864
  }
1333
2865
  return visible ? floor : minimum;
1334
2866
  }
1335
- function richTextLayouter(input, box, requestedSize, options) {
2867
+ function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
1336
2868
  let offset = 0;
1337
2869
  const source = input.map((value, runIndex) => {
1338
2870
  const run = typeof value === "string" ? { text: value } : value;
@@ -1340,23 +2872,26 @@ function richTextLayouter(input, box, requestedSize, options) {
1340
2872
  const start = offset;
1341
2873
  offset += run.text.length;
1342
2874
  const style = resolveTextStyle({ ...options.style, fontFamily: run.fontFamily ?? options.style.fontFamily, fontWeight: run.bold === void 0 ? options.style.fontWeight : run.bold ? 700 : 400, italic: run.italic ?? options.style.italic, path: options.style.path ? `${options.style.path}.${runIndex}` : void 0 }, options.textMeasurement);
1343
- return { run, runIndex, start, end: offset, style };
2875
+ const marker = options.citationMarker && options.style.path && run.text ? options.citationMarker(`${options.style.path}.${runIndex}`) : void 0;
2876
+ return { run, runIndex, start, end: offset, style, ...marker ? { marker } : {} };
1344
2877
  });
1345
2878
  const whole = source.map((entry) => entry.run.text).join("");
1346
- const layout = (fontSize) => {
1347
- const ratio = fontSize / requestedSize;
2879
+ const paint = (raw) => raw >= minimum - 1e-6 ? gridFontSize(raw, minimum) : snapFontSizeDown(raw);
2880
+ const directionAt = options.direction === "rtl" ? paragraphDirectionAt(whole, "rtl") : void 0;
2881
+ const layout = (rawFontSize) => {
2882
+ const fontSize = paint(rawFontSize), ratio = rawFontSize / requestedSize;
1348
2883
  const fragments = (start, end) => {
1349
2884
  let x = 0;
1350
2885
  const result = [];
1351
2886
  for (const entry of source) {
1352
2887
  const a = Math.max(start, entry.start), b = Math.min(end, entry.end);
1353
2888
  if (b <= a) continue;
1354
- const text = whole.slice(a, b), normalSize = (entry.run.fontSize !== void 0 ? entry.run.fontSize * 4 / 3 : requestedSize) * ratio;
2889
+ const text2 = whole.slice(a, b), normalSize = entry.run.fontSize !== void 0 ? entry.run.fontSize * 4 / 3 * ratio : fontSize;
1355
2890
  const script = entry.run.superscript || entry.run.subscript;
1356
- const size = normalSize * (script ? 0.7 : 1), baselineShift = entry.run.superscript ? -normalSize * 0.35 : entry.run.subscript ? normalSize * 0.2 : 0;
2891
+ const size = script ? paint(normalSize * 0.7) : paint(normalSize), baselineShift = entry.run.superscript ? -normalSize * 0.35 : entry.run.subscript ? normalSize * 0.2 : 0;
1357
2892
  const measure = textWidthMeasurer(entry.style, options.textMeasurement);
1358
2893
  let cursor = 0;
1359
- for (const [index, segment] of text.split(" ").entries()) {
2894
+ for (const [index, segment] of text2.split(" ").entries()) {
1360
2895
  if (index) {
1361
2896
  const tabStart = a + cursor, tabWidth = measure(" ", size) * 4;
1362
2897
  if (!Number.isFinite(tabWidth) || tabWidth <= 0) throw new RangeError("Rich text tabs require a positive finite measured space advance.");
@@ -1372,13 +2907,20 @@ function richTextLayouter(input, box, requestedSize, options) {
1372
2907
  cursor += segment.length;
1373
2908
  }
1374
2909
  }
2910
+ if (entry.marker && b === entry.end) {
2911
+ const markerSize = Math.round(normalSize * CITATION_MARKER_SCALE * FONT_SIZE_GRID_PER_PX) / FONT_SIZE_GRID_PER_PX, width2 = measure(entry.marker, markerSize);
2912
+ result.push({ kind: "marker", text: entry.marker, runIndex: entry.runIndex, start: entry.run.text.length, end: entry.run.text.length, x, width: width2, fontSize: markerSize, nominalSize: normalSize, baselineShift: -normalSize * CITATION_MARKER_RAISE, style: entry.style, run: entry.run });
2913
+ x += width2;
2914
+ }
1375
2915
  }
1376
2916
  return result;
1377
2917
  };
1378
2918
  const width = (start, end) => fragments(start, end).reduce((sum, fragment) => sum + fragment.width, 0);
1379
- const ranges = [];
1380
- let lineStart = 0, lineEnd = 0;
2919
+ const ranges = [], hung = /* @__PURE__ */ new Map();
2920
+ let lineStart = 0, lineEnd = 0, hangFrom = -1;
1381
2921
  const push = (end) => {
2922
+ if (hangFrom >= 0) hung.set(ranges.length, hangFrom);
2923
+ hangFrom = -1;
1382
2924
  ranges.push({ start: lineStart, end });
1383
2925
  lineStart = end;
1384
2926
  lineEnd = end;
@@ -1391,26 +2933,32 @@ function richTextLayouter(input, box, requestedSize, options) {
1391
2933
  lineEnd = end;
1392
2934
  continue;
1393
2935
  }
1394
- if (width(lineStart, end) <= box.width) {
2936
+ if (width(lineStart, end) <= box.width + WRAP_TOLERANCE) {
2937
+ lineEnd = end;
2938
+ continue;
2939
+ }
2940
+ if (HANGING_SPACE.test(match[0][0]) && lineEnd > lineStart && /\S/u.test(whole.slice(lineStart, lineEnd))) {
2941
+ if (hangFrom < 0) hangFrom = lineEnd;
1395
2942
  lineEnd = end;
1396
2943
  continue;
1397
2944
  }
1398
2945
  if (lineEnd > lineStart) push(start);
1399
- if (width(start, end) <= box.width) {
2946
+ if (width(start, end) <= box.width + WRAP_TOLERANCE) {
1400
2947
  lineEnd = end;
1401
2948
  continue;
1402
2949
  }
1403
2950
  for (const { segment, index } of textSegments.segment(match[0])) {
1404
2951
  const next = start + index + segment.length;
1405
- if (lineEnd > lineStart && width(lineStart, next) > box.width) push(start + index);
2952
+ if (lineEnd > lineStart && width(lineStart, next) > box.width + WRAP_TOLERANCE) push(start + index);
1406
2953
  lineEnd = next;
1407
2954
  }
1408
2955
  }
2956
+ if (hangFrom >= 0) hung.set(ranges.length, hangFrom);
1409
2957
  ranges.push({ start: lineStart, end: lineEnd });
1410
2958
  let y = 0;
1411
- const richLines = ranges.map((range) => {
1412
- const parts = fragments(range.start, range.end), ascent = Math.max(fontSize, ...parts.map((part) => part.fontSize - part.baselineShift)), descent = Math.max(fontSize * 0.22, ...parts.map((part) => part.fontSize * 0.22 + part.baselineShift));
1413
- const height = ascent + descent, line = { fragments: parts, width: parts.reduce((sum, p) => sum + p.width, 0), y, baseline: y + ascent, height };
2959
+ const richLines = ranges.map((range, rangeIndex) => {
2960
+ const parts = fragments(range.start, range.end), shown = hung.get(rangeIndex) ?? range.end, ascent = Math.max(fontSize, ...parts.map((part) => part.fontSize - part.baselineShift)), descent = Math.max(fontSize * 0.22, ...parts.map((part) => part.fontSize * 0.22 + part.baselineShift));
2961
+ const height = ascent + descent, line = { fragments: parts, width: shown === range.end ? parts.reduce((sum, p) => sum + p.width, 0) : width(range.start, shown), y, baseline: y + ascent, height };
1414
2962
  y += height;
1415
2963
  return line;
1416
2964
  });
@@ -1424,7 +2972,15 @@ function richTextLayouter(input, box, requestedSize, options) {
1424
2972
  y += height;
1425
2973
  }
1426
2974
  }
1427
- return { lines: ranges.map((range) => whole.slice(range.start, range.end)), fontSize, lineHeight: Math.max(fontSize * 1.22, ...richLines.map((line) => line.height)), richLines, height: y, overflow: y > box.height + 0.01 || richLines.some((line) => line.width > box.width + 0.01) };
2975
+ return {
2976
+ lines: ranges.map((range) => whole.slice(range.start, range.end)),
2977
+ fontSize,
2978
+ lineHeight: Math.max(fontSize * 1.22, ...richLines.map((line) => line.height)),
2979
+ richLines,
2980
+ height: y,
2981
+ overflow: y > box.height + 0.01 || richLines.some((line) => line.width > box.width + 0.01),
2982
+ ...directionAt ? { directions: ranges.map((range) => directionAt(range.start)) } : {}
2983
+ };
1428
2984
  };
1429
2985
  return layout;
1430
2986
  }
@@ -1433,36 +2989,50 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
1433
2989
  if (!Array.isArray(input) || ![box.width, box.height, requestedSize, minFontSize].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("List content, dimensions and font sizes must be valid.");
1434
2990
  const source = input.map((value, index) => {
1435
2991
  const object = typeof value === "object" && !Array.isArray(value) ? value : void 0;
1436
- const text = object ? object.text : value, description = object?.description, level = object?.level ?? 0;
2992
+ const text2 = object ? object.text : value, description = object?.description, level = object?.level ?? 0;
1437
2993
  if (!Number.isInteger(level) || level < 0) throw new RangeError("List levels must be nonnegative integers.");
1438
2994
  const runs = (value2) => typeof value2 === "string" ? [value2] : value2;
1439
- if (!Array.isArray(runs(text)) || description !== void 0 && !Array.isArray(runs(description))) throw new TypeError("List text and descriptions must be strings or text runs.");
2995
+ if (!Array.isArray(runs(text2)) || description !== void 0 && !Array.isArray(runs(description))) throw new TypeError("List text and descriptions must be strings or text runs.");
1440
2996
  const base = options.style.path ? `${options.style.path}.${index}` : void 0;
1441
- return { index, level, value: text, descriptionValue: description, textPath: base ? base + (object ? ".text" : "") : void 0, descriptionPath: base ? base + ".description" : void 0, runs: runs(text), descriptionRuns: description === void 0 ? void 0 : runs(description) };
2997
+ const plain = (value2) => typeof value2 === "string" ? value2 : value2.map((run) => typeof run === "string" ? run : run.text).join("");
2998
+ const direction = options.direction === "rtl" ? paragraphDirection(plain(text2), "rtl") : void 0;
2999
+ return { index, level, direction, value: text2, descriptionValue: description, textPath: base ? base + (object ? ".text" : "") : void 0, descriptionPath: base ? base + ".description" : void 0, runs: runs(text2), descriptionRuns: description === void 0 ? void 0 : runs(description) };
1442
3000
  });
3001
+ const numbers = options.numbering === void 0 ? void 0 : listNumbers(input, options.numbering);
3002
+ const markerStyleFor = (item) => {
3003
+ const first = item.runs.map((run) => typeof run === "string" ? { text: run } : run).find((run) => run.text);
3004
+ return { ...options.style, path: item.textPath, fontWeight: first?.bold === void 0 ? options.style.fontWeight : first.bold ? 700 : 400, italic: first?.italic ?? options.style.italic };
3005
+ };
1443
3006
  const layout = (fontSize) => {
1444
3007
  let y = box.y, overflow = false;
1445
- const entries = [], lines = [];
3008
+ const entries = [], lines = [], directions = [];
3009
+ const markerStyles = numbers ? source.map((item) => resolveTextStyle(markerStyleFor(item), options.textMeasurement)) : [];
3010
+ const markerWidths = numbers ? numbers.map((number, index) => textWidthMeasurer(markerStyles[index], options.textMeasurement)(number.text, fontSize)) : [];
3011
+ const hanging = numbers ? Math.max(fontSize * 1.1, Math.max(0, ...markerWidths) + fontSize * 0.3) : fontSize * 1.1;
1446
3012
  for (const item of source) {
1447
- const indent = fontSize * 1.1, offset = Math.min(box.width, item.level * indent), x = box.x + offset + indent, width = Math.max(1, box.x + box.width - x);
3013
+ const rtl = item.direction === "rtl";
3014
+ const indent = hanging, offset = Math.min(box.width, item.level * indent), x = rtl ? box.x : box.x + offset + indent, width = Math.max(1, rtl ? box.width - offset - indent : box.x + box.width - x);
1448
3015
  if (offset + indent >= box.width) overflow = true;
1449
3016
  const textBox = { x, y, width, height: box.height };
1450
3017
  const style = { ...options.style, path: item.textPath };
1451
- const text = richTextLayouter(item.runs, textBox, requestedSize, { ...options, style })(fontSize);
1452
- textBox.height = text.height;
1453
- lines.push(...text.lines);
1454
- y += text.height;
3018
+ const text2 = richTextLayouter(item.runs, textBox, requestedSize, { ...options, style }, minFontSize)(fontSize);
3019
+ textBox.height = text2.height;
3020
+ lines.push(...text2.lines);
3021
+ if (options.direction === "rtl") directions.push(...text2.directions ?? []);
3022
+ y += text2.height;
1455
3023
  let description, descriptionBox;
1456
3024
  if (item.descriptionRuns !== void 0) {
1457
3025
  y += fontSize * 0.12;
1458
3026
  descriptionBox = { x, y, width, height: box.height };
1459
- description = richTextLayouter(item.descriptionRuns, descriptionBox, requestedSize * 0.82, { ...options, style: { ...options.style, path: item.descriptionPath } })(fontSize * 0.82);
3027
+ description = richTextLayouter(item.descriptionRuns, descriptionBox, requestedSize * 0.82, { ...options, style: { ...options.style, path: item.descriptionPath } }, minFontSize)(fontSize * 0.82);
1460
3028
  descriptionBox.height = description.height;
1461
3029
  lines.push(...description.lines);
3030
+ if (options.direction === "rtl") directions.push(...description.directions ?? []);
1462
3031
  y += description.height;
1463
3032
  }
1464
- overflow ||= text.overflow || !!description?.overflow;
1465
- const marker = { text: ["\u2022", "\u25E6", "\u25AA"][item.level % 3], x: box.x + offset, y: textBox.y + (text.richLines[0]?.baseline ?? fontSize), fontSize, style: resolveTextStyle(style, options.textMeasurement), indent }, side = fontSize * PICTURE_BULLET_SCALE;
3033
+ overflow ||= text2.overflow || !!description?.overflow;
3034
+ const number = numbers?.[item.index];
3035
+ const marker = { text: number ? number.text : ["\u2022", "\u25E6", "\u25AA"][item.level % 3], x: rtl ? box.x + box.width - offset : box.x + offset, y: textBox.y + (text2.richLines[0]?.baseline ?? fontSize), fontSize, style: number ? markerStyles[item.index] : resolveTextStyle(style, options.textMeasurement), indent, ...number ? { number, width: markerWidths[item.index] } : {}, ...rtl ? { anchor: "end" } : {} }, side = fontSize * PICTURE_BULLET_SCALE;
1466
3036
  entries.push({
1467
3037
  index: item.index,
1468
3038
  level: item.level,
@@ -1470,17 +3040,18 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
1470
3040
  descriptionValue: item.descriptionValue,
1471
3041
  textPath: item.textPath,
1472
3042
  descriptionPath: item.descriptionPath,
1473
- text,
3043
+ text: text2,
1474
3044
  textBox,
1475
3045
  description,
1476
3046
  descriptionBox,
1477
3047
  marker,
1478
- ...options.bulletImage ? { bulletImage: options.bulletImage, bulletBox: { x: marker.x, y: marker.y - side, width: side, height: side } } : {}
3048
+ ...item.direction ? { direction: item.direction } : {},
3049
+ ...options.bulletImage && !number ? { bulletImage: options.bulletImage, bulletBox: { x: rtl ? marker.x - side : marker.x, y: marker.y - side, width: side, height: side } } : {}
1479
3050
  });
1480
3051
  if (item.index < source.length - 1) y += fontSize * 0.28;
1481
3052
  }
1482
3053
  const height = y - box.y;
1483
- return { listEntries: entries, lines, fontSize, lineHeight: fontSize * 1.22, height, overflow: overflow || height > box.height + 0.01 };
3054
+ return { listEntries: entries, lines, fontSize, lineHeight: fontSize * 1.22, height, overflow: overflow || height > box.height + 0.01, ...options.direction === "rtl" ? { directions } : {} };
1484
3055
  };
1485
3056
  let minimum = minFontSize;
1486
3057
  for (const item of source) {
@@ -1492,16 +3063,16 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
1492
3063
  function contentText(field, value) {
1493
3064
  if (field === "text" || headings.has(field)) return flatten(value);
1494
3065
  if (field === "items" || field === "bullets") return (Array.isArray(value) ? value : [value]).map((item) => {
1495
- const entry = record2(item);
3066
+ const entry = record4(item);
1496
3067
  return `\u2022 ${flatten(item)}${entry.description ? `
1497
3068
  ${flatten(entry.description)}` : ""}`;
1498
3069
  }).join("\n");
1499
- if (field === "code") return flatten(record2(value).source ?? value);
1500
- if (field === "quote") return flatten(record2(value).text ?? value);
3070
+ if (field === "code") return flatten(record4(value).source ?? value);
3071
+ if (field === "quote") return flatten(record4(value).text ?? value);
1501
3072
  return void 0;
1502
3073
  }
1503
3074
  function layoutTable(value, box, options = {}) {
1504
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale, requested = Math.max(15 * scale, minimum);
3075
+ const scale = options.scale ?? 1, minimum = snapFontSizeUp((options.minFontSize ?? 16) * scale), requested = gridFontSize(15 * scale, minimum);
1505
3076
  if (![box.x, box.y, box.width, box.height, scale, minimum].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0) throw new RangeError("Table dimensions, scale and font sizes must be finite and positive.");
1506
3077
  const grid = tableGrid(value, options.path ?? "table");
1507
3078
  if (grid.issues.length) throw new RangeError(`${grid.issues[0].path}: ${grid.issues[0].message}`);
@@ -1515,13 +3086,13 @@ function layoutTable(value, box, options = {}) {
1515
3086
  const cellMinimum = (cell) => Array.isArray(cell.value) ? richMinimum(cell.value, requested, minimum) : minimum;
1516
3087
  const fitCell = (cell, height, min) => {
1517
3088
  const textBox = { width: Math.max(scale, cell.width), height: Math.max(scale, height) };
1518
- return Array.isArray(cell.value) ? fitRichText(cell.value, textBox, requested, min, { style: cell.textStyle, textMeasurement: options.textMeasurement, uniformLineHeight: true }) : fitText(flatten(cell.value), textBox, requested, min, textWidthMeasurer(resolveTextStyle(cell.textStyle, options.textMeasurement), options.textMeasurement));
3089
+ return Array.isArray(cell.value) ? fitRichText(cell.value, textBox, requested, min, { style: cell.textStyle, textMeasurement: options.textMeasurement, uniformLineHeight: true, direction: options.direction }) : fitText(flatten(cell.value), textBox, requested, min, textWidthMeasurer(resolveTextStyle(cell.textStyle, options.textMeasurement), options.textMeasurement), options.direction);
1519
3090
  };
1520
3091
  const textHeight = (fit) => "height" in fit ? fit.height : fit.lines.length * fit.lineHeight;
1521
3092
  const required = (cell, natural2) => {
1522
- const floor = cellMinimum(cell), size = natural2 ? Math.max(requested, floor) : floor;
3093
+ const floor = cellMinimum(cell), size = gridFontSize(natural2 ? Math.max(requested, floor) : floor, floor);
1523
3094
  const textBox = { width: Math.max(scale, cell.width), height: scale };
1524
- const fit = Array.isArray(cell.value) ? richTextLayouter(cell.value, textBox, requested, { style: cell.textStyle, textMeasurement: options.textMeasurement, uniformLineHeight: true })(size) : fitText(flatten(cell.value), textBox, size, size, textWidthMeasurer(resolveTextStyle(cell.textStyle, options.textMeasurement), options.textMeasurement));
3095
+ const fit = Array.isArray(cell.value) ? richTextLayouter(cell.value, textBox, requested, { style: cell.textStyle, textMeasurement: options.textMeasurement, uniformLineHeight: true }, minimum)(size) : fitText(flatten(cell.value), textBox, size, size, textWidthMeasurer(resolveTextStyle(cell.textStyle, options.textMeasurement), options.textMeasurement));
1525
3096
  return textHeight(fit) + (cell.padding.top + cell.padding.bottom) * scale;
1526
3097
  };
1527
3098
  const natural = Array(grid.rowCount).fill(0), needed = Array(grid.rowCount).fill(0);
@@ -1544,13 +3115,13 @@ function layoutTable(value, box, options = {}) {
1544
3115
  let overflow = false;
1545
3116
  const rows2 = heights.map((height, r) => ({ box: { x: box.x, y: ys[r], width: box.width, height }, cells: [] }));
1546
3117
  for (const cell of cells) {
1547
- const cellBox = { x: box.x + cell.column * cellWidth, y: ys[cell.row], width: cell.colSpan * cellWidth, height: ys[cell.row + cell.rowSpan] - ys[cell.row] };
3118
+ const cellBox = { x: box.x + (options.direction === "rtl" ? columnCount - cell.column - cell.colSpan : cell.column) * cellWidth, y: ys[cell.row], width: cell.colSpan * cellWidth, height: ys[cell.row + cell.rowSpan] - ys[cell.row] };
1548
3119
  const available = cellBox.height - (cell.padding.top + cell.padding.bottom) * scale;
1549
3120
  const fit = fitCell(cell, available, minimum), height = textHeight(fit);
1550
3121
  const offset = cell.style.verticalAlign === "bottom" ? Math.max(0, available - height) : cell.style.verticalAlign === "middle" ? Math.max(0, (available - height) / 2) : 0;
1551
3122
  const textBox = { x: cellBox.x + cell.padding.left * scale, y: cellBox.y + cell.padding.top * scale + offset, width: Math.max(scale, cell.width), height: Math.max(scale, available - offset) };
1552
3123
  overflow ||= cell.width <= 0 || available <= 0 || fit.overflow;
1553
- rows2[cell.row].cells.push({ value: cell.value, input: cell.input, path: cell.valuePath, sourcePath: cell.path, style: cell.style, row: cell.row, column: cell.column, rowSpan: cell.rowSpan, colSpan: cell.colSpan, header: cell.header, rich: Array.isArray(cell.value), box: cellBox, textBox, textStyle: resolveTextStyle(cell.textStyle, options.textMeasurement), fit });
3124
+ rows2[cell.row].cells.push({ value: cell.value, input: cell.input, path: cell.valuePath, sourcePath: cell.path, style: cell.style, row: cell.row, column: cell.column, rowSpan: cell.rowSpan, colSpan: cell.colSpan, header: cell.header, rich: Array.isArray(cell.value), box: cellBox, textBox, textStyle: resolveTextStyle(cell.textStyle, options.textMeasurement), fit, ...fit.directions ? { direction: fit.directions[0] } : {} });
1554
3125
  }
1555
3126
  return { rows: rows2, columnCount, height: sum(heights), overflow };
1556
3127
  }
@@ -1561,7 +3132,7 @@ function flatten(value) {
1561
3132
  if (value == null) return "";
1562
3133
  if (typeof value !== "object") return String(value);
1563
3134
  if (Array.isArray(value)) return value.map(flatten).join("");
1564
- const item = record2(value);
3135
+ const item = record4(value);
1565
3136
  return flatten(item.text ?? item.value ?? item.source ?? JSON.stringify(value));
1566
3137
  }
1567
3138
  function regionParts(key) {
@@ -1617,26 +3188,28 @@ function assertComposition(value) {
1617
3188
  if (value.overflow !== void 0 && !["warn", "error"].includes(value.overflow)) throw new RangeError("Invalid composition.overflow.");
1618
3189
  }
1619
3190
  function composeSlide(input, options = {}) {
1620
- const slide = record2(input), layout = record2(options.layout);
3191
+ const slide = record4(input), layout = record4(options.layout);
1621
3192
  const width = options.width ?? 1280, height = options.height ?? 720;
1622
3193
  if (![width, height].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
1623
3194
  const scale = Math.min(width, height) / 720;
1624
- const hasCards = record2(slide.design).contentBox ?? options.contentBox ?? false;
1625
- const composition = { ...record2(layout.composition), ...record2(slide.composition) };
3195
+ const deckDirection = options.direction ?? resolveSlideDirection(options.presentation, options.slideIndex);
3196
+ const rtl = deckDirection === "rtl", textDirection = rtl ? "rtl" : void 0;
3197
+ const hasCards = record4(slide.design).contentBox ?? options.contentBox ?? false;
3198
+ const composition = { ...record4(layout.composition), ...record4(slide.composition) };
1626
3199
  assertComposition(composition);
1627
3200
  const padding = (composition.padding ?? 0.08) * Math.min(width, height);
1628
3201
  const gap = (composition.gap ?? 1 / 30) * Math.min(width, height);
1629
- const minSize = (composition.minFontSize ?? 16) * scale;
3202
+ const minSize = snapFontSizeUp((composition.minFontSize ?? 16) * scale);
1630
3203
  const rasterPadding = (options.textRasterPadding ?? 1) * scale;
1631
3204
  if (!Number.isFinite(rasterPadding) || rasterPadding < 0) throw new RangeError("Text raster padding must be finite and nonnegative.");
1632
3205
  let coverGroup = false;
1633
- const alignmentFor = (field) => (field === "title" || coverGroup && (field === "tag" || field === "subtitle") && record2(slide.design).contentAlignment === void 0 ? record2(slide.design).titleAlignment ?? options.titleAlignment : record2(slide.design).contentAlignment ?? options.contentAlignment) ?? "left";
3206
+ const alignmentFor = (field) => (field === "title" || coverGroup && (field === "tag" || field === "subtitle") && record4(slide.design).contentAlignment === void 0 ? record4(slide.design).titleAlignment ?? options.titleAlignment : record4(slide.design).contentAlignment ?? options.contentAlignment) ?? "left";
1634
3207
  const styleFor = (field, path2) => resolveTextStyle({
1635
3208
  fontFamily: (field === "title" ? options.fonts?.heading : field === "code" ? options.fonts?.code : field === "tag" ? options.fonts?.accent ?? options.fonts?.body : options.fonts?.body) ?? (field === "code" ? "monospace" : "sans-serif"),
1636
3209
  fontWeight: field === "title" ? 700 : 400,
1637
3210
  path: path2
1638
3211
  }, options.textMeasurement);
1639
- const slideDesign = record2(slide.design), deckDesign = record2(record2(options.presentation).design);
3212
+ const slideDesign = record4(slide.design), deckDesign = record4(record4(options.presentation).design);
1640
3213
  const designHint = (field) => slideDesign[field] !== void 0 ? { value: slideDesign[field], path: `slides.${options.slideIndex ?? 0}.design.${field}` } : deckDesign[field] !== void 0 ? { value: deckDesign[field], path: `design.${field}` } : void 0;
1641
3214
  const listBullet = designHint("listBullet");
1642
3215
  const bulletImage = listBullet?.value === "image" ? (() => {
@@ -1644,15 +3217,18 @@ function composeSlide(input, options = {}) {
1644
3217
  return resolved ? { source: resolved.source, path: resolved.path } : void 0;
1645
3218
  })() : void 0;
1646
3219
  const widthFor = (field, path2) => textWidthMeasurer(styleFor(field, path2), options.textMeasurement);
1647
- const fitPlacedText = (field, value, text, box, size, minimum, path2) => {
3220
+ const citations = slideCitations(slide, options.slideIndex ?? 0, options.presentation);
3221
+ const citationMarker = citations ? (runPath) => citations.markers.get(runPath) : void 0;
3222
+ const richOptions = (style) => ({ style, textMeasurement: options.textMeasurement, ...citationMarker ? { citationMarker } : {}, ...rtl ? { direction: "rtl" } : {} });
3223
+ const fitPlacedText = (field, value, text2, box, size, minimum, path2, explicitAlignment) => {
1648
3224
  const style = styleFor(field, path2), rich = field === "text" && Array.isArray(value);
1649
- if (!options.textMeasurement?.outlineBounds) return rich ? fitRichText(value, box, size, minimum, { style, textMeasurement: options.textMeasurement }) : fitText(text, box, size, minimum, textWidthMeasurer(style, options.textMeasurement));
1650
- const alignment = alignmentFor(field);
1651
- const richLayout = rich ? richTextLayouter(value, box, size, { style, textMeasurement: options.textMeasurement }) : void 0;
1652
- const measure = textWidthMeasurer(style, options.textMeasurement), floor = rich ? richMinimum(value, size, minimum) : minimum, start = Math.max(size, floor);
3225
+ if (!options.textMeasurement?.outlineBounds) return rich ? fitRichText(value, box, size, minimum, richOptions(style)) : fitText(text2, box, size, minimum, textWidthMeasurer(style, options.textMeasurement), textDirection);
3226
+ const alignment = explicitAlignment ?? alignmentFor(field);
3227
+ const richLayout = rich ? richTextLayouter(value, box, size, richOptions(style), minimum) : void 0;
3228
+ const measure = textWidthMeasurer(style, options.textMeasurement), floor = snapFontSizeUp(rich ? richMinimum(value, size, minimum) : minimum), start = Math.max(size, floor);
1653
3229
  for (let trial = 0; trial <= 64; trial++) {
1654
- const fontSize = trial === 64 ? floor : Math.max(floor, start - trial * scale);
1655
- const fit = richLayout ? richLayout(fontSize) : fitText(text, box, fontSize, fontSize, measure);
3230
+ const raw = start - trial * scale, last = trial === 64 || raw <= floor, fontSize = last ? floor : gridFontSize(raw, floor);
3231
+ const fit = richLayout ? richLayout(fontSize) : fitText(text2, box, fontSize, fontSize, measure, textDirection);
1656
3232
  const richLines = "richLines" in fit ? fit.richLines : void 0;
1657
3233
  const lines = richLines ? richLines.map((line) => {
1658
3234
  let outline = null;
@@ -1675,7 +3251,7 @@ function composeSlide(input, options = {}) {
1675
3251
  let outline = null;
1676
3252
  for (const segment of line.segments) {
1677
3253
  if (segment.kind === "tab") continue;
1678
- const bounds = measureTextOutline(text.slice(segment.start, segment.end), fontSize, style, options.textMeasurement);
3254
+ const bounds = measureTextOutline(text2.slice(segment.start, segment.end), fontSize, style, options.textMeasurement);
1679
3255
  if (!bounds) continue;
1680
3256
  const next = { ...bounds, x: segment.x + bounds.x };
1681
3257
  if (!outline) outline = next;
@@ -1689,15 +3265,17 @@ function composeSlide(input, options = {}) {
1689
3265
  }
1690
3266
  return { width: line.width, y: index * fit.lineHeight, baseline: fontSize + index * fit.lineHeight, height: fit.lineHeight, outline };
1691
3267
  });
1692
- const placement = placeTextLines(lines, box, alignment, rasterPadding), result = { ...fit, placement, overflow: fit.overflow || placement.overflow };
1693
- if (!result.overflow || fontSize === floor) return result;
3268
+ const placement = placeTextLines(lines, box, alignment, rasterPadding, fit.directions), result = { ...fit, placement, overflow: fit.overflow || placement.overflow };
3269
+ if (!result.overflow || last) return result;
1694
3270
  }
1695
3271
  throw new Error("Text placement did not evaluate its bounded floor trial.");
1696
3272
  };
1697
- 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, ...bulletImage ? { bulletImage } : {} }) : fitText(text, box, size, minimum, widthFor(field, path2));
3273
+ const fitContent = (field, value, text2, box, size, minimum, path2, numbering) => field === "text" ? fitPlacedText(field, value, text2, box, size, minimum, path2) : field === "items" || field === "bullets" ? fitList(value, box, size, minimum, { ...richOptions(styleFor(field, path2)), ...numbering !== void 0 ? { numbering } : bulletImage ? { bulletImage } : {} }) : fitText(text2, box, size, minimum, widthFor(field, path2), field === "code" ? void 0 : textDirection);
1698
3274
  const path = `slides.${options.slideIndex ?? 0}`;
3275
+ const annotationFit = (value, box, requestedSize, minFontSize, fitPath, alignment) => fitPlacedText("text", value, annotationText(value), box, requestedSize, minFontSize, fitPath, alignment);
3276
+ const annotationOptions = { scale, minFontSize: minSize, fit: annotationFit, textStyle: (fitPath) => styleFor("text", fitPath) };
1699
3277
  const slideImageDiagnostics = [];
1700
- const slideImage = resolveSlideImage(slide, layout, options.presentation, width, height, path, padding, scale, slideImageDiagnostics);
3278
+ const slideImage = resolveSlideImage(slide, layout, options.presentation, width, height, path, padding, scale, slideImageDiagnostics, rtl);
1701
3279
  const area = { left: 0, top: 0, right: width, bottom: height };
1702
3280
  if (slideImage?.position === "left") area.left = slideImage.region.width;
1703
3281
  else if (slideImage?.position === "right") area.right = slideImage.region.x;
@@ -1707,14 +3285,20 @@ function composeSlide(input, options = {}) {
1707
3285
  const measuredFurniture = layoutFurniture(slide, options);
1708
3286
  const furniture = measuredFurniture.configured || measuredFurniture.diagnostics.length ? measuredFurniture : void 0;
1709
3287
  if (furniture) diagnostics.push(...furniture.diagnostics);
1710
- const bodyBottom = Math.min(area.bottom - padding, furniture && furniture.footerTop < height ? furniture.footerTop - gap * 0.5 : height - padding);
3288
+ let bodyBottom = Math.min(area.bottom - padding, furniture && furniture.footerTop < height ? furniture.footerTop - gap * 0.5 : height - padding);
1711
3289
  const headingTop = Math.max(area.top + padding, furniture?.headerBottom ? furniture.headerBottom + gap * 0.5 : padding);
3290
+ let footnotes;
3291
+ if (citations) {
3292
+ footnotes = layoutFootnotes(citations, { ...annotationOptions, x: area.left + padding, width: area.right - area.left - padding * 2, bottom: bodyBottom, maxHeight: Math.max(minSize * 1.22 + 2 * scale, (bodyBottom - headingTop) * FOOTNOTE_MAX_RATIO), path });
3293
+ diagnostics.push(...footnotes.diagnostics);
3294
+ bodyBottom = footnotes.box.y - gap * 0.5;
3295
+ }
1712
3296
  const layoutPlaceholders = Array.isArray(layout.placeholders) ? layout.placeholders : [];
1713
3297
  const headingOnlyLayout = COVER_LAYOUT_IDS.has(String(layout.id ?? "")) || layoutPlaceholders.length > 0 && layoutPlaceholders.every((placeholder) => headings.has(placeholder.type ?? ""));
1714
3298
  const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
1715
3299
  const emptyPayload = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
1716
3300
  const emptyHost = (host) => emptyPayload(host.blocks) && fields.every((field) => emptyPayload(host[field]));
1717
- const hasBodyPayload = regions.some((key) => !emptyHost(record2(slide[key]))) || !emptyPayload(slide.blocks) || fields.some((field) => !emptyPayload(slide[field]));
3301
+ const hasBodyPayload = regions.some((key) => !emptyHost(record4(slide[key]))) || !emptyPayload(slide.blocks) || fields.some((field) => !emptyPayload(slide[field]));
1718
3302
  const isCover = !hasBodyPayload && (headingOnlyLayout || !layout.id && !layoutPlaceholders.some((placeholder) => !headings.has(placeholder.type ?? "")));
1719
3303
  coverGroup = isCover;
1720
3304
  let logo;
@@ -1722,8 +3306,9 @@ function composeSlide(input, options = {}) {
1722
3306
  const resolved = resolveLogo(options.presentation, slide, { slot: "lockup", onDark: options.darkBackground, slideIndex: options.slideIndex });
1723
3307
  if (resolved) {
1724
3308
  const logoHeight = 56 * scale;
1725
- const box = { x: round(area.left + padding), y: round(headingTop), width: round(Math.max(scale, Math.min(4 * logoHeight, area.right - area.left - 2 * padding))), height: round(logoHeight) };
1726
- logo = { box, slot: "lockup", path: resolved.path, source: resolved.source, variant: resolved.variant, anchor: "left" };
3309
+ const logoWidth = Math.max(scale, Math.min(4 * logoHeight, area.right - area.left - 2 * padding));
3310
+ const box = { x: round2(rtl ? area.right - padding - logoWidth : area.left + padding), y: round2(headingTop), width: round2(logoWidth), height: round2(logoHeight) };
3311
+ logo = { box, slot: "lockup", path: resolved.path, source: resolved.source, variant: resolved.variant, anchor: rtl ? "right" : "left" };
1727
3312
  }
1728
3313
  }
1729
3314
  let y = logo ? logo.box.y + logo.box.height + gap : headingTop;
@@ -1733,10 +3318,10 @@ function composeSlide(input, options = {}) {
1733
3318
  const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale;
1734
3319
  const maxHeight = Math.max(height * (field === "title" ? 0.26 : field === "subtitle" ? 0.12 : 0.045), minSize * 1.22 + 2 * rasterPadding);
1735
3320
  const box = { x: area.left + padding, y, width: area.right - area.left - padding * 2, height: maxHeight };
1736
- const text = fitPlacedText(field, slide[field], String(slide[field]), box, requested, minSize, `${path}.${field}`);
1737
- box.height = Math.min(maxHeight, Math.max(text.lines.length * text.lineHeight, text.placement?.height ?? 0));
3321
+ const text2 = fitPlacedText(field, slide[field], String(slide[field]), box, requested, minSize, `${path}.${field}`);
3322
+ box.height = Math.min(maxHeight, Math.max(text2.lines.length * text2.lineHeight, text2.placement?.height ?? 0));
1738
3323
  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." });
1739
- const item = { path: `${path}.${field}`, field, type: "text", value: slide[field], payload: { text: slide[field] }, box, text, textStyle: styleFor(field, `${path}.${field}`), composition, alignment: alignmentFor(field) };
3324
+ const item = { path: `${path}.${field}`, field, type: "text", value: slide[field], payload: { text: slide[field] }, box, text: text2, textStyle: styleFor(field, `${path}.${field}`), composition, alignment: alignmentFor(field) };
1740
3325
  headingItems.push(item);
1741
3326
  items.push(item);
1742
3327
  y += box.height + gap * 0.5;
@@ -1756,7 +3341,7 @@ function composeSlide(input, options = {}) {
1756
3341
  const collect = (host, basePath, depth = 0, ancestors = []) => {
1757
3342
  if (Array.isArray(host.blocks)) {
1758
3343
  if (depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(host)) throw new RangeError(`Content groups must be acyclic and nest at most ${MAX_COMPOSITION_DEPTH} levels.`);
1759
- const settings = record2(host.composition);
3344
+ const settings = record4(host.composition);
1760
3345
  assertComposition(settings);
1761
3346
  return [{
1762
3347
  field: "blocks",
@@ -1765,12 +3350,21 @@ function composeSlide(input, options = {}) {
1765
3350
  path: basePath,
1766
3351
  payload: host,
1767
3352
  composition: settings,
1768
- children: host.blocks.flatMap((block, index) => collect(record2(block), `${basePath}.blocks.${index}`, depth + 1, [...ancestors, host]))
3353
+ children: host.blocks.flatMap((block, index) => collect(record4(block), `${basePath}.blocks.${index}`, depth + 1, [...ancestors, host]))
1769
3354
  }];
1770
3355
  }
1771
- return fields.filter((field) => host[field] !== void 0).map((field) => ({ field, type: host.type ?? kind(field), value: host[field], path: `${basePath}.${field}`, payload: { type: host.type ?? kind(field), [field]: host[field] } }));
3356
+ const captioned = host.caption !== void 0 ? fields.filter((field) => host[field] !== void 0 && CAPTIONABLE_FIELDS.includes(field)) : [];
3357
+ return fields.filter((field) => host[field] !== void 0).map((field) => ({
3358
+ field,
3359
+ type: host.type ?? kind(field),
3360
+ value: host[field],
3361
+ path: `${basePath}.${field}`,
3362
+ payload: { type: host.type ?? kind(field), [field]: host[field], ...host.numbering !== void 0 && (field === "items" || field === "bullets") ? { numbering: host.numbering } : {} },
3363
+ ...captioned.length === 1 && captioned[0] === field ? { caption: host.caption, captionPath: `${basePath}.caption` } : {}
3364
+ }));
1772
3365
  };
1773
- const pending = regions.length ? regions.flatMap((key) => collect(record2(slide[key]), `${path}.${key}`).map((item) => ({ ...item, region: regionParts(key) }))) : Array.isArray(slide.blocks) ? slide.blocks.flatMap((block, index) => collect(record2(block), `${path}.blocks.${index}`)) : collect(slideImage?.replacesContent ? { ...slide, image: void 0 } : slide, path);
3366
+ const regionsInReadingOrder = visualReadingOrder(regions.map((key) => ({ key, box: regionBox(regionParts(key), contentBox, gap) }))).map((entry) => entry.key);
3367
+ const pending = regions.length ? regionsInReadingOrder.flatMap((key) => collect(record4(slide[key]), `${path}.${key}`).map((item) => ({ ...item, region: regionParts(key) }))) : Array.isArray(slide.blocks) ? slide.blocks.flatMap((block, index) => collect(record4(block), `${path}.blocks.${index}`)) : collect(slideImage?.replacesContent ? { ...slide, image: void 0 } : slide, path);
1774
3368
  const groups = [], flows = [];
1775
3369
  const decisions = options.explain ? [] : void 0;
1776
3370
  const inheritedSettings = (parent, own = {}) => ({
@@ -1782,7 +3376,7 @@ function composeSlide(input, options = {}) {
1782
3376
  const amount = (settings.padding ?? 0) * Math.min(box.width, box.height);
1783
3377
  return { x: box.x + amount, y: box.y + amount, width: box.width - amount * 2, height: box.height - amount * 2 };
1784
3378
  };
1785
- const acceptedBox = (box) => ({ x: round(box.x), y: round(box.y), width: round(box.width), height: round(box.height) });
3379
+ const acceptedBox = (box) => ({ x: round2(box.x), y: round2(box.y), width: round2(box.width), height: round2(box.height) });
1786
3380
  const payloadBox = (box) => {
1787
3381
  if (!hasCards) return box;
1788
3382
  const frame = acceptedBox(box), padding2 = Math.min(12 * scale, frame.width / 4, frame.height / 4);
@@ -1793,7 +3387,8 @@ function composeSlide(input, options = {}) {
1793
3387
  textMeasurement: options.textMeasurement,
1794
3388
  scale,
1795
3389
  minFontSize: settings.minFontSize,
1796
- path: node.path
3390
+ path: node.path,
3391
+ direction: textDirection
1797
3392
  });
1798
3393
  const measureCode = (node, box, settings) => layoutCode(node.value, acceptedBox(box), {
1799
3394
  fonts: options.fonts,
@@ -1809,7 +3404,8 @@ function composeSlide(input, options = {}) {
1809
3404
  minFontSize: settings.minFontSize,
1810
3405
  path: node.path,
1811
3406
  textRasterPadding: options.textRasterPadding,
1812
- align: alignmentFor("metric")
3407
+ align: alignmentFor("metric"),
3408
+ direction: textDirection
1813
3409
  });
1814
3410
  const measureTimeline = (node, box, settings) => layoutTimeline(node.value, acceptedBox(box), {
1815
3411
  fonts: options.fonts,
@@ -1817,13 +3413,20 @@ function composeSlide(input, options = {}) {
1817
3413
  scale,
1818
3414
  minFontSize: settings.minFontSize,
1819
3415
  path: node.path,
1820
- textRasterPadding: options.textRasterPadding
3416
+ textRasterPadding: options.textRasterPadding,
3417
+ direction: textDirection
1821
3418
  });
3419
+ const captionOf = (node, box) => node.caption !== void 0 && node.captionPath ? layoutCaption(node.caption, box, node.captionPath, annotationOptions) : void 0;
1822
3420
  const leafScore = (node, box, settings, penalties) => {
1823
3421
  box = payloadBox(box);
1824
- const text = contentText(node.field, node.value);
1825
- let score = Math.abs(Math.log(box.width / box.height / 1.6));
1826
- if (penalties) penalties.cellProportions += score;
3422
+ const captioned = captionOf(node, box);
3423
+ if (captioned) box = captioned.mediaBox;
3424
+ const text2 = contentText(node.field, node.value);
3425
+ let score = Math.abs(Math.log(box.width / box.height / 1.6)) + (captioned?.overflow ? 1e3 : 0);
3426
+ if (penalties) {
3427
+ penalties.cellProportions += Math.abs(Math.log(box.width / box.height / 1.6));
3428
+ if (captioned?.overflow) penalties.textOverflow += 1e3;
3429
+ }
1827
3430
  if (node.field === "quote" || node.field === "code" || node.field === "metric" || node.field === "timeline") {
1828
3431
  const internal = node.field === "quote" ? measureQuote(node, box, settings) : node.field === "code" ? measureCode(node, box, settings) : node.field === "metric" ? measureMetric(node, box, settings) : measureTimeline(node, box, settings);
1829
3432
  const reduction = internal.parts.reduce((sum, part) => sum + (part.fit ? (part.requestedFontSize - part.fit.fontSize) / scale : 0), 0);
@@ -1832,9 +3435,9 @@ function composeSlide(input, options = {}) {
1832
3435
  penalties.fontReduction += reduction;
1833
3436
  penalties.textOverflow += internal.overflow ? 1e3 : 0;
1834
3437
  }
1835
- } else if (text) {
1836
- const fit = fitContent(node.field, node.value, text, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path);
1837
- const reduction = Math.max(0, 25 * scale - fit.fontSize) / scale;
3438
+ } else if (text2) {
3439
+ const fit = fitContent(node.field, node.value, text2, box, 25 * scale, snapFontSizeUp((settings.minFontSize ?? 16) * scale), node.path, node.payload.numbering);
3440
+ const reduction = Math.max(0, snapFontSizeDown(25 * scale) - fit.fontSize) / scale;
1838
3441
  score += reduction + (fit.overflow ? 1e3 : 0);
1839
3442
  if (penalties) {
1840
3443
  penalties.fontReduction += reduction;
@@ -1893,10 +3496,11 @@ function composeSlide(input, options = {}) {
1893
3496
  candidates: candidates ?? []
1894
3497
  });
1895
3498
  const grid = gridGeometry(count, area2, cols, actualGap, settings.weights ?? [], mode === "column");
1896
- const boxes = grid.boxes;
1897
- if (recorded && !hasRegions) flows.push({ path: containerPath, box: { ...area2 }, composition: { ...settings }, columns: grid.columns, rows: grid.rows, gap: grid.gap, itemCount: nodes.length, slotCount: count });
3499
+ const mirrored = (box) => rtl ? { ...box, x: area2.x + area2.width - (box.x - area2.x) - box.width } : box;
3500
+ const boxes = grid.boxes.map(mirrored);
3501
+ 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 });
1898
3502
  nodes.forEach((node, index) => {
1899
- let box = node.region ? regionBox(node.region, area2, actualGap) : boxes[index];
3503
+ let box = node.region ? mirrored(regionBox(node.region, area2, actualGap)) : boxes[index];
1900
3504
  if (node.children) {
1901
3505
  const own = inheritedSettings(settings, node.composition), inner = inset(box, own);
1902
3506
  if (!node.synthetic) groups.push({ path: node.path, box, contentBox: inner, composition: own });
@@ -1904,13 +3508,18 @@ function composeSlide(input, options = {}) {
1904
3508
  } else {
1905
3509
  const frameBox = hasCards ? acceptedBox(box) : void 0;
1906
3510
  box = payloadBox(box);
3511
+ const caption = captionOf(node, box);
3512
+ if (caption) {
3513
+ box = caption.mediaBox;
3514
+ diagnostics.push(...caption.diagnostics);
3515
+ }
1907
3516
  const textValue = contentText(node.field, node.value);
1908
3517
  const quoteLayout = node.field === "quote" ? measureQuote(node, box, settings) : void 0;
1909
3518
  const codeLayout = node.field === "code" ? measureCode(node, box, settings) : void 0;
1910
3519
  const metricLayout = node.field === "metric" ? measureMetric(node, box, settings) : void 0;
1911
3520
  const timelineLayout = node.field === "timeline" ? measureTimeline(node, box, settings) : void 0;
1912
3521
  const internal = quoteLayout ?? codeLayout ?? metricLayout ?? timelineLayout, body = internal?.parts.find((part) => part.role === "body" || part.role === "value");
1913
- const text = internal ? body?.fit : textValue !== void 0 ? fitContent(node.field, node.value, textValue, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path) : void 0;
3522
+ const text2 = internal ? body?.fit : textValue !== void 0 ? fitContent(node.field, node.value, textValue, box, 25 * scale, snapFontSizeUp((settings.minFontSize ?? 16) * scale), node.path, node.payload.numbering) : void 0;
1914
3523
  items.push({
1915
3524
  path: node.path,
1916
3525
  field: node.field,
@@ -1919,7 +3528,7 @@ function composeSlide(input, options = {}) {
1919
3528
  payload: node.payload,
1920
3529
  box: internal ? acceptedBox(box) : box,
1921
3530
  ...frameBox ? { frameBox } : {},
1922
- text,
3531
+ text: text2,
1923
3532
  textStyle: body?.style ?? styleFor(node.field, node.path),
1924
3533
  composition: settings,
1925
3534
  alignment: alignmentFor(node.field),
@@ -1927,7 +3536,8 @@ function composeSlide(input, options = {}) {
1927
3536
  ...codeLayout ? { codeLayout } : {},
1928
3537
  ...metricLayout ? { metricLayout } : {},
1929
3538
  ...timelineLayout ? { timelineLayout } : {},
1930
- ...bulletImage && (node.field === "items" || node.field === "bullets") ? { bulletImage } : {}
3539
+ ...bulletImage && node.payload.numbering === void 0 && (node.field === "items" || node.field === "bullets") ? { bulletImage } : {},
3540
+ ...caption ? { caption } : {}
1931
3541
  });
1932
3542
  if (box.width < 100 * scale || box.height < 60 * scale) diagnostics.push({ code: "small-cell", path: node.path, message: "Content cell is too small for comfortable reading; use fewer blocks or a different composition." });
1933
3543
  }
@@ -1938,7 +3548,7 @@ function composeSlide(input, options = {}) {
1938
3548
  const picture = placeholders.findIndex((p) => p.type === "picture");
1939
3549
  if (picture >= 0) placeholders.splice(picture, 1);
1940
3550
  }
1941
- const ownMode = record2(slide.composition).mode;
3551
+ const ownMode = record4(slide.composition).mode;
1942
3552
  const direction = designHint("contentDirection")?.value;
1943
3553
  const directionMode = direction === "vertical" ? "column" : direction === "horizontal" ? "row" : void 0;
1944
3554
  const layoutDirectionMode = layout.slideLayoutDirection === "Vertical" ? "column" : layout.slideLayoutDirection === "Horizontal" ? "row" : "auto";
@@ -1952,19 +3562,23 @@ function composeSlide(input, options = {}) {
1952
3562
  const container = { field: "blocks", type: "group", value: rest.map((node) => node.payload), path, payload: {}, children: rest, composition: {}, synthetic: true };
1953
3563
  arrange(chartPrimary === "left" || chartPrimary === "top" ? [primary, container] : [container, primary], contentBox, rootSettings, 0);
1954
3564
  } else arrange(pending, contentBox, rootSettings, composition.mode ? 0 : placeholders.length);
1955
- if (listBullet?.value === "image" && !bulletImage && items.some((item) => item.field === "items" || item.field === "bullets")) {
3565
+ if (listBullet?.value === "image" && !bulletImage && items.some((item) => (item.field === "items" || item.field === "bullets") && item.payload.numbering === void 0)) {
1956
3566
  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." });
1957
3567
  }
3568
+ const numberingDiagnostics = [];
1958
3569
  for (const item of items) {
1959
- for (const key of ["x", "y", "width", "height"]) item.box[key] = round(item.box[key]);
3570
+ for (const key of ["x", "y", "width", "height"]) item.box[key] = round2(item.box[key]);
1960
3571
  if (item.field === "table" && tableOverflows(item.value, item.box, scale, item.composition, options, item.path)) diagnostics.push({ code: "text-overflow", path: item.path, message: "Table cells do not fit; use fewer rows, fewer columns, or split the table across slides." });
1961
3572
  if (item.quoteLayout) diagnostics.push(...item.quoteLayout.diagnostics);
1962
3573
  else if (item.codeLayout) diagnostics.push(...item.codeLayout.diagnostics);
1963
3574
  else if (item.metricLayout) diagnostics.push(...item.metricLayout.diagnostics);
1964
3575
  else if (item.timelineLayout) diagnostics.push(...item.timelineLayout.diagnostics);
1965
3576
  else if (item.text?.overflow) diagnostics.push({ code: "text-overflow", path: item.path, message: "Text exceeds its cell at the minimum font size; shorten it, increase its space, or split the slide." });
3577
+ for (const entry of item.text?.listEntries ?? []) {
3578
+ if (entry.marker.number?.adapted) numberingDiagnostics.push({ code: "numbering-adapted", path: entry.textPath ?? item.path, message: `Roman numerals stop at 3999; this entry is numbered ${entry.marker.number.value} in arabic, as PowerPoint draws it.` });
3579
+ }
1966
3580
  }
1967
- 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]);
3581
+ for (const group of groups) for (const box of [group.box, group.contentBox]) for (const key of ["x", "y", "width", "height"]) box[key] = round2(box[key]);
1968
3582
  const strictPaths = new Set(items.filter((item) => item.composition.overflow === "error").map((item) => item.path));
1969
3583
  const failures = diagnostics.filter((diagnostic) => {
1970
3584
  if (composition.overflow === "error") return true;
@@ -1986,8 +3600,8 @@ function composeSlide(input, options = {}) {
1986
3600
  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)
1987
3601
  } : void 0;
1988
3602
  if (failures.length) throw new OPFCompositionError(failures, explanation);
1989
- diagnostics.push(...slideImageDiagnostics);
1990
- return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...furniture ? { furniture } : {}, ...slideImage ? { slideImage } : {}, ...logo ? { logo } : {}, ...explanation ? { explanation } : {} };
3603
+ diagnostics.push(...slideImageDiagnostics, ...numberingDiagnostics);
3604
+ return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...furniture ? { furniture } : {}, ...slideImage ? { slideImage } : {}, ...logo ? { logo } : {}, ...rtl ? { direction: "rtl" } : {}, ...footnotes ? { footnotes } : {}, ...explanation ? { explanation } : {} };
1991
3605
  }
1992
3606
  function resolveCanvasDimensions(input) {
1993
3607
  const presets = {
@@ -1999,7 +3613,7 @@ function resolveCanvasDimensions(input) {
1999
3613
  letter: [11, 8.5],
2000
3614
  a4: [11.69, 8.27]
2001
3615
  };
2002
- const value = record2(input);
3616
+ const value = record4(input);
2003
3617
  const preset = presets[typeof input === "string" ? input : value.preset] ?? presets.widescreen;
2004
3618
  const width = (value.widthInches ?? preset[0]) * 96;
2005
3619
  const height = (value.heightInches ?? preset[1]) * 96;
@@ -2007,4 +3621,4 @@ function resolveCanvasDimensions(input) {
2007
3621
  return { width, height };
2008
3622
  }
2009
3623
 
2010
- export { DEFAULT_FONT_SCHEME, DEFAULT_FURNITURE_DATE_FORMAT, DEFAULT_SLIDE_NUMBER_FORMAT, MAX_COMPOSITION_DEPTH, OPFCompositionError, PICTURE_BULLET_SCALE, chartColorForFill, colorContrast, composeSlide, fitList, fitRichText, fitText, formatFurnitureDate, formatSlideNumber, layoutCode, layoutFurniture, layoutMetric, layoutQuote, layoutTable, layoutTimeline, measureText, measureTextOutline, normalizeHexColor, placeTextLines, resolveCanvasDimensions, resolveColorRef, resolveFontFamilies, resolveFontSchemeReference, resolveLogo, resolveSocialProfile, resolveTextStyle, slideImageShape, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, wrapText };
3624
+ export { CAPTIONABLE_FIELDS, CAPTION_FONT_RATIO, CAPTION_MAX_RATIO, CHART_SERIES_MIN_DIFFERENCE, CHART_SERIES_MIN_LIGHTNESS_STEP, CITATION_MARKER_RAISE, CITATION_MARKER_SCALE, DEFAULT_CHART_LABEL_SEPARATOR, DEFAULT_FONT_SCHEME, DEFAULT_FURNITURE_DATE_FORMAT, DEFAULT_SLIDE_NUMBER_FORMAT, FONT_SIZE_GRID_PER_PX, FOOTNOTE_MAX_RATIO, MAX_COMPOSITION_DEPTH, MAX_NUMBERING_LEVELS, MAX_NUMBERING_VALUE, MAX_ROMAN_VALUE, NUMBERING_STYLES, NUMBERING_SUFFIXES, OPFCompositionError, PICTURE_BULLET_SCALE, annotationText, bareIdPattern, captionSettings, catalogIds, chartColorForFill, chartLabelText, chartOptionSupport, chartOptionTarget, chartPaletteForFill, citationMarkerText, collectCitations, colorContrast, composeSlide, deprecatedCatalogIds, fitList, fitRichText, fitText, formatChartLabelNumber, formatChartLabelPercent, formatFurnitureDate, formatListNumber, formatSlideNumber, intrinsicImageSize, isRecord2 as isRecord, layoutCaption, layoutCode, layoutFootnotes, layoutFurniture, layoutMetric, layoutQuote, layoutTable, layoutTimeline, listNumbers, measureText, measureTextOutline, normalizeHexColor, numberingAtLevel, numberingFindings, numberingStyleDraws, paragraphDirection, paragraphDirectionAt, parseIsoDate, pathFor, physicalAlignment, placeTextLines, promotedRegionKeys, readingRows, referencesSlide, resolveCanvasDimensions, resolveChartOptions, resolveColorRef, resolveFontFamilies, resolveFontSchemeReference, resolveLogo, resolveNumbering, resolveScriptFonts, resolveSlideDirection, resolveSocialProfile, resolveTextStyle, scriptFontRole, sliceNumberedItems, slideCitations, slideImageShape, snapFontSizeDown, snapFontSizeUp, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, visitContentPayloads, visualReadingOrder, walkCitationRuns, wrapText };