@openpresentation/opf 0.11.4 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) 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-BnI7JKcn.d.ts} +3 -3
  5. package/dist/catalogs.d.ts +2 -2
  6. package/dist/catalogs.js +2 -1
  7. package/dist/{chunk-KJKASEPE.js → chunk-4LAM3N6S.js} +753 -42
  8. package/dist/chunk-6LZK2IZB.js +1690 -0
  9. package/dist/{chunk-NSVVQ52O.js → chunk-AUM5MXBR.js} +8 -6
  10. package/dist/{chunk-AEUSVEUY.js → chunk-E34DJ5LL.js} +50 -14
  11. package/dist/chunk-FYLJVF7X.js +18448 -0
  12. package/dist/{chunk-HJL64ETN.js → chunk-GJKJL4YY.js} +14 -0
  13. package/dist/{chunk-3H2U7FDR.js → chunk-HOBSL7AJ.js} +1867 -247
  14. package/dist/chunk-JKVKYVWL.js +1332 -0
  15. package/dist/{chunk-3OJN7QAW.js → chunk-MWPMJE6P.js} +18 -4
  16. package/dist/chunk-RUC7L5DJ.js +261 -0
  17. package/dist/chunk-S5W34SIJ.js +1 -0
  18. package/dist/chunk-V2537ENL.js +198 -0
  19. package/dist/{chunk-RXNFDGPC.js → chunk-WXSPETQ6.js} +311 -40
  20. package/dist/{composition-Dl-d9FRj.d.ts → composition-DWbCiMwF.d.ts} +559 -10
  21. package/dist/composition.d.ts +1 -1
  22. package/dist/composition.js +2 -1
  23. package/dist/convert.d.ts +261 -0
  24. package/dist/convert.js +1107 -0
  25. package/dist/diff.d.ts +145 -0
  26. package/dist/diff.js +604 -0
  27. package/dist/docs.js +72 -18
  28. package/dist/examples.d.ts +1 -1
  29. package/dist/examples.js +112 -112
  30. package/dist/font-policy.d.ts +15 -0
  31. package/dist/font-policy.js +1 -1
  32. package/dist/format.d.ts +20 -0
  33. package/dist/format.js +83 -0
  34. package/dist/index.d.ts +173 -24
  35. package/dist/index.js +926 -262
  36. package/dist/lint.d.ts +4 -4
  37. package/dist/lint.js +6 -5
  38. package/dist/markdown.d.ts +74 -0
  39. package/dist/markdown.js +1941 -0
  40. package/dist/pagination.d.ts +1 -1
  41. package/dist/pagination.js +6 -5
  42. package/dist/patch.d.ts +99 -0
  43. package/dist/patch.js +6 -0
  44. package/dist/{presentation-j5PVrqfV.d.ts → presentation-DcF-MZcz.d.ts} +452 -17
  45. package/dist/repo-readme.js +1 -1
  46. package/dist/{schemas-X_NniU4A.d.ts → schemas-BSjlG8-R.d.ts} +28 -0
  47. package/dist/schemas.d.ts +1 -1
  48. package/dist/schemas.js +1 -1
  49. package/dist/spec/catalogs/audiences/customers.json +5 -1
  50. package/dist/spec/catalogs/audiences/executives.json +5 -1
  51. package/dist/spec/catalogs/audiences/index.json +20 -8
  52. package/dist/spec/catalogs/audiences/investors.json +5 -1
  53. package/dist/spec/catalogs/audiences/marketing-team.json +5 -1
  54. package/dist/spec/catalogs/audiences/regulators.json +5 -1
  55. package/dist/spec/catalogs/audiences/sales-team.json +5 -1
  56. package/dist/spec/catalogs/manifest.json +2 -2
  57. package/dist/spec/reference/engine-defaults.json +1 -1
  58. package/dist/spec/reference/font-policy.json +311 -40
  59. package/dist/spec/reference/font-policy.schema.json +40 -0
  60. package/dist/spec/reference/symbol-font-encodings.json +18354 -0
  61. package/dist/spec/reference/symbol-font-encodings.schema.json +156 -0
  62. package/dist/spec/schemas/audience.schema.json +7 -7
  63. package/dist/spec/schemas/narrative.schema.json +1 -1
  64. package/dist/spec/schemas/opf.schema.json +597 -28
  65. package/dist/spec-files.d.ts +1 -1
  66. package/dist/spec-files.js +1 -1
  67. package/dist/symbol-font-encodings.d.ts +128 -0
  68. package/dist/symbol-font-encodings.js +1 -0
  69. package/dist/types.d.ts +4 -4
  70. package/dist/validator-DfG9yxba.d.ts +172 -0
  71. package/dist/validator.d.ts +4 -36
  72. package/dist/validator.js +5 -4
  73. package/package.json +32 -3
  74. package/dist/chunk-FDCWQHWO.js +0 -1366
@@ -1,3 +1,5 @@
1
+ import { catalogs } from './chunk-E34DJ5LL.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,988 @@ 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
+ "audiences/executives": "executive",
1664
+ "audiences/investors": "investor",
1665
+ "audiences/customers": "customer",
1666
+ "audiences/sales-team": "sales",
1667
+ "audiences/marketing-team": "marketing",
1668
+ "audiences/regulators": "regulatory",
1669
+ "chartTypes/100pct-bullet-bar": "100pct-stacked-bar-3x",
1670
+ "chartTypes/100pct-bullet-bar-2x": "100pct-stacked-bar-3x",
1671
+ "chartTypes/100pct-bullet-bar-3x": "100pct-stacked-bar-3x",
1672
+ "chartTypes/100pct-bullet-column": "100pct-stacked-column-3x",
1673
+ "chartTypes/100pct-bullet-column-2x": "100pct-stacked-column-3x",
1674
+ "chartTypes/100pct-bullet-column-3x": "100pct-stacked-column-3x",
1675
+ "chartTypes/100pct-progress-bar": "100pct-stacked-bar-3x",
1676
+ "chartTypes/100pct-stacked-area-2x": "100pct-stacked-area-3x",
1677
+ "chartTypes/100pct-stacked-bar-2x": "100pct-stacked-bar-3x",
1678
+ "chartTypes/100pct-stacked-column-2x": "100pct-stacked-column-3x",
1679
+ "chartTypes/australia": "world",
1680
+ "chartTypes/box-and-whisker-2x": "box-and-whisker",
1681
+ "chartTypes/box-and-whisker-3x": "box-and-whisker",
1682
+ "chartTypes/bullet-bar": "bar",
1683
+ "chartTypes/bullet-bar-2x": "bar",
1684
+ "chartTypes/bullet-bar-3x": "bar",
1685
+ "chartTypes/bullet-column": "column",
1686
+ "chartTypes/bullet-column-2x": "column",
1687
+ "chartTypes/bullet-column-3x": "column",
1688
+ "chartTypes/canada": "world",
1689
+ "chartTypes/clustered-bar-2x": "bar",
1690
+ "chartTypes/clustered-column": "column",
1691
+ "chartTypes/dot-plot": "scatter",
1692
+ "chartTypes/dot-plot-2x": "scatter",
1693
+ "chartTypes/dot-plot-3x": "scatter",
1694
+ "chartTypes/dot-plot-4x": "scatter",
1695
+ "chartTypes/dot-plot-5x": "scatter",
1696
+ "chartTypes/dot-plot-6x": "scatter",
1697
+ "chartTypes/dumbbell": "scatter",
1698
+ "chartTypes/line-2x": "line",
1699
+ "chartTypes/line-3x": "line",
1700
+ "chartTypes/line-with-high-low": "line",
1701
+ "chartTypes/line-with-high-low-and-markers": "line-with-markers",
1702
+ "chartTypes/line-with-markers-2x": "line-with-markers",
1703
+ "chartTypes/line-with-markers-3x": "line-with-markers",
1704
+ "chartTypes/sparkline": "line",
1705
+ "chartTypes/sparkline-2x": "line",
1706
+ "chartTypes/sparkline-3x": "line",
1707
+ "chartTypes/sparkline-4x": "line",
1708
+ "chartTypes/sparkline-5x": "line",
1709
+ "chartTypes/sparkline-6x": "line",
1710
+ "chartTypes/stacked-area-2x": "stacked-area-3x",
1711
+ "chartTypes/stacked-bar-2x": "stacked-bar-3x",
1712
+ "chartTypes/stacked-column-2x": "stacked-column-3x",
1713
+ "chartTypes/stacked-line-2x": "stacked-line-3x",
1714
+ "chartTypes/stacked-line-with-markers-2x": "stacked-line-with-markers-3x",
1715
+ "chartTypes/treemap-2x": "treemap",
1716
+ "chartTypes/treemap-3x": "treemap",
1717
+ "chartTypes/united-kingdom": "world",
1718
+ "chartTypes/united-states": "world"
1719
+ };
1720
+
1721
+ // src/chart-options.ts
1722
+ var END_POSITIONS = ["center", "inside-end", "inside-base", "outside-end"];
1723
+ var STACKED_POSITIONS = ["center", "inside-end", "inside-base"];
1724
+ var POINT_POSITIONS = ["above", "below", "left", "right", "center"];
1725
+ var PIE_POSITIONS = ["center", "inside-end", "outside-end"];
1726
+ var NONE = [];
1727
+ var VALUE_CATEGORY = ["category", "value"];
1728
+ var ALL_CONTENT = ["category", "value", "percent"];
1729
+ var categoryKinds = /* @__PURE__ */ new Set(["bar", "line", "area", "scatter", "histogram", "pareto", "box", "waterfall", "funnel"]);
1730
+ var valueKinds = /* @__PURE__ */ new Set(["bar", "line", "area", "scatter", "histogram", "pareto", "box", "waterfall"]);
1731
+ var legendKinds = /* @__PURE__ */ new Set(["bar", "line", "area", "pie", "doughnut", "scatter", "radar", "box"]);
1732
+ function labelSupport(target) {
1733
+ const none = { supported: true, content: VALUE_CATEGORY, positions: NONE, defaultPosition: null, defaultOn: false };
1734
+ switch (target.kind) {
1735
+ case "bar":
1736
+ return target.stacked ? { ...none, positions: STACKED_POSITIONS, defaultPosition: "center" } : { ...none, positions: END_POSITIONS, defaultPosition: "outside-end" };
1737
+ case "line":
1738
+ case "scatter":
1739
+ return { ...none, positions: POINT_POSITIONS, defaultPosition: "above" };
1740
+ case "pie":
1741
+ return { ...none, content: ALL_CONTENT, positions: PIE_POSITIONS, defaultPosition: "outside-end" };
1742
+ case "doughnut":
1743
+ return { ...none, content: ALL_CONTENT };
1744
+ // The funnel labels its bars with values and the treemap its tiles with category names by default.
1745
+ case "funnel":
1746
+ case "treemap":
1747
+ return { ...none, defaultOn: true };
1748
+ case "area":
1749
+ case "radar":
1750
+ return none;
1751
+ case "histogram":
1752
+ case "pareto":
1753
+ case "waterfall":
1754
+ return { ...none, positions: END_POSITIONS, defaultPosition: "outside-end" };
1755
+ default:
1756
+ return { ...none, supported: false, content: [] };
1757
+ }
1758
+ }
1759
+ function chartOptionSupport(target) {
1760
+ return {
1761
+ axisTitles: { category: categoryKinds.has(target.kind), value: valueKinds.has(target.kind) },
1762
+ legend: legendKinds.has(target.kind),
1763
+ dataLabels: labelSupport(target)
1764
+ };
1765
+ }
1766
+ var KEPT = {
1767
+ column: { kind: "bar" },
1768
+ "stacked-column-3x": { kind: "bar", stacked: true },
1769
+ "100pct-stacked-column-3x": { kind: "bar", stacked: true },
1770
+ bar: { kind: "bar" },
1771
+ "stacked-bar-3x": { kind: "bar", stacked: true },
1772
+ "100pct-stacked-bar-3x": { kind: "bar", stacked: true },
1773
+ line: { kind: "line" },
1774
+ "line-with-markers": { kind: "line" },
1775
+ "stacked-line-3x": { kind: "line", stacked: true },
1776
+ "stacked-line-with-markers-3x": { kind: "line", stacked: true },
1777
+ area: { kind: "area" },
1778
+ "stacked-area-3x": { kind: "area", stacked: true },
1779
+ "100pct-stacked-area-3x": { kind: "area", stacked: true },
1780
+ pie: { kind: "pie" },
1781
+ doughnut: { kind: "doughnut" },
1782
+ scatter: { kind: "scatter" },
1783
+ radar: { kind: "radar" },
1784
+ "radar-with-markers": { kind: "radar" },
1785
+ "filled-radar": { kind: "radar" },
1786
+ treemap: { kind: "treemap" },
1787
+ histogram: { kind: "histogram" },
1788
+ pareto: { kind: "pareto" },
1789
+ "box-and-whisker": { kind: "box" },
1790
+ waterfall: { kind: "waterfall" },
1791
+ funnel: { kind: "funnel" },
1792
+ world: { kind: "map" }
1793
+ };
1794
+ function chartOptionTarget(typeId) {
1795
+ if (typeof typeId !== "string") return void 0;
1796
+ const raw = typeId.trim().toLowerCase();
1797
+ const id = raw === "donut" ? "doughnut" : raw;
1798
+ const kept = deprecatedCatalogIds[`chartTypes/${id}`] ?? id;
1799
+ const target = Object.hasOwn(KEPT, kept) ? KEPT[kept] : void 0;
1800
+ return target ? { ...target } : void 0;
1801
+ }
1802
+ var record3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1803
+ var LEGEND_POSITIONS = /* @__PURE__ */ new Set(["none", "top", "bottom", "left", "right"]);
1804
+ var CONTENT_ORDER = ["category", "value", "percent"];
1805
+ var POSITIONS = /* @__PURE__ */ new Set(["auto", "center", "inside-end", "inside-base", "outside-end", "above", "below", "left", "right"]);
1806
+ var DEFAULT_CHART_LABEL_SEPARATOR = ", ";
1807
+ function resolveChartOptions(chart, target) {
1808
+ const result = { active: false, axisTitles: {}, diagnostics: [] };
1809
+ if (!record3(chart)) return result;
1810
+ const support = target ? chartOptionSupport(target) : void 0;
1811
+ const adapt = (option, reason, message) => result.diagnostics.push({ code: "chart-option-adapted", option, reason, message });
1812
+ const kind2 = target?.kind ?? "unknown";
1813
+ if (record3(chart.axisTitles)) {
1814
+ for (const axis of ["category", "value"]) {
1815
+ const raw = chart.axisTitles[axis];
1816
+ if (typeof raw !== "string" || !raw.trim()) continue;
1817
+ if (support && !support.axisTitles[axis]) {
1818
+ adapt(`axisTitles.${axis}`, "unsupported-type", `A '${kind2}' chart has no ${axis} axis to title; the ${axis} axis title is not drawn or exported.`);
1819
+ continue;
1820
+ }
1821
+ result.axisTitles[axis] = raw.trim();
1822
+ }
1823
+ }
1824
+ if (typeof chart.legend === "string" && LEGEND_POSITIONS.has(chart.legend)) {
1825
+ if (support && !support.legend) adapt("legend", "unsupported-type", `A '${kind2}' chart has no legend option; the legend setting is ignored.`);
1826
+ else result.legend = chart.legend;
1827
+ }
1828
+ const labels = chart.dataLabels;
1829
+ if (labels === false && support?.dataLabels.defaultOn) result.dataLabelsOff = true;
1830
+ if (labels === true || record3(labels)) {
1831
+ const options = record3(labels) ? labels : {};
1832
+ if (support && !support.dataLabels.supported) {
1833
+ adapt("dataLabels", "unsupported-type", `A '${kind2}' chart has no data label option; data labels are not drawn or exported.`);
1834
+ } else {
1835
+ const allowed = support?.dataLabels.content ?? ALL_CONTENT;
1836
+ const requested = Array.isArray(options.content) ? options.content.filter((part) => typeof part === "string" && CONTENT_ORDER.includes(part)) : ["value"];
1837
+ const content = CONTENT_ORDER.filter((part) => requested.includes(part) && allowed.includes(part));
1838
+ for (const part of CONTENT_ORDER) {
1839
+ 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.`);
1840
+ }
1841
+ if (!content.length) content.push("value");
1842
+ const positions = support?.dataLabels.positions ?? NONE;
1843
+ let position = support?.dataLabels.defaultPosition ?? null;
1844
+ if (typeof options.position === "string" && POSITIONS.has(options.position) && options.position !== "auto") {
1845
+ if (!support || positions.includes(options.position)) position = options.position;
1846
+ 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.`);
1847
+ }
1848
+ let separator = typeof options.separator === "string" ? options.separator : DEFAULT_CHART_LABEL_SEPARATOR;
1849
+ if (/[\r\n]/.test(separator)) {
1850
+ separator = separator.replace(/[\r\n]+/g, " ");
1851
+ adapt("dataLabels.separator", "unsupported-content", "A data label is one line; the line break in the separator is replaced with a space.");
1852
+ }
1853
+ result.dataLabels = { content, position, separator };
1854
+ }
1855
+ }
1856
+ result.active = result.legend !== void 0 || result.dataLabels !== void 0 || result.dataLabelsOff === true || result.axisTitles.category !== void 0 || result.axisTitles.value !== void 0;
1857
+ return result;
1858
+ }
1859
+ function formatChartLabelNumber(value) {
1860
+ return String(Number(value.toPrecision(12)));
1861
+ }
1862
+ function formatChartLabelPercent(share) {
1863
+ return `${Math.round(Number((share * 100).toPrecision(12)))}%`;
1864
+ }
1865
+ function chartLabelText(parts, content, separator = DEFAULT_CHART_LABEL_SEPARATOR) {
1866
+ return CONTENT_ORDER.filter((part) => content.includes(part) && parts[part] !== void 0).map((part) => parts[part]).join(separator);
1867
+ }
361
1868
 
362
1869
  // src/composition.ts
363
1870
  var MAX_COMPOSITION_DEPTH = 32;
@@ -368,14 +1875,14 @@ function resolveFontSchemeReference(reference, lookup, path = "design.fontScheme
368
1875
  const overrides = typeof reference === "object" && reference !== null && !Array.isArray(reference) ? reference : void 0;
369
1876
  const id = typeof reference === "string" ? reference : typeof overrides?.id === "string" ? overrides.id : void 0;
370
1877
  const found = id === void 0 ? void 0 : lookup(id);
371
- const base = record2(found ?? lookup(DEFAULT_FONT_SCHEME));
1878
+ const base = record4(found ?? lookup(DEFAULT_FONT_SCHEME));
372
1879
  const scheme = overrides ? { ...base, ...overrides } : { ...base };
373
1880
  if (id === void 0 || found !== void 0) return { scheme };
374
1881
  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
1882
  }
376
1883
  function resolveFontFamilies(input) {
377
- const scheme = record2(input);
378
- const family = (value) => typeof value === "string" ? value : record2(value).family;
1884
+ const scheme = record4(input);
1885
+ const family = (value) => typeof value === "string" ? value : record4(value).family;
379
1886
  const accent = family(scheme.accent);
380
1887
  return {
381
1888
  heading: family(scheme.heading) ?? scheme.major ?? scheme.minor ?? DEFAULT_FONT_FAMILIES.heading,
@@ -389,27 +1896,28 @@ function resolveTextStyle(style, measurement) {
389
1896
  }
390
1897
  function textWidthMeasurer(style, measurement) {
391
1898
  if (!measurement) return measureText;
392
- return (text, size) => {
393
- const width = measurement.measure(text, size, style);
1899
+ return (text2, size) => {
1900
+ const width = measurement.measure(text2, size, style);
394
1901
  if (!Number.isFinite(width) || width < 0) throw new RangeError("Text measurement must return a finite, nonnegative width.");
395
1902
  return width;
396
1903
  };
397
1904
  }
398
- function measureTextOutline(text, fontSize, style, measurement) {
1905
+ function measureTextOutline(text2, fontSize, style, measurement) {
399
1906
  if (measurement?.outlineBounds === void 0) return void 0;
400
1907
  if (typeof measurement.outlineBounds !== "function") throw new TypeError("Text outline provider must be a function.");
401
- const bounds = measurement.outlineBounds(text, fontSize, style);
1908
+ const bounds = measurement.outlineBounds(text2, fontSize, style);
402
1909
  if (bounds === null) return null;
403
1910
  if (!bounds || ![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite) || bounds.width < 0 || bounds.height < 0) {
404
1911
  throw new RangeError("Text outline measurement must return null or finite bounds with nonnegative dimensions.");
405
1912
  }
406
1913
  return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
407
1914
  }
408
- function placeTextLines(lines, box, alignment = "left", rasterPadding = 0) {
1915
+ function placeTextLines(lines, box, alignment = "left", rasterPadding = 0, directions) {
409
1916
  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 = [];
1917
+ const placed = [];
411
1918
  let shift = 0, bottom = box.y, height = 0, overflow = false;
412
- for (const line of lines) {
1919
+ for (const [lineIndex, line] of lines.entries()) {
1920
+ const lineAlignment = physicalAlignment(alignment, directions?.[lineIndex]), factor = lineAlignment === "right" ? 1 : lineAlignment === "center" ? 0.5 : 0;
413
1921
  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
1922
  const ink = line.outline;
415
1923
  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 +1933,7 @@ function placeTextLines(lines, box, alignment = "left", rasterPadding = 0) {
425
1933
  if (outline) bottom = outline.y + outline.height + rasterPadding;
426
1934
  height = Math.max(height, y + line.height - box.y, outline ? bottom - box.y : 0);
427
1935
  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 });
1936
+ placed.push({ x, y, baseline, height: line.height, width: line.width, outline, ...directions ? { alignment: lineAlignment } : {} });
429
1937
  }
430
1938
  return { alignment, rasterPadding, lines: placed, height, overflow };
431
1939
  }
@@ -460,14 +1968,14 @@ var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
460
1968
  var COVER_LAYOUT_IDS = /* @__PURE__ */ new Set(["title", "title-subtitle"]);
461
1969
  var rows = ["top", "middle", "bottom"];
462
1970
  var columns = ["left", "center", "right"];
463
- var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
1971
+ var record4 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
464
1972
  var kind = (field) => field === "items" ? "list" : field === "bullets" ? "text" : field;
465
- var round = (value) => Math.round(value * 1e6) / 1e6 || value;
1973
+ var round2 = (value) => Math.round(value * 1e6) / 1e6 || value;
466
1974
  var SLIDE_IMAGE_POSITIONS = ["background", "top", "bottom", "left", "right"];
467
1975
  var SLIDE_IMAGE_BAND = 0.5;
468
- var assetSource = (value) => typeof value === "string" ? value : record2(value).src;
1976
+ var assetSource = (value) => typeof value === "string" ? value : record4(value).src;
469
1977
  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));
1978
+ var pathNumber = (value) => String(round2(value));
471
1979
  var SLIDE_IMAGE_SHAPES = { rectangle: "rect", rounded: "roundRect", circle: "ellipse", hexagon: "hexagon" };
472
1980
  function slideImageShape(kind2, box, cornerRadius = 1 / 6) {
473
1981
  const { x, y, width: w, height: h } = box, r = x + w, b = y + h, ss = Math.min(w, h), n = pathNumber;
@@ -488,15 +1996,17 @@ function slideImageShape(kind2, box, cornerRadius = 1 / 6) {
488
1996
  }
489
1997
  return { kind: "rectangle", preset: "rect", adjust: {}, path: rect };
490
1998
  }
491
- function resolveSlideImage(slide, layout, presentation, width, height, path, padding, scale, diagnostics) {
492
- const own = record2(slide.design), deck = record2(record2(presentation).design);
1999
+ function resolveSlideImage(slide, layout, presentation, width, height, path, padding, scale, diagnostics, rtl = false) {
2000
+ const own = record4(slide.design), deck = record4(record4(presentation).design);
493
2001
  const local = own.slideImage !== void 0;
494
2002
  const configured = local ? own.slideImage : deck.slideImage;
495
2003
  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;
2004
+ const treatment = typeof configured === "object" && !Array.isArray(configured) && "position" in configured ? record4(configured) : void 0;
497
2005
  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;
2006
+ const authoredPosition = treatment ? treatment.position : SLIDE_IMAGE_POSITIONS.find((value2) => value2 === alignment) ?? "background";
2007
+ if (!SLIDE_IMAGE_POSITIONS.includes(authoredPosition)) return void 0;
2008
+ const mirrorSide = (side) => !rtl ? side : side === "left" ? "right" : side === "right" ? "left" : side;
2009
+ const position = mirrorSide(authoredPosition);
500
2010
  const designSource = treatment ? treatment.src : configured;
501
2011
  const root = slide.image, sameSource = root !== void 0 && designSource !== void 0 && assetSource(root) === assetSource(designSource);
502
2012
  if (!local && layout.slideImage !== true && !sameSource) return void 0;
@@ -507,8 +2017,8 @@ function resolveSlideImage(slide, layout, presentation, width, height, path, pad
507
2017
  const t = treatment ?? {};
508
2018
  const fill = t.fill === "crop" || t.fill === "fit" ? t.fill : (own.imageFill ?? deck.imageFill) === "fit" ? "fit" : "crop";
509
2019
  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 };
2020
+ const band = { width: round2(width * share), height: round2(height * share) };
2021
+ 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
2022
  const designPath = local ? `${path}.design.slideImage` : "design.slideImage";
513
2023
  const kind2 = Object.hasOwn(SLIDE_IMAGE_SHAPES, t.shape) ? t.shape : "rectangle";
514
2024
  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 +2027,7 @@ function resolveSlideImage(slide, layout, presentation, width, height, path, pad
517
2027
  const frameWidth = Math.min(box.width, box.height * aspect), frameHeight = frameWidth / aspect;
518
2028
  box = { x: box.x + (box.width - frameWidth) / 2, y: box.y + (box.height - frameHeight) / 2, width: frameWidth, height: frameHeight };
519
2029
  }
520
- box = { x: round(box.x), y: round(box.y), width: round(box.width), height: round(box.height) };
2030
+ box = { x: round2(box.x), y: round2(box.y), width: round2(box.width), height: round2(box.height) };
521
2031
  const result = {
522
2032
  path: designPath,
523
2033
  sourcePath: replacesContent ? `${path}.image` : designPath,
@@ -530,19 +2040,19 @@ function resolveSlideImage(slide, layout, presentation, width, height, path, pad
530
2040
  shape: slideImageShape(kind2, box, finite(t.cornerRadius, 0, 0.5))
531
2041
  };
532
2042
  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) };
2043
+ const border = record4(t.border), borderWidth = finite(border.width, 0, 64);
2044
+ if (border.color !== void 0 && borderWidth) result.border = { color: border.color, width: round2(borderWidth * scale) };
535
2045
  const opacity = finite(t.opacity, 0, 1);
536
2046
  if (opacity !== void 0 && opacity < 1) result.opacity = opacity;
537
2047
  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);
2048
+ 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 };
2049
+ const overlay = record4(t.overlay), overlayOpacity = finite(overlay.opacity, 0, 1);
540
2050
  if (overlay.color !== void 0 && overlayOpacity !== void 0) {
541
- const edge = ["top", "bottom", "left", "right"].includes(overlay.edge) ? overlay.edge : void 0;
2051
+ const edge = ["top", "bottom", "left", "right"].includes(overlay.edge) ? mirrorSide(overlay.edge) : void 0;
542
2052
  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
2053
  else {
544
2054
  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) };
2055
+ 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
2056
  result.overlay = { color: overlay.color, opacity: overlayOpacity, box: bandBox, shape: edge ? slideImageShape("rectangle", bandBox) : result.shape };
547
2057
  }
548
2058
  }
@@ -565,10 +2075,10 @@ function resolveLogo(presentation, slide, options = {}) {
565
2075
  };
566
2076
  const pick = (value, path) => {
567
2077
  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)) {
2078
+ if (typeof value === "string" || typeof value === "object" && !Array.isArray(value) && "src" in record4(value)) {
569
2079
  return usable(value) ? { source: value, path, variant: "default", slot } : null;
570
2080
  }
571
- const set = record2(value);
2081
+ const set = record4(value);
572
2082
  if (!Object.keys(set).some((key) => LOGO_SET_KEYS.has(key))) return null;
573
2083
  for (const key of chain) {
574
2084
  const variant = set[key];
@@ -576,16 +2086,16 @@ function resolveLogo(presentation, slide, options = {}) {
576
2086
  }
577
2087
  return null;
578
2088
  };
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`);
2089
+ const deck = record4(presentation), slideIndex = Number.isSafeInteger(options.slideIndex) && options.slideIndex >= 0 ? options.slideIndex : 0;
2090
+ const own = pick(record4(record4(slide).design).logo, `slides.${slideIndex}.design.logo`);
581
2091
  if (own) return own;
582
- const shared = pick(record2(deck.design).logo, "design.logo");
2092
+ const shared = pick(record4(deck.design).logo, "design.logo");
583
2093
  if (shared) return shared;
584
2094
  const organizations = Array.isArray(deck.organization) ? deck.organization : [deck.organization];
585
- const primaryIndex = organizations.findIndex((item) => record2(item).role === "primary");
2095
+ const primaryIndex = organizations.findIndex((item) => record4(item).role === "primary");
586
2096
  const index = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
587
2097
  if (index < 0) return null;
588
- return pick(record2(organizations[index]).logo, Array.isArray(deck.organization) ? `organization.${index}.logo` : "organization.logo");
2098
+ return pick(record4(organizations[index]).logo, Array.isArray(deck.organization) ? `organization.${index}.logo` : "organization.logo");
589
2099
  }
590
2100
  var webUrl = /^https?:\/\/\S+$/i;
591
2101
  function resolveSocialProfile(platform, value, records = [], owner = "organization") {
@@ -603,10 +2113,11 @@ function resolveSocialProfile(platform, value, records = [], owner = "organizati
603
2113
  return { text: display(url), href: encodeURI(url), resolved: true };
604
2114
  }
605
2115
  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) };
2116
+ const slide = record4(input), width = options.width ?? 1280, height = options.height ?? 720, scale = Math.min(width, height) / 720;
2117
+ const settings = { ...record4(record4(options.layout).composition), ...record4(slide.composition) };
608
2118
  assertComposition(settings);
609
- const minimum = (settings.minFontSize ?? 16) * scale, size = Math.max(13 * scale, minimum), padding = (options.textRasterPadding ?? 2) * scale;
2119
+ const furnitureRtl = (options.direction ?? resolveSlideDirection(options.presentation, options.slideIndex)) === "rtl";
2120
+ const minimum = snapFontSizeUp((settings.minFontSize ?? 16) * scale), size = gridFontSize(13 * scale, minimum), padding = (options.textRasterPadding ?? 2) * scale;
610
2121
  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
2122
  const number = options.slideNumber ?? (options.slideIndex ?? 0) + 1;
612
2123
  if (!Number.isSafeInteger(number) || number < 1) throw new RangeError("Displayed slide number must be a positive safe integer.");
@@ -615,32 +2126,33 @@ function layoutFurniture(input, options = {}) {
615
2126
  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
2127
  const outlines = options.textMeasurement?.outlineBounds !== void 0, parts = [], diagnostics = [];
617
2128
  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 ?? []];
2129
+ const primaryIndex = organizations.findIndex((item) => record4(item).role === "primary"), organizationIndex = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
2130
+ const organization = record4(organizations[organizationIndex]), organizationRoot = Array.isArray(options.presentation?.organization) ? `organization.${organizationIndex}` : "organization", organizationPath = `${organizationRoot}.name`;
2131
+ const inlinePlatforms = record4(record4(options.presentation?.catalogs).socialPlatforms).records, platformRecords = [...Array.isArray(inlinePlatforms) ? inlinePlatforms : [], ...options.socialPlatforms ?? []];
621
2132
  const fontFamily = options.fonts?.body ?? "sans-serif";
622
2133
  let headerBottom = 0, footerTop = height, configured = false;
623
2134
  const error = (path, message, code = "text-overflow") => diagnostics.push({ code, path, message });
624
2135
  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];
2136
+ const local = record4(slide.design)[kind2] !== void 0, value = local ? record4(slide.design)[kind2] : options.presentation?.design?.[kind2];
626
2137
  if (value === void 0 || value === false) continue;
627
2138
  const root = local ? `${sourceRoot}.design.${kind2}` : `design.${kind2}`;
628
2139
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Header/footer content must be an object or false.");
629
2140
  configured = true;
630
2141
  const zones = [];
631
2142
  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 = [];
2143
+ const side = furnitureRtl && zone !== "center" ? zone === "left" ? "right" : "left" : zone;
2144
+ 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
2145
  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.`);
2146
+ const add = (field, text2, generated = false, sourcePath, extras = {}) => {
2147
+ if (text2 === void 0) return;
2148
+ if (typeof text2 !== "string") throw new TypeError(`Furniture field ${path}.${field} requires string content.`);
637
2149
  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));
2150
+ 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
2151
  const ink = fit.sourceLines.map((line, index2) => {
640
2152
  let outline = null;
641
2153
  if (outlines) {
642
2154
  for (const segment of line.segments) if (segment.kind === "text") {
643
- const bounds = measureTextOutline(text.slice(segment.start, segment.end), size, style, options.textMeasurement);
2155
+ const bounds = measureTextOutline(text2.slice(segment.start, segment.end), size, style, options.textMeasurement);
644
2156
  if (bounds) {
645
2157
  const next = { ...bounds, x: bounds.x + segment.x };
646
2158
  if (!outline) outline = next;
@@ -656,30 +2168,30 @@ function layoutFurniture(input, options = {}) {
656
2168
  }
657
2169
  return { width: line.width, y: index2 * fit.lineHeight, baseline: size + index2 * fit.lineHeight, height: fit.lineHeight, outline };
658
2170
  });
659
- const natural = outlines ? placeTextLines(ink, { x, y, width: zoneWidth, height: Number.MAX_VALUE }, zone, padding) : void 0;
2171
+ const natural = outlines ? placeTextLines(ink, { x, y, width: zoneWidth, height: Number.MAX_VALUE }, side, padding) : void 0;
660
2172
  const partHeight = natural?.height ?? fit.sourceLines.length * fit.lineHeight;
661
2173
  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;
2174
+ const box = { x, y, width: zoneWidth, height: Math.max(scale, partHeight) }, placement = outlines ? placeTextLines(ink, box, side, padding) : void 0;
663
2175
  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 } : {} });
2176
+ 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
2177
  if (accepted.overflow) error(partPath, "Repeated text exceeds its zone at the selected readability floor; change the furniture or slide design.");
666
2178
  y += box.height;
667
2179
  };
668
2180
  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 };
2181
+ 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));
2182
+ return { x: side === "left" ? x : side === "center" ? x + (zoneWidth - imageWidth) / 2 : x + zoneWidth - imageWidth, y, width: imageWidth, height: imageHeight };
671
2183
  };
672
2184
  if (content.logo === true) {
673
2185
  const resolved = resolveLogo(options.presentation, slide, { slot: "icon", onDark: options.darkBackground, slideIndex: options.slideIndex });
674
2186
  if (resolved) {
675
2187
  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 });
2188
+ zoneParts.push({ type: "image", kind: kind2, zone, field: "logo", path: `${path}.logo`, sourcePath: resolved.path, generated: true, image: resolved.source, box, alignment: side });
677
2189
  y += box.height;
678
2190
  } else error(`${path}.logo`, "Generated logo needs design.logo or a primary organization logo.", "unresolved-content");
679
2191
  }
680
2192
  if (content.image !== void 0) {
681
2193
  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 });
2194
+ zoneParts.push({ type: "image", kind: kind2, zone, field: "image", path: `${path}.image`, sourcePath: `${path}.image`, generated: false, image: content.image, box, alignment: side });
683
2195
  y += box.height;
684
2196
  }
685
2197
  add("text", content.text);
@@ -688,7 +2200,7 @@ function layoutFurniture(input, options = {}) {
688
2200
  else error(`${path}.organization`, "Generated organization name needs a named organization in the presentation.", "unresolved-content");
689
2201
  }
690
2202
  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") }));
2203
+ 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
2204
  if (links.length) add("socials", links.map((link) => link.text).join("\n"), true, `${organizationRoot}.socials`, { links });
693
2205
  else error(`${path}.socials`, "Generated social profiles need a primary organization with socials.", "unresolved-content");
694
2206
  }
@@ -740,29 +2252,43 @@ function layoutFurniture(input, options = {}) {
740
2252
  return { algorithm: "furniture-flow-v2", configured, textMeasurement: options.textMeasurement ? "provided" : "estimated", textOutlines: outlines ? "provided" : "unavailable", parts, headerBottom, footerTop, diagnostics, overflow: diagnostics.length > 0 };
741
2253
  }
742
2254
  var textSegments = new Intl.Segmenter("und", { granularity: "grapheme" });
743
- function measureText(text, fontSize) {
2255
+ var WRAP_TOLERANCE = 0.01;
2256
+ var HANGING_SPACE = /[^\S\r\n\u00a0\u202f\ufeff]/u;
2257
+ function measureText(text2, fontSize) {
744
2258
  let units = 0;
745
- for (const character of text) {
2259
+ for (const character of text2) {
746
2260
  if (/\p{Mark}|\u200d|\ufe0f/u.test(character)) continue;
747
2261
  units += character === " " ? 0.32 : /[A-Z0-9]/.test(character) ? 0.62 : character.codePointAt(0) > 11904 ? 1 : 0.54;
748
2262
  }
749
2263
  return units * fontSize;
750
2264
  }
751
- function wrapText(text, width, fontSize, measure = measureText) {
752
- return fitSourceText(text, { width, height: Number.MAX_VALUE }, fontSize, fontSize, 1, measure, true).lines;
2265
+ function wrapText(text2, width, fontSize, measure = measureText) {
2266
+ return fitSourceText(text2, { width, height: Number.MAX_VALUE }, fontSize, fontSize, 1, measure, true, false).lines;
753
2267
  }
754
- function fitText(text, box, requestedSize = 25, minFontSize = 16, measure = measureText) {
2268
+ function fitText(text2, box, requestedSize = 25, minFontSize = 16, measure = measureText, direction) {
755
2269
  if (![box.width, box.height, requestedSize, minFontSize].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || requestedSize <= 0 || minFontSize <= 0) {
756
2270
  throw new RangeError("Text dimensions and font sizes must be finite and positive.");
757
2271
  }
758
- return fitSourceText(text, box, Math.max(requestedSize, minFontSize), minFontSize, 1, measure, true);
2272
+ return fitSourceText(text2, box, Math.max(requestedSize, minFontSize), minFontSize, 1, measure, true, true, direction);
759
2273
  }
760
- function fitAtSizes(layout, requested, minimum, step = 1) {
2274
+ var FONT_SIZE_GRID_PER_PX = 75;
2275
+ var FONT_GRID_EPSILON = 1e-6;
2276
+ function snapFontSizeDown(px) {
2277
+ return Math.max(px > 0 ? 1 : -Infinity, Math.floor(px * FONT_SIZE_GRID_PER_PX + FONT_GRID_EPSILON)) / FONT_SIZE_GRID_PER_PX;
2278
+ }
2279
+ function snapFontSizeUp(px) {
2280
+ return Math.max(px > 0 ? 1 : -Infinity, Math.ceil(px * FONT_SIZE_GRID_PER_PX - FONT_GRID_EPSILON)) / FONT_SIZE_GRID_PER_PX;
2281
+ }
2282
+ function gridFontSize(raw, minimum) {
2283
+ return Math.max(snapFontSizeUp(minimum), snapFontSizeDown(raw));
2284
+ }
2285
+ function fitAtSizes(layout, requested, minimum, step = 1, snap = true) {
761
2286
  const start = Math.max(requested, minimum);
762
2287
  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
2288
  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;
2289
+ const raw = start - trial * step, last = trial === 64 || raw <= minimum;
2290
+ const size = !snap ? last ? minimum : Math.max(minimum, raw) : last ? snapFontSizeUp(minimum) : gridFontSize(raw, minimum), result = layout(size);
2291
+ if (!result.overflow || last) return result;
766
2292
  }
767
2293
  throw new Error("Text fitting did not evaluate its bounded floor trial.");
768
2294
  }
@@ -772,7 +2298,7 @@ function layoutQuote(value, box, options = {}) {
772
2298
  if (!quote || Array.isArray(quote) || typeof quote.text !== "string" || [quote.attribution, quote.source].some((field) => field !== void 0 && typeof field !== "string")) {
773
2299
  throw new TypeError("Quote content must be a string or a text object with optional string attribution/source.");
774
2300
  }
775
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
2301
+ const scale = options.scale ?? 1, minimum = snapFontSizeUp((options.minFontSize ?? 16) * scale);
776
2302
  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
2303
  throw new RangeError("Quote dimensions, scale and minimum font size must be finite and positive.");
778
2304
  }
@@ -780,23 +2306,23 @@ function layoutQuote(value, box, options = {}) {
780
2306
  const path = options.path ?? "quote";
781
2307
  const diagnostics = [], parts = [];
782
2308
  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) => {
2309
+ const source = (sourcePath, text2, outputStart) => ({ path: sourcePath, start: 0, end: text2.length, outputStart, outputEnd: outputStart + text2.length });
2310
+ const add = (role, text2, sources, area, fontSize, fontFamily, fontWeight, partPath) => {
785
2311
  const requestedStyle = { fontFamily, fontWeight, italic: false, path: partPath };
786
2312
  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 };
2313
+ const requestedFontSize = gridFontSize(fontSize * scale, minimum);
2314
+ const part = { role, path: partPath, text: text2, sources, box: area, requestedFontSize, minFontSize: minimum, requestedStyle, style };
789
2315
  parts.push(part);
790
2316
  return part;
791
2317
  };
792
2318
  let footer = "";
793
2319
  const footerSources = [];
794
2320
  for (const field of ["attribution", "source"]) {
795
- const text = quote[field];
796
- if (!text) continue;
2321
+ const text2 = quote[field];
2322
+ if (!text2) continue;
797
2323
  if (footer) footer += " - ";
798
- footerSources.push(source(`${path}.${field}`, text, footer.length));
799
- footer += text;
2324
+ footerSources.push(source(`${path}.${field}`, text2, footer.length));
2325
+ footer += text2;
800
2326
  }
801
2327
  const bodyPath = shorthand ? path : `${path}.text`;
802
2328
  const body = add(
@@ -820,7 +2346,7 @@ function layoutQuote(value, box, options = {}) {
820
2346
  path
821
2347
  ) : void 0;
822
2348
  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;
2349
+ 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
2350
  const inner = { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - 36 };
825
2351
  if (!attribution) body.fit = fit(body, body.box);
826
2352
  else if (usable(inner) && inner.height > 18) {
@@ -877,31 +2403,31 @@ function layoutMetric(value, box, options = {}) {
877
2403
  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
2404
  throw new TypeError("Metric content requires a finite numeric or string value and schema-valid display metadata.");
879
2405
  }
880
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
2406
+ const scale = options.scale ?? 1, minimum = snapFontSizeUp((options.minFontSize ?? 16) * scale);
881
2407
  const rasterPadding = (options.textRasterPadding ?? 1) * scale, hasOutlines = options.textMeasurement?.outlineBounds !== void 0;
882
2408
  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
2409
  throw new RangeError("Metric dimensions, scale and minimum font size must be finite and positive.");
884
2410
  }
885
2411
  if (!Number.isFinite(rasterPadding) || rasterPadding < 0) throw new RangeError("Metric raster padding must be finite and nonnegative.");
886
2412
  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.");
2413
+ const requestedAlignment = options.align ?? "left";
2414
+ if (!["left", "center", "right"].includes(requestedAlignment)) throw new RangeError("Invalid metric alignment.");
889
2415
  const sourcePath = options.path ?? "metric", parts = [], diagnostics = [];
890
2416
  for (const role of ["value", "unit", "label", "description", "delta", "trend"]) {
891
2417
  const sourceValue = metric[role];
892
2418
  if (sourceValue === void 0) continue;
893
- const text = String(sourceValue), path = scalar ? sourcePath : `${sourcePath}.${role}`;
2419
+ const text2 = String(sourceValue), path = scalar ? sourcePath : `${sourcePath}.${role}`;
894
2420
  const nominal = role === "value" ? Math.min(76 * scale, box.height * 0.28) : (role === "description" ? 20 : role === "trend" ? 18 : 23) * scale;
895
2421
  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
2422
  const part = {
897
2423
  role,
898
2424
  path,
899
- text,
900
- sources: [{ path, value: sourceValue, start: 0, end: text.length }],
901
- visible: role === "value" || text.length > 0,
2425
+ text: text2,
2426
+ sources: [{ path, value: sourceValue, start: 0, end: text2.length }],
2427
+ visible: role === "value" || text2.length > 0,
902
2428
  linePositions: [],
903
2429
  box: { ...box, height: 0 },
904
- requestedFontSize: Math.max(nominal, minimum),
2430
+ requestedFontSize: gridFontSize(nominal, minimum),
905
2431
  minFontSize: minimum,
906
2432
  requestedStyle,
907
2433
  style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement)
@@ -910,6 +2436,8 @@ function layoutMetric(value, box, options = {}) {
910
2436
  parts.push(part);
911
2437
  }
912
2438
  const primary = parts[0], metadata = parts.filter((part) => part !== primary && part.visible), unit = metadata.find((part) => part.role === "unit");
2439
+ 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;
2440
+ const alignment = physicalAlignment(requestedAlignment, metricDirection);
913
2441
  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
2442
  const occupied = (fit) => fit.placement?.height ?? fit.lines.length * fit.lineHeight;
915
2443
  const gap = 8 * scale, primaryGap = 12 * scale;
@@ -946,7 +2474,7 @@ function layoutMetric(value, box, options = {}) {
946
2474
  const key = JSON.stringify([part.role, size, width]);
947
2475
  let fit = cache2.get(key);
948
2476
  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));
2477
+ 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
2478
  fit = place(part, fit, width);
951
2479
  if (!Number.isFinite(occupied(fit))) throw new RangeError("Metric text layout exceeds finite coordinates.");
952
2480
  cache2.set(key, fit);
@@ -956,7 +2484,7 @@ function layoutMetric(value, box, options = {}) {
956
2484
  const fitValue = (area, singleLine) => {
957
2485
  if (!usable(area)) return void 0;
958
2486
  for (let step = 0; step <= 76; step++) {
959
- const size = Math.max(minimum, primary.requestedFontSize - step * scale);
2487
+ const size = gridFontSize(primary.requestedFontSize - step * scale, minimum);
960
2488
  const natural = measure(primary, size, area.width);
961
2489
  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
2490
  if (!fit.overflow || size === minimum) return fit;
@@ -965,7 +2493,7 @@ function layoutMetric(value, box, options = {}) {
965
2493
  let selected, attempts = 0;
966
2494
  const reductions = Math.min(23, Math.ceil(Math.max(0, ...metadata.map((part) => (part.requestedFontSize - minimum) / scale))));
967
2495
  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)]));
2496
+ const measured = new Map(metadata.map((part) => [part, measure(part, gridFontSize(part.requestedFontSize - reduction * scale, minimum), box.width)]));
969
2497
  const measuredUnit = unit ? measured.get(unit) : void 0;
970
2498
  const unitWidth = unit && measuredUnit ? naturalWidth(unit, measuredUnit) : 0;
971
2499
  const unitFit = unit && measuredUnit && unitWidth > 0 ? place(unit, measuredUnit, unitWidth) : measuredUnit;
@@ -1043,17 +2571,17 @@ function layoutTimeline(value, box, options = {}) {
1043
2571
  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
2572
  throw new TypeError("Timeline content requires ordered events with string labels and optional string metadata.");
1045
2573
  }
1046
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale, padding = (options.textRasterPadding ?? 1) * scale;
2574
+ const scale = options.scale ?? 1, minimum = snapFontSizeUp((options.minFontSize ?? 16) * scale), padding = (options.textRasterPadding ?? 1) * scale;
1047
2575
  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
2576
  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;
2577
+ const outlines = options.textMeasurement?.outlineBounds !== void 0, rtl = options.direction === "rtl";
1050
2578
  if (outlines && typeof options.textMeasurement?.outlineBounds !== "function") throw new TypeError("Text outline provider must be a function.");
1051
2579
  const path = options.path ?? "timeline", eventPath = (index) => shorthand ? `${path}.${index}` : `${path}.events.${index}`;
1052
2580
  const fonts = resolveFontFamilies(options.fonts), source = [];
1053
- const add = (role, text, partPath, size, weight, eventIndex) => {
1054
- if (text === void 0) return;
2581
+ const add = (role, text2, partPath, size, weight, eventIndex) => {
2582
+ if (text2 === void 0) return;
1055
2583
  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) });
2584
+ 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
2585
  };
1058
2586
  add("name", timeline.name, `${path}.name`, 24, 700);
1059
2587
  add("description", timeline.description, `${path}.description`, 18, 400);
@@ -1070,7 +2598,7 @@ function layoutTimeline(value, box, options = {}) {
1070
2598
  const key = JSON.stringify([part.path, size, width, alignment]);
1071
2599
  let measured = cache2.get(key);
1072
2600
  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));
2601
+ 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
2602
  const ink = fit.sourceLines.map((line, index) => {
1075
2603
  let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity, hasInk = false;
1076
2604
  if (outlines) {
@@ -1087,7 +2615,7 @@ function layoutTimeline(value, box, options = {}) {
1087
2615
  }
1088
2616
  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
2617
  });
1090
- const placement = outlines ? placeTextLines(ink, { x: 0, y: 0, width, height: Number.MAX_VALUE }, alignment, padding) : void 0;
2618
+ const placement = outlines ? placeTextLines(ink, { x: 0, y: 0, width, height: Number.MAX_VALUE }, alignment, padding, fit.directions) : void 0;
1091
2619
  const height = placement?.height ?? fit.sourceLines.length * fit.lineHeight;
1092
2620
  if (!Number.isFinite(height)) throw new RangeError("Timeline text exceeds finite layout coordinates.");
1093
2621
  measured = { fit, ink, height };
@@ -1108,8 +2636,8 @@ function layoutTimeline(value, box, options = {}) {
1108
2636
  score2 += Math.max(1, excess);
1109
2637
  };
1110
2638
  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;
2639
+ const measured = measure(part, gridFontSize(part.requestedFontSize - reduction * scale, minimum), Math.max(scale, width), alignment);
2640
+ 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
2641
  const fit = { ...measured.fit, ...placement ? { placement } : {}, overflow: measured.fit.overflow || !!placement?.overflow };
1114
2642
  const accepted = { ...part, box: area, alignment, fit };
1115
2643
  parts.push(accepted);
@@ -1127,7 +2655,7 @@ function layoutTimeline(value, box, options = {}) {
1127
2655
  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
2656
  const lineY = y + Math.max(scale, available) * 0.46;
1129
2657
  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;
2658
+ 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
2659
  markers.push({ path: eventPath(index), eventIndex: index, x, y: lineY, radius });
1132
2660
  let cursor = top;
1133
2661
  for (const part of fields2) {
@@ -1137,7 +2665,7 @@ function layoutTimeline(value, box, options = {}) {
1137
2665
  }
1138
2666
  });
1139
2667
  } else {
1140
- const x = box.x + radius, textX = box.x + 24 * scale, width = box.width - 24 * scale;
2668
+ 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
2669
  events.forEach((fields2, index) => {
1142
2670
  const top = y;
1143
2671
  let first2;
@@ -1154,7 +2682,7 @@ function layoutTimeline(value, box, options = {}) {
1154
2682
  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
2683
  score2 += 1;
1156
2684
  }
1157
- const first = markers[0], last = markers.at(-1), connector = { x1: first.x, y1: first.y, x2: last.x, y2: last.y };
2685
+ 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
2686
  const candidate = { arrangement, parts, markers, connector, diagnostics, score: score2 };
1159
2687
  if (!selected || score2 < selected.score) selected = candidate;
1160
2688
  if (!diagnostics.length) break;
@@ -1165,26 +2693,27 @@ function layoutTimeline(value, box, options = {}) {
1165
2693
  if (result.diagnostics.length && options.overflow === "error") throw new OPFCompositionError(result.diagnostics);
1166
2694
  return { algorithm: "timeline-flow-v1", attempts, textMeasurement: options.textMeasurement ? "provided" : "estimated", textOutlines: outlines ? "provided" : "unavailable", ...result, overflow: result.diagnostics.length > 0 };
1167
2695
  }
1168
- function fitCodeText(text, box, size, minimum, step, measure) {
1169
- return fitSourceText(text, box, size, minimum, step, measure);
2696
+ function fitCodeText(text2, box, size, minimum, step, measure, direction) {
2697
+ return fitSourceText(text2, box, size, minimum, step, measure, false, true, direction);
1170
2698
  }
1171
- function fitSourceText(text, box, size, minimum, step, measure, prose = false) {
2699
+ function fitSourceText(text2, box, size, minimum, step, measure, prose = false, snap = true, direction) {
2700
+ const directionAt = direction === "rtl" ? paragraphDirectionAt(text2, "rtl") : void 0;
1172
2701
  const layout = (fontSize) => {
1173
2702
  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) => {
2703
+ if (!Number.isFinite(tabWidth) || text2.includes(" ") && tabWidth <= 0) throw new RangeError("Text tabs require a positive finite measured space advance.");
2704
+ const measureLine = (start2, end2, record5 = false) => {
1176
2705
  let x = 0, offset = start2;
1177
2706
  const segments = [];
1178
- for (const [index, segment] of text.slice(start2, end2).split(" ").entries()) {
2707
+ for (const [index, segment] of text2.slice(start2, end2).split(" ").entries()) {
1179
2708
  if (index) {
1180
2709
  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 });
2710
+ if (record5) segments.push({ kind: "tab", start: offset, end: offset + 1, x, width: next - x });
1182
2711
  x = next;
1183
2712
  offset++;
1184
2713
  }
1185
2714
  if (segment) {
1186
2715
  const advance = measure(segment, fontSize);
1187
- if (record3) segments.push({ kind: "text", start: offset, end: offset + segment.length, x, width: advance });
2716
+ if (record5) segments.push({ kind: "text", start: offset, end: offset + segment.length, x, width: advance });
1188
2717
  x += advance;
1189
2718
  offset += segment.length;
1190
2719
  }
@@ -1193,26 +2722,34 @@ function fitSourceText(text, box, size, minimum, step, measure, prose = false) {
1193
2722
  };
1194
2723
  const width = (start2, end2) => measureLine(start2, end2).width;
1195
2724
  const sourceLines = [];
1196
- let start = 0, end = 0;
2725
+ let start = 0, end = 0, hangFrom = -1;
1197
2726
  const push = (last, nextStart, boundary) => {
1198
- sourceLines.push({ start, end: last, nextStart, boundary, ...measureLine(start, last, true) });
2727
+ const measured = measureLine(start, last, true);
2728
+ if (hangFrom >= 0 && hangFrom < last) measured.width = measureLine(start, hangFrom).width;
2729
+ hangFrom = -1;
2730
+ sourceLines.push({ start, end: last, nextStart, boundary, ...measured });
1199
2731
  start = nextStart;
1200
2732
  end = nextStart;
1201
2733
  };
1202
2734
  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)) {
2735
+ for (const token of text2.matchAll(tokens)) {
1204
2736
  if (token.index === void 0) throw new Error("Missing code token source offset.");
1205
2737
  const a = token.index, b = a + token[0].length;
1206
2738
  if (/^[\r\n]/.test(token[0])) {
1207
2739
  push(a, b, "hard");
1208
2740
  continue;
1209
2741
  }
1210
- if (width(start, b) <= box.width + 0.01) {
2742
+ if (width(start, b) <= box.width + WRAP_TOLERANCE) {
2743
+ end = b;
2744
+ continue;
2745
+ }
2746
+ if (prose && end > start && /\S/u.test(text2.slice(start, end)) && HANGING_SPACE.test(token[0][0])) {
2747
+ if (hangFrom < 0) hangFrom = end;
1211
2748
  end = b;
1212
2749
  continue;
1213
2750
  }
1214
- if (end > start && /\S/u.test(text.slice(start, end))) push(a, a, "soft");
1215
- if (width(start, b) <= box.width + 0.01) {
2751
+ if (end > start && /\S/u.test(text2.slice(start, end))) push(a, a, "soft");
2752
+ if (width(start, b) <= box.width + WRAP_TOLERANCE) {
1216
2753
  end = b;
1217
2754
  continue;
1218
2755
  }
@@ -1229,39 +2766,40 @@ function fitSourceText(text, box, size, minimum, step, measure, prose = false) {
1229
2766
  push(end, end, "end");
1230
2767
  const lineHeight = fontSize * 1.22;
1231
2768
  return {
1232
- lines: sourceLines.map((line) => text.slice(line.start, line.end)),
2769
+ lines: sourceLines.map((line) => text2.slice(line.start, line.end)),
1233
2770
  sourceLines,
1234
2771
  fontSize,
1235
2772
  lineHeight,
1236
2773
  tabSize: 4,
1237
2774
  tabWidth,
2775
+ ...directionAt ? { directions: sourceLines.map((line) => directionAt(line.start)) } : {},
1238
2776
  overflow: sourceLines.length * lineHeight > box.height + 0.01 || sourceLines.some((line) => line.width > box.width + 0.01)
1239
2777
  };
1240
2778
  };
1241
- return fitAtSizes(layout, size, minimum, step);
2779
+ return fitAtSizes(layout, size, minimum, step, snap);
1242
2780
  }
1243
2781
  function layoutCode(value, box, options = {}) {
1244
2782
  const shorthand = typeof value === "string", code = shorthand ? { source: value } : value;
1245
2783
  if (!code || Array.isArray(code) || typeof code.source !== "string" || [code.language, code.filename].some((field) => field !== void 0 && typeof field !== "string")) {
1246
2784
  throw new TypeError("Code content must be a string or source object with optional string language/filename.");
1247
2785
  }
1248
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
2786
+ const scale = options.scale ?? 1, minimum = snapFontSizeUp((options.minFontSize ?? 16) * scale);
1249
2787
  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
2788
  throw new RangeError("Code dimensions, scale and minimum font size must be finite and positive.");
1251
2789
  }
1252
2790
  if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid code overflow policy.");
1253
2791
  const path = options.path ?? "code", inner = { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - 36 };
1254
2792
  const parts = [], diagnostics = [];
1255
- const add = (role, text, partPath, generated = false) => {
2793
+ const add = (role, text2, partPath, generated = false) => {
1256
2794
  const requestedStyle = { fontFamily: options.fonts?.code ?? "monospace", fontWeight: role === "body" ? 400 : 700, italic: false, path: partPath };
1257
2795
  const part = {
1258
2796
  role,
1259
2797
  path: partPath,
1260
- text,
1261
- sources: generated ? [] : [{ path: partPath, start: 0, end: text.length }],
2798
+ text: text2,
2799
+ sources: generated ? [] : [{ path: partPath, start: 0, end: text2.length }],
1262
2800
  generated,
1263
2801
  box: { ...inner },
1264
- requestedFontSize: Math.max((role === "body" ? 18 : 14) * scale, minimum),
2802
+ requestedFontSize: gridFontSize((role === "body" ? 18 : 14) * scale, minimum),
1265
2803
  minFontSize: minimum,
1266
2804
  requestedStyle,
1267
2805
  style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement)
@@ -1318,7 +2856,7 @@ function layoutCode(value, box, options = {}) {
1318
2856
  }
1319
2857
  function fitRichText(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
1320
2858
  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);
2859
+ const layout = richTextLayouter(input, box, requestedSize, options, minFontSize);
1322
2860
  return fitAtSizes(layout, requestedSize, richMinimum(input, requestedSize, minFontSize));
1323
2861
  }
1324
2862
  function richMinimum(input, requested, minimum) {
@@ -1332,7 +2870,7 @@ function richMinimum(input, requested, minimum) {
1332
2870
  }
1333
2871
  return visible ? floor : minimum;
1334
2872
  }
1335
- function richTextLayouter(input, box, requestedSize, options) {
2873
+ function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
1336
2874
  let offset = 0;
1337
2875
  const source = input.map((value, runIndex) => {
1338
2876
  const run = typeof value === "string" ? { text: value } : value;
@@ -1340,23 +2878,26 @@ function richTextLayouter(input, box, requestedSize, options) {
1340
2878
  const start = offset;
1341
2879
  offset += run.text.length;
1342
2880
  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 };
2881
+ const marker = options.citationMarker && options.style.path && run.text ? options.citationMarker(`${options.style.path}.${runIndex}`) : void 0;
2882
+ return { run, runIndex, start, end: offset, style, ...marker ? { marker } : {} };
1344
2883
  });
1345
2884
  const whole = source.map((entry) => entry.run.text).join("");
1346
- const layout = (fontSize) => {
1347
- const ratio = fontSize / requestedSize;
2885
+ const paint = (raw) => raw >= minimum - 1e-6 ? gridFontSize(raw, minimum) : snapFontSizeDown(raw);
2886
+ const directionAt = options.direction === "rtl" ? paragraphDirectionAt(whole, "rtl") : void 0;
2887
+ const layout = (rawFontSize) => {
2888
+ const fontSize = paint(rawFontSize), ratio = rawFontSize / requestedSize;
1348
2889
  const fragments = (start, end) => {
1349
2890
  let x = 0;
1350
2891
  const result = [];
1351
2892
  for (const entry of source) {
1352
2893
  const a = Math.max(start, entry.start), b = Math.min(end, entry.end);
1353
2894
  if (b <= a) continue;
1354
- const text = whole.slice(a, b), normalSize = (entry.run.fontSize !== void 0 ? entry.run.fontSize * 4 / 3 : requestedSize) * ratio;
2895
+ const text2 = whole.slice(a, b), normalSize = entry.run.fontSize !== void 0 ? entry.run.fontSize * 4 / 3 * ratio : fontSize;
1355
2896
  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;
2897
+ const size = script ? paint(normalSize * 0.7) : paint(normalSize), baselineShift = entry.run.superscript ? -normalSize * 0.35 : entry.run.subscript ? normalSize * 0.2 : 0;
1357
2898
  const measure = textWidthMeasurer(entry.style, options.textMeasurement);
1358
2899
  let cursor = 0;
1359
- for (const [index, segment] of text.split(" ").entries()) {
2900
+ for (const [index, segment] of text2.split(" ").entries()) {
1360
2901
  if (index) {
1361
2902
  const tabStart = a + cursor, tabWidth = measure(" ", size) * 4;
1362
2903
  if (!Number.isFinite(tabWidth) || tabWidth <= 0) throw new RangeError("Rich text tabs require a positive finite measured space advance.");
@@ -1372,13 +2913,20 @@ function richTextLayouter(input, box, requestedSize, options) {
1372
2913
  cursor += segment.length;
1373
2914
  }
1374
2915
  }
2916
+ if (entry.marker && b === entry.end) {
2917
+ const markerSize = Math.round(normalSize * CITATION_MARKER_SCALE * FONT_SIZE_GRID_PER_PX) / FONT_SIZE_GRID_PER_PX, width2 = measure(entry.marker, markerSize);
2918
+ 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 });
2919
+ x += width2;
2920
+ }
1375
2921
  }
1376
2922
  return result;
1377
2923
  };
1378
2924
  const width = (start, end) => fragments(start, end).reduce((sum, fragment) => sum + fragment.width, 0);
1379
- const ranges = [];
1380
- let lineStart = 0, lineEnd = 0;
2925
+ const ranges = [], hung = /* @__PURE__ */ new Map();
2926
+ let lineStart = 0, lineEnd = 0, hangFrom = -1;
1381
2927
  const push = (end) => {
2928
+ if (hangFrom >= 0) hung.set(ranges.length, hangFrom);
2929
+ hangFrom = -1;
1382
2930
  ranges.push({ start: lineStart, end });
1383
2931
  lineStart = end;
1384
2932
  lineEnd = end;
@@ -1391,26 +2939,32 @@ function richTextLayouter(input, box, requestedSize, options) {
1391
2939
  lineEnd = end;
1392
2940
  continue;
1393
2941
  }
1394
- if (width(lineStart, end) <= box.width) {
2942
+ if (width(lineStart, end) <= box.width + WRAP_TOLERANCE) {
2943
+ lineEnd = end;
2944
+ continue;
2945
+ }
2946
+ if (HANGING_SPACE.test(match[0][0]) && lineEnd > lineStart && /\S/u.test(whole.slice(lineStart, lineEnd))) {
2947
+ if (hangFrom < 0) hangFrom = lineEnd;
1395
2948
  lineEnd = end;
1396
2949
  continue;
1397
2950
  }
1398
2951
  if (lineEnd > lineStart) push(start);
1399
- if (width(start, end) <= box.width) {
2952
+ if (width(start, end) <= box.width + WRAP_TOLERANCE) {
1400
2953
  lineEnd = end;
1401
2954
  continue;
1402
2955
  }
1403
2956
  for (const { segment, index } of textSegments.segment(match[0])) {
1404
2957
  const next = start + index + segment.length;
1405
- if (lineEnd > lineStart && width(lineStart, next) > box.width) push(start + index);
2958
+ if (lineEnd > lineStart && width(lineStart, next) > box.width + WRAP_TOLERANCE) push(start + index);
1406
2959
  lineEnd = next;
1407
2960
  }
1408
2961
  }
2962
+ if (hangFrom >= 0) hung.set(ranges.length, hangFrom);
1409
2963
  ranges.push({ start: lineStart, end: lineEnd });
1410
2964
  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 };
2965
+ const richLines = ranges.map((range, rangeIndex) => {
2966
+ 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));
2967
+ 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
2968
  y += height;
1415
2969
  return line;
1416
2970
  });
@@ -1424,7 +2978,15 @@ function richTextLayouter(input, box, requestedSize, options) {
1424
2978
  y += height;
1425
2979
  }
1426
2980
  }
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) };
2981
+ return {
2982
+ lines: ranges.map((range) => whole.slice(range.start, range.end)),
2983
+ fontSize,
2984
+ lineHeight: Math.max(fontSize * 1.22, ...richLines.map((line) => line.height)),
2985
+ richLines,
2986
+ height: y,
2987
+ overflow: y > box.height + 0.01 || richLines.some((line) => line.width > box.width + 0.01),
2988
+ ...directionAt ? { directions: ranges.map((range) => directionAt(range.start)) } : {}
2989
+ };
1428
2990
  };
1429
2991
  return layout;
1430
2992
  }
@@ -1433,36 +2995,50 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
1433
2995
  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
2996
  const source = input.map((value, index) => {
1435
2997
  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;
2998
+ const text2 = object ? object.text : value, description = object?.description, level = object?.level ?? 0;
1437
2999
  if (!Number.isInteger(level) || level < 0) throw new RangeError("List levels must be nonnegative integers.");
1438
3000
  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.");
3001
+ 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
3002
  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) };
3003
+ const plain = (value2) => typeof value2 === "string" ? value2 : value2.map((run) => typeof run === "string" ? run : run.text).join("");
3004
+ const direction = options.direction === "rtl" ? paragraphDirection(plain(text2), "rtl") : void 0;
3005
+ 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
3006
  });
3007
+ const numbers = options.numbering === void 0 ? void 0 : listNumbers(input, options.numbering);
3008
+ const markerStyleFor = (item) => {
3009
+ const first = item.runs.map((run) => typeof run === "string" ? { text: run } : run).find((run) => run.text);
3010
+ return { ...options.style, path: item.textPath, fontWeight: first?.bold === void 0 ? options.style.fontWeight : first.bold ? 700 : 400, italic: first?.italic ?? options.style.italic };
3011
+ };
1443
3012
  const layout = (fontSize) => {
1444
3013
  let y = box.y, overflow = false;
1445
- const entries = [], lines = [];
3014
+ const entries = [], lines = [], directions = [];
3015
+ const markerStyles = numbers ? source.map((item) => resolveTextStyle(markerStyleFor(item), options.textMeasurement)) : [];
3016
+ const markerWidths = numbers ? numbers.map((number, index) => textWidthMeasurer(markerStyles[index], options.textMeasurement)(number.text, fontSize)) : [];
3017
+ const hanging = numbers ? Math.max(fontSize * 1.1, Math.max(0, ...markerWidths) + fontSize * 0.3) : fontSize * 1.1;
1446
3018
  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);
3019
+ const rtl = item.direction === "rtl";
3020
+ 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
3021
  if (offset + indent >= box.width) overflow = true;
1449
3022
  const textBox = { x, y, width, height: box.height };
1450
3023
  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;
3024
+ const text2 = richTextLayouter(item.runs, textBox, requestedSize, { ...options, style }, minFontSize)(fontSize);
3025
+ textBox.height = text2.height;
3026
+ lines.push(...text2.lines);
3027
+ if (options.direction === "rtl") directions.push(...text2.directions ?? []);
3028
+ y += text2.height;
1455
3029
  let description, descriptionBox;
1456
3030
  if (item.descriptionRuns !== void 0) {
1457
3031
  y += fontSize * 0.12;
1458
3032
  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);
3033
+ description = richTextLayouter(item.descriptionRuns, descriptionBox, requestedSize * 0.82, { ...options, style: { ...options.style, path: item.descriptionPath } }, minFontSize)(fontSize * 0.82);
1460
3034
  descriptionBox.height = description.height;
1461
3035
  lines.push(...description.lines);
3036
+ if (options.direction === "rtl") directions.push(...description.directions ?? []);
1462
3037
  y += description.height;
1463
3038
  }
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;
3039
+ overflow ||= text2.overflow || !!description?.overflow;
3040
+ const number = numbers?.[item.index];
3041
+ 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
3042
  entries.push({
1467
3043
  index: item.index,
1468
3044
  level: item.level,
@@ -1470,17 +3046,18 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
1470
3046
  descriptionValue: item.descriptionValue,
1471
3047
  textPath: item.textPath,
1472
3048
  descriptionPath: item.descriptionPath,
1473
- text,
3049
+ text: text2,
1474
3050
  textBox,
1475
3051
  description,
1476
3052
  descriptionBox,
1477
3053
  marker,
1478
- ...options.bulletImage ? { bulletImage: options.bulletImage, bulletBox: { x: marker.x, y: marker.y - side, width: side, height: side } } : {}
3054
+ ...item.direction ? { direction: item.direction } : {},
3055
+ ...options.bulletImage && !number ? { bulletImage: options.bulletImage, bulletBox: { x: rtl ? marker.x - side : marker.x, y: marker.y - side, width: side, height: side } } : {}
1479
3056
  });
1480
3057
  if (item.index < source.length - 1) y += fontSize * 0.28;
1481
3058
  }
1482
3059
  const height = y - box.y;
1483
- return { listEntries: entries, lines, fontSize, lineHeight: fontSize * 1.22, height, overflow: overflow || height > box.height + 0.01 };
3060
+ return { listEntries: entries, lines, fontSize, lineHeight: fontSize * 1.22, height, overflow: overflow || height > box.height + 0.01, ...options.direction === "rtl" ? { directions } : {} };
1484
3061
  };
1485
3062
  let minimum = minFontSize;
1486
3063
  for (const item of source) {
@@ -1492,16 +3069,16 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
1492
3069
  function contentText(field, value) {
1493
3070
  if (field === "text" || headings.has(field)) return flatten(value);
1494
3071
  if (field === "items" || field === "bullets") return (Array.isArray(value) ? value : [value]).map((item) => {
1495
- const entry = record2(item);
3072
+ const entry = record4(item);
1496
3073
  return `\u2022 ${flatten(item)}${entry.description ? `
1497
3074
  ${flatten(entry.description)}` : ""}`;
1498
3075
  }).join("\n");
1499
- if (field === "code") return flatten(record2(value).source ?? value);
1500
- if (field === "quote") return flatten(record2(value).text ?? value);
3076
+ if (field === "code") return flatten(record4(value).source ?? value);
3077
+ if (field === "quote") return flatten(record4(value).text ?? value);
1501
3078
  return void 0;
1502
3079
  }
1503
3080
  function layoutTable(value, box, options = {}) {
1504
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale, requested = Math.max(15 * scale, minimum);
3081
+ const scale = options.scale ?? 1, minimum = snapFontSizeUp((options.minFontSize ?? 16) * scale), requested = gridFontSize(15 * scale, minimum);
1505
3082
  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
3083
  const grid = tableGrid(value, options.path ?? "table");
1507
3084
  if (grid.issues.length) throw new RangeError(`${grid.issues[0].path}: ${grid.issues[0].message}`);
@@ -1515,13 +3092,13 @@ function layoutTable(value, box, options = {}) {
1515
3092
  const cellMinimum = (cell) => Array.isArray(cell.value) ? richMinimum(cell.value, requested, minimum) : minimum;
1516
3093
  const fitCell = (cell, height, min) => {
1517
3094
  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));
3095
+ 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
3096
  };
1520
3097
  const textHeight = (fit) => "height" in fit ? fit.height : fit.lines.length * fit.lineHeight;
1521
3098
  const required = (cell, natural2) => {
1522
- const floor = cellMinimum(cell), size = natural2 ? Math.max(requested, floor) : floor;
3099
+ const floor = cellMinimum(cell), size = gridFontSize(natural2 ? Math.max(requested, floor) : floor, floor);
1523
3100
  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));
3101
+ 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
3102
  return textHeight(fit) + (cell.padding.top + cell.padding.bottom) * scale;
1526
3103
  };
1527
3104
  const natural = Array(grid.rowCount).fill(0), needed = Array(grid.rowCount).fill(0);
@@ -1544,13 +3121,13 @@ function layoutTable(value, box, options = {}) {
1544
3121
  let overflow = false;
1545
3122
  const rows2 = heights.map((height, r) => ({ box: { x: box.x, y: ys[r], width: box.width, height }, cells: [] }));
1546
3123
  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] };
3124
+ 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
3125
  const available = cellBox.height - (cell.padding.top + cell.padding.bottom) * scale;
1549
3126
  const fit = fitCell(cell, available, minimum), height = textHeight(fit);
1550
3127
  const offset = cell.style.verticalAlign === "bottom" ? Math.max(0, available - height) : cell.style.verticalAlign === "middle" ? Math.max(0, (available - height) / 2) : 0;
1551
3128
  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
3129
  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 });
3130
+ 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
3131
  }
1555
3132
  return { rows: rows2, columnCount, height: sum(heights), overflow };
1556
3133
  }
@@ -1561,7 +3138,7 @@ function flatten(value) {
1561
3138
  if (value == null) return "";
1562
3139
  if (typeof value !== "object") return String(value);
1563
3140
  if (Array.isArray(value)) return value.map(flatten).join("");
1564
- const item = record2(value);
3141
+ const item = record4(value);
1565
3142
  return flatten(item.text ?? item.value ?? item.source ?? JSON.stringify(value));
1566
3143
  }
1567
3144
  function regionParts(key) {
@@ -1617,26 +3194,28 @@ function assertComposition(value) {
1617
3194
  if (value.overflow !== void 0 && !["warn", "error"].includes(value.overflow)) throw new RangeError("Invalid composition.overflow.");
1618
3195
  }
1619
3196
  function composeSlide(input, options = {}) {
1620
- const slide = record2(input), layout = record2(options.layout);
3197
+ const slide = record4(input), layout = record4(options.layout);
1621
3198
  const width = options.width ?? 1280, height = options.height ?? 720;
1622
3199
  if (![width, height].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
1623
3200
  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) };
3201
+ const deckDirection = options.direction ?? resolveSlideDirection(options.presentation, options.slideIndex);
3202
+ const rtl = deckDirection === "rtl", textDirection = rtl ? "rtl" : void 0;
3203
+ const hasCards = record4(slide.design).contentBox ?? options.contentBox ?? false;
3204
+ const composition = { ...record4(layout.composition), ...record4(slide.composition) };
1626
3205
  assertComposition(composition);
1627
3206
  const padding = (composition.padding ?? 0.08) * Math.min(width, height);
1628
3207
  const gap = (composition.gap ?? 1 / 30) * Math.min(width, height);
1629
- const minSize = (composition.minFontSize ?? 16) * scale;
3208
+ const minSize = snapFontSizeUp((composition.minFontSize ?? 16) * scale);
1630
3209
  const rasterPadding = (options.textRasterPadding ?? 1) * scale;
1631
3210
  if (!Number.isFinite(rasterPadding) || rasterPadding < 0) throw new RangeError("Text raster padding must be finite and nonnegative.");
1632
3211
  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";
3212
+ 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
3213
  const styleFor = (field, path2) => resolveTextStyle({
1635
3214
  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
3215
  fontWeight: field === "title" ? 700 : 400,
1637
3216
  path: path2
1638
3217
  }, options.textMeasurement);
1639
- const slideDesign = record2(slide.design), deckDesign = record2(record2(options.presentation).design);
3218
+ const slideDesign = record4(slide.design), deckDesign = record4(record4(options.presentation).design);
1640
3219
  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
3220
  const listBullet = designHint("listBullet");
1642
3221
  const bulletImage = listBullet?.value === "image" ? (() => {
@@ -1644,15 +3223,18 @@ function composeSlide(input, options = {}) {
1644
3223
  return resolved ? { source: resolved.source, path: resolved.path } : void 0;
1645
3224
  })() : void 0;
1646
3225
  const widthFor = (field, path2) => textWidthMeasurer(styleFor(field, path2), options.textMeasurement);
1647
- const fitPlacedText = (field, value, text, box, size, minimum, path2) => {
3226
+ const citations = slideCitations(slide, options.slideIndex ?? 0, options.presentation);
3227
+ const citationMarker = citations ? (runPath) => citations.markers.get(runPath) : void 0;
3228
+ const richOptions = (style) => ({ style, textMeasurement: options.textMeasurement, ...citationMarker ? { citationMarker } : {}, ...rtl ? { direction: "rtl" } : {} });
3229
+ const fitPlacedText = (field, value, text2, box, size, minimum, path2, explicitAlignment) => {
1648
3230
  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);
3231
+ if (!options.textMeasurement?.outlineBounds) return rich ? fitRichText(value, box, size, minimum, richOptions(style)) : fitText(text2, box, size, minimum, textWidthMeasurer(style, options.textMeasurement), textDirection);
3232
+ const alignment = explicitAlignment ?? alignmentFor(field);
3233
+ const richLayout = rich ? richTextLayouter(value, box, size, richOptions(style), minimum) : void 0;
3234
+ const measure = textWidthMeasurer(style, options.textMeasurement), floor = snapFontSizeUp(rich ? richMinimum(value, size, minimum) : minimum), start = Math.max(size, floor);
1653
3235
  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);
3236
+ const raw = start - trial * scale, last = trial === 64 || raw <= floor, fontSize = last ? floor : gridFontSize(raw, floor);
3237
+ const fit = richLayout ? richLayout(fontSize) : fitText(text2, box, fontSize, fontSize, measure, textDirection);
1656
3238
  const richLines = "richLines" in fit ? fit.richLines : void 0;
1657
3239
  const lines = richLines ? richLines.map((line) => {
1658
3240
  let outline = null;
@@ -1675,7 +3257,7 @@ function composeSlide(input, options = {}) {
1675
3257
  let outline = null;
1676
3258
  for (const segment of line.segments) {
1677
3259
  if (segment.kind === "tab") continue;
1678
- const bounds = measureTextOutline(text.slice(segment.start, segment.end), fontSize, style, options.textMeasurement);
3260
+ const bounds = measureTextOutline(text2.slice(segment.start, segment.end), fontSize, style, options.textMeasurement);
1679
3261
  if (!bounds) continue;
1680
3262
  const next = { ...bounds, x: segment.x + bounds.x };
1681
3263
  if (!outline) outline = next;
@@ -1689,15 +3271,17 @@ function composeSlide(input, options = {}) {
1689
3271
  }
1690
3272
  return { width: line.width, y: index * fit.lineHeight, baseline: fontSize + index * fit.lineHeight, height: fit.lineHeight, outline };
1691
3273
  });
1692
- const placement = placeTextLines(lines, box, alignment, rasterPadding), result = { ...fit, placement, overflow: fit.overflow || placement.overflow };
1693
- if (!result.overflow || fontSize === floor) return result;
3274
+ const placement = placeTextLines(lines, box, alignment, rasterPadding, fit.directions), result = { ...fit, placement, overflow: fit.overflow || placement.overflow };
3275
+ if (!result.overflow || last) return result;
1694
3276
  }
1695
3277
  throw new Error("Text placement did not evaluate its bounded floor trial.");
1696
3278
  };
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));
3279
+ 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
3280
  const path = `slides.${options.slideIndex ?? 0}`;
3281
+ const annotationFit = (value, box, requestedSize, minFontSize, fitPath, alignment) => fitPlacedText("text", value, annotationText(value), box, requestedSize, minFontSize, fitPath, alignment);
3282
+ const annotationOptions = { scale, minFontSize: minSize, fit: annotationFit, textStyle: (fitPath) => styleFor("text", fitPath) };
1699
3283
  const slideImageDiagnostics = [];
1700
- const slideImage = resolveSlideImage(slide, layout, options.presentation, width, height, path, padding, scale, slideImageDiagnostics);
3284
+ const slideImage = resolveSlideImage(slide, layout, options.presentation, width, height, path, padding, scale, slideImageDiagnostics, rtl);
1701
3285
  const area = { left: 0, top: 0, right: width, bottom: height };
1702
3286
  if (slideImage?.position === "left") area.left = slideImage.region.width;
1703
3287
  else if (slideImage?.position === "right") area.right = slideImage.region.x;
@@ -1707,14 +3291,20 @@ function composeSlide(input, options = {}) {
1707
3291
  const measuredFurniture = layoutFurniture(slide, options);
1708
3292
  const furniture = measuredFurniture.configured || measuredFurniture.diagnostics.length ? measuredFurniture : void 0;
1709
3293
  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);
3294
+ let bodyBottom = Math.min(area.bottom - padding, furniture && furniture.footerTop < height ? furniture.footerTop - gap * 0.5 : height - padding);
1711
3295
  const headingTop = Math.max(area.top + padding, furniture?.headerBottom ? furniture.headerBottom + gap * 0.5 : padding);
3296
+ let footnotes;
3297
+ if (citations) {
3298
+ 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 });
3299
+ diagnostics.push(...footnotes.diagnostics);
3300
+ bodyBottom = footnotes.box.y - gap * 0.5;
3301
+ }
1712
3302
  const layoutPlaceholders = Array.isArray(layout.placeholders) ? layout.placeholders : [];
1713
3303
  const headingOnlyLayout = COVER_LAYOUT_IDS.has(String(layout.id ?? "")) || layoutPlaceholders.length > 0 && layoutPlaceholders.every((placeholder) => headings.has(placeholder.type ?? ""));
1714
3304
  const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
1715
3305
  const emptyPayload = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
1716
3306
  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]));
3307
+ const hasBodyPayload = regions.some((key) => !emptyHost(record4(slide[key]))) || !emptyPayload(slide.blocks) || fields.some((field) => !emptyPayload(slide[field]));
1718
3308
  const isCover = !hasBodyPayload && (headingOnlyLayout || !layout.id && !layoutPlaceholders.some((placeholder) => !headings.has(placeholder.type ?? "")));
1719
3309
  coverGroup = isCover;
1720
3310
  let logo;
@@ -1722,8 +3312,9 @@ function composeSlide(input, options = {}) {
1722
3312
  const resolved = resolveLogo(options.presentation, slide, { slot: "lockup", onDark: options.darkBackground, slideIndex: options.slideIndex });
1723
3313
  if (resolved) {
1724
3314
  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" };
3315
+ const logoWidth = Math.max(scale, Math.min(4 * logoHeight, area.right - area.left - 2 * padding));
3316
+ const box = { x: round2(rtl ? area.right - padding - logoWidth : area.left + padding), y: round2(headingTop), width: round2(logoWidth), height: round2(logoHeight) };
3317
+ logo = { box, slot: "lockup", path: resolved.path, source: resolved.source, variant: resolved.variant, anchor: rtl ? "right" : "left" };
1727
3318
  }
1728
3319
  }
1729
3320
  let y = logo ? logo.box.y + logo.box.height + gap : headingTop;
@@ -1733,10 +3324,10 @@ function composeSlide(input, options = {}) {
1733
3324
  const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale;
1734
3325
  const maxHeight = Math.max(height * (field === "title" ? 0.26 : field === "subtitle" ? 0.12 : 0.045), minSize * 1.22 + 2 * rasterPadding);
1735
3326
  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));
3327
+ const text2 = fitPlacedText(field, slide[field], String(slide[field]), box, requested, minSize, `${path}.${field}`);
3328
+ box.height = Math.min(maxHeight, Math.max(text2.lines.length * text2.lineHeight, text2.placement?.height ?? 0));
1738
3329
  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) };
3330
+ 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
3331
  headingItems.push(item);
1741
3332
  items.push(item);
1742
3333
  y += box.height + gap * 0.5;
@@ -1756,7 +3347,7 @@ function composeSlide(input, options = {}) {
1756
3347
  const collect = (host, basePath, depth = 0, ancestors = []) => {
1757
3348
  if (Array.isArray(host.blocks)) {
1758
3349
  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);
3350
+ const settings = record4(host.composition);
1760
3351
  assertComposition(settings);
1761
3352
  return [{
1762
3353
  field: "blocks",
@@ -1765,12 +3356,21 @@ function composeSlide(input, options = {}) {
1765
3356
  path: basePath,
1766
3357
  payload: host,
1767
3358
  composition: settings,
1768
- children: host.blocks.flatMap((block, index) => collect(record2(block), `${basePath}.blocks.${index}`, depth + 1, [...ancestors, host]))
3359
+ children: host.blocks.flatMap((block, index) => collect(record4(block), `${basePath}.blocks.${index}`, depth + 1, [...ancestors, host]))
1769
3360
  }];
1770
3361
  }
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] } }));
3362
+ const captioned = host.caption !== void 0 ? fields.filter((field) => host[field] !== void 0 && CAPTIONABLE_FIELDS.includes(field)) : [];
3363
+ return fields.filter((field) => host[field] !== void 0).map((field) => ({
3364
+ field,
3365
+ type: host.type ?? kind(field),
3366
+ value: host[field],
3367
+ path: `${basePath}.${field}`,
3368
+ payload: { type: host.type ?? kind(field), [field]: host[field], ...host.numbering !== void 0 && (field === "items" || field === "bullets") ? { numbering: host.numbering } : {} },
3369
+ ...captioned.length === 1 && captioned[0] === field ? { caption: host.caption, captionPath: `${basePath}.caption` } : {}
3370
+ }));
1772
3371
  };
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);
3372
+ const regionsInReadingOrder = visualReadingOrder(regions.map((key) => ({ key, box: regionBox(regionParts(key), contentBox, gap) }))).map((entry) => entry.key);
3373
+ 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
3374
  const groups = [], flows = [];
1775
3375
  const decisions = options.explain ? [] : void 0;
1776
3376
  const inheritedSettings = (parent, own = {}) => ({
@@ -1782,7 +3382,7 @@ function composeSlide(input, options = {}) {
1782
3382
  const amount = (settings.padding ?? 0) * Math.min(box.width, box.height);
1783
3383
  return { x: box.x + amount, y: box.y + amount, width: box.width - amount * 2, height: box.height - amount * 2 };
1784
3384
  };
1785
- const acceptedBox = (box) => ({ x: round(box.x), y: round(box.y), width: round(box.width), height: round(box.height) });
3385
+ const acceptedBox = (box) => ({ x: round2(box.x), y: round2(box.y), width: round2(box.width), height: round2(box.height) });
1786
3386
  const payloadBox = (box) => {
1787
3387
  if (!hasCards) return box;
1788
3388
  const frame = acceptedBox(box), padding2 = Math.min(12 * scale, frame.width / 4, frame.height / 4);
@@ -1793,7 +3393,8 @@ function composeSlide(input, options = {}) {
1793
3393
  textMeasurement: options.textMeasurement,
1794
3394
  scale,
1795
3395
  minFontSize: settings.minFontSize,
1796
- path: node.path
3396
+ path: node.path,
3397
+ direction: textDirection
1797
3398
  });
1798
3399
  const measureCode = (node, box, settings) => layoutCode(node.value, acceptedBox(box), {
1799
3400
  fonts: options.fonts,
@@ -1809,7 +3410,8 @@ function composeSlide(input, options = {}) {
1809
3410
  minFontSize: settings.minFontSize,
1810
3411
  path: node.path,
1811
3412
  textRasterPadding: options.textRasterPadding,
1812
- align: alignmentFor("metric")
3413
+ align: alignmentFor("metric"),
3414
+ direction: textDirection
1813
3415
  });
1814
3416
  const measureTimeline = (node, box, settings) => layoutTimeline(node.value, acceptedBox(box), {
1815
3417
  fonts: options.fonts,
@@ -1817,13 +3419,20 @@ function composeSlide(input, options = {}) {
1817
3419
  scale,
1818
3420
  minFontSize: settings.minFontSize,
1819
3421
  path: node.path,
1820
- textRasterPadding: options.textRasterPadding
3422
+ textRasterPadding: options.textRasterPadding,
3423
+ direction: textDirection
1821
3424
  });
3425
+ const captionOf = (node, box) => node.caption !== void 0 && node.captionPath ? layoutCaption(node.caption, box, node.captionPath, annotationOptions) : void 0;
1822
3426
  const leafScore = (node, box, settings, penalties) => {
1823
3427
  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;
3428
+ const captioned = captionOf(node, box);
3429
+ if (captioned) box = captioned.mediaBox;
3430
+ const text2 = contentText(node.field, node.value);
3431
+ let score = Math.abs(Math.log(box.width / box.height / 1.6)) + (captioned?.overflow ? 1e3 : 0);
3432
+ if (penalties) {
3433
+ penalties.cellProportions += Math.abs(Math.log(box.width / box.height / 1.6));
3434
+ if (captioned?.overflow) penalties.textOverflow += 1e3;
3435
+ }
1827
3436
  if (node.field === "quote" || node.field === "code" || node.field === "metric" || node.field === "timeline") {
1828
3437
  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
3438
  const reduction = internal.parts.reduce((sum, part) => sum + (part.fit ? (part.requestedFontSize - part.fit.fontSize) / scale : 0), 0);
@@ -1832,9 +3441,9 @@ function composeSlide(input, options = {}) {
1832
3441
  penalties.fontReduction += reduction;
1833
3442
  penalties.textOverflow += internal.overflow ? 1e3 : 0;
1834
3443
  }
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;
3444
+ } else if (text2) {
3445
+ const fit = fitContent(node.field, node.value, text2, box, 25 * scale, snapFontSizeUp((settings.minFontSize ?? 16) * scale), node.path, node.payload.numbering);
3446
+ const reduction = Math.max(0, snapFontSizeDown(25 * scale) - fit.fontSize) / scale;
1838
3447
  score += reduction + (fit.overflow ? 1e3 : 0);
1839
3448
  if (penalties) {
1840
3449
  penalties.fontReduction += reduction;
@@ -1893,10 +3502,11 @@ function composeSlide(input, options = {}) {
1893
3502
  candidates: candidates ?? []
1894
3503
  });
1895
3504
  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 });
3505
+ const mirrored = (box) => rtl ? { ...box, x: area2.x + area2.width - (box.x - area2.x) - box.width } : box;
3506
+ const boxes = grid.boxes.map(mirrored);
3507
+ 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
3508
  nodes.forEach((node, index) => {
1899
- let box = node.region ? regionBox(node.region, area2, actualGap) : boxes[index];
3509
+ let box = node.region ? mirrored(regionBox(node.region, area2, actualGap)) : boxes[index];
1900
3510
  if (node.children) {
1901
3511
  const own = inheritedSettings(settings, node.composition), inner = inset(box, own);
1902
3512
  if (!node.synthetic) groups.push({ path: node.path, box, contentBox: inner, composition: own });
@@ -1904,13 +3514,18 @@ function composeSlide(input, options = {}) {
1904
3514
  } else {
1905
3515
  const frameBox = hasCards ? acceptedBox(box) : void 0;
1906
3516
  box = payloadBox(box);
3517
+ const caption = captionOf(node, box);
3518
+ if (caption) {
3519
+ box = caption.mediaBox;
3520
+ diagnostics.push(...caption.diagnostics);
3521
+ }
1907
3522
  const textValue = contentText(node.field, node.value);
1908
3523
  const quoteLayout = node.field === "quote" ? measureQuote(node, box, settings) : void 0;
1909
3524
  const codeLayout = node.field === "code" ? measureCode(node, box, settings) : void 0;
1910
3525
  const metricLayout = node.field === "metric" ? measureMetric(node, box, settings) : void 0;
1911
3526
  const timelineLayout = node.field === "timeline" ? measureTimeline(node, box, settings) : void 0;
1912
3527
  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;
3528
+ 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
3529
  items.push({
1915
3530
  path: node.path,
1916
3531
  field: node.field,
@@ -1919,7 +3534,7 @@ function composeSlide(input, options = {}) {
1919
3534
  payload: node.payload,
1920
3535
  box: internal ? acceptedBox(box) : box,
1921
3536
  ...frameBox ? { frameBox } : {},
1922
- text,
3537
+ text: text2,
1923
3538
  textStyle: body?.style ?? styleFor(node.field, node.path),
1924
3539
  composition: settings,
1925
3540
  alignment: alignmentFor(node.field),
@@ -1927,7 +3542,8 @@ function composeSlide(input, options = {}) {
1927
3542
  ...codeLayout ? { codeLayout } : {},
1928
3543
  ...metricLayout ? { metricLayout } : {},
1929
3544
  ...timelineLayout ? { timelineLayout } : {},
1930
- ...bulletImage && (node.field === "items" || node.field === "bullets") ? { bulletImage } : {}
3545
+ ...bulletImage && node.payload.numbering === void 0 && (node.field === "items" || node.field === "bullets") ? { bulletImage } : {},
3546
+ ...caption ? { caption } : {}
1931
3547
  });
1932
3548
  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
3549
  }
@@ -1938,7 +3554,7 @@ function composeSlide(input, options = {}) {
1938
3554
  const picture = placeholders.findIndex((p) => p.type === "picture");
1939
3555
  if (picture >= 0) placeholders.splice(picture, 1);
1940
3556
  }
1941
- const ownMode = record2(slide.composition).mode;
3557
+ const ownMode = record4(slide.composition).mode;
1942
3558
  const direction = designHint("contentDirection")?.value;
1943
3559
  const directionMode = direction === "vertical" ? "column" : direction === "horizontal" ? "row" : void 0;
1944
3560
  const layoutDirectionMode = layout.slideLayoutDirection === "Vertical" ? "column" : layout.slideLayoutDirection === "Horizontal" ? "row" : "auto";
@@ -1952,19 +3568,23 @@ function composeSlide(input, options = {}) {
1952
3568
  const container = { field: "blocks", type: "group", value: rest.map((node) => node.payload), path, payload: {}, children: rest, composition: {}, synthetic: true };
1953
3569
  arrange(chartPrimary === "left" || chartPrimary === "top" ? [primary, container] : [container, primary], contentBox, rootSettings, 0);
1954
3570
  } 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")) {
3571
+ if (listBullet?.value === "image" && !bulletImage && items.some((item) => (item.field === "items" || item.field === "bullets") && item.payload.numbering === void 0)) {
1956
3572
  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
3573
  }
3574
+ const numberingDiagnostics = [];
1958
3575
  for (const item of items) {
1959
- for (const key of ["x", "y", "width", "height"]) item.box[key] = round(item.box[key]);
3576
+ for (const key of ["x", "y", "width", "height"]) item.box[key] = round2(item.box[key]);
1960
3577
  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
3578
  if (item.quoteLayout) diagnostics.push(...item.quoteLayout.diagnostics);
1962
3579
  else if (item.codeLayout) diagnostics.push(...item.codeLayout.diagnostics);
1963
3580
  else if (item.metricLayout) diagnostics.push(...item.metricLayout.diagnostics);
1964
3581
  else if (item.timelineLayout) diagnostics.push(...item.timelineLayout.diagnostics);
1965
3582
  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." });
3583
+ for (const entry of item.text?.listEntries ?? []) {
3584
+ 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.` });
3585
+ }
1966
3586
  }
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]);
3587
+ 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
3588
  const strictPaths = new Set(items.filter((item) => item.composition.overflow === "error").map((item) => item.path));
1969
3589
  const failures = diagnostics.filter((diagnostic) => {
1970
3590
  if (composition.overflow === "error") return true;
@@ -1986,8 +3606,8 @@ function composeSlide(input, options = {}) {
1986
3606
  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
3607
  } : void 0;
1988
3608
  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 } : {} };
3609
+ diagnostics.push(...slideImageDiagnostics, ...numberingDiagnostics);
3610
+ return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...furniture ? { furniture } : {}, ...slideImage ? { slideImage } : {}, ...logo ? { logo } : {}, ...rtl ? { direction: "rtl" } : {}, ...footnotes ? { footnotes } : {}, ...explanation ? { explanation } : {} };
1991
3611
  }
1992
3612
  function resolveCanvasDimensions(input) {
1993
3613
  const presets = {
@@ -1999,7 +3619,7 @@ function resolveCanvasDimensions(input) {
1999
3619
  letter: [11, 8.5],
2000
3620
  a4: [11.69, 8.27]
2001
3621
  };
2002
- const value = record2(input);
3622
+ const value = record4(input);
2003
3623
  const preset = presets[typeof input === "string" ? input : value.preset] ?? presets.widescreen;
2004
3624
  const width = (value.widthInches ?? preset[0]) * 96;
2005
3625
  const height = (value.heightInches ?? preset[1]) * 96;
@@ -2007,4 +3627,4 @@ function resolveCanvasDimensions(input) {
2007
3627
  return { width, height };
2008
3628
  }
2009
3629
 
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 };
3630
+ 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 };