@openpresentation/opf 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/{catalogs-BnnkUX9O.d.ts → catalogs-HCTav5Wv.d.ts} +1 -1
- package/dist/catalogs.d.ts +2 -2
- package/dist/chunk-BAWLYSUN.js +1326 -0
- package/dist/{chunk-HIY26FJV.js → chunk-GCB6LGJM.js} +47 -4
- package/dist/chunk-NPOKUFEX.js +595 -0
- package/dist/{chunk-X22DFHMW.js → chunk-TK3QHM3U.js} +3 -3
- package/dist/composition.d.ts +264 -5
- package/dist/composition.js +1 -1
- package/dist/docs.js +36 -6
- package/dist/examples.js +2284 -1417
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/lint.d.ts +70 -0
- package/dist/lint.js +5 -0
- package/dist/pagination.d.ts +2 -0
- package/dist/pagination.js +3 -3
- package/dist/{schemas--QRVBhU1.d.ts → schemas-BYe5y8-i.d.ts} +1 -1
- package/dist/schemas.d.ts +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/validator.d.ts +2 -2
- package/dist/validator.js +2 -2
- package/package.json +11 -6
- package/dist/chunk-NEQAPMS2.js +0 -719
package/dist/chunk-NEQAPMS2.js
DELETED
|
@@ -1,719 +0,0 @@
|
|
|
1
|
-
// src/table.ts
|
|
2
|
-
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3
|
-
function tableGrid(value, path = "table") {
|
|
4
|
-
const table = record(value), hasHeaders = Array.isArray(table.columns) && table.columns.length > 0;
|
|
5
|
-
const values = [...hasHeaders ? [table.columns] : [], ...Array.isArray(table.rows) ? table.rows : []];
|
|
6
|
-
const columnCount = values.reduce((count, row) => Math.max(count, Array.isArray(row) ? row.length : 0), 1), rowCount = values.length;
|
|
7
|
-
const owners = Array.from({ length: rowCount }, () => Array(columnCount));
|
|
8
|
-
const rows2 = Array.from({ length: rowCount }, () => []), issues = [];
|
|
9
|
-
const location = (r, c) => `${path}.${hasHeaders && r === 0 ? "columns" : `rows.${r - Number(hasHeaders)}`}.${c}`;
|
|
10
|
-
for (let r = 0; r < rowCount; r++) for (let c = 0; c < columnCount; c++) {
|
|
11
|
-
const input = values[r]?.[c], cellPath = location(r, c), existing = owners[r][c];
|
|
12
|
-
if (existing) {
|
|
13
|
-
if (input !== null) issues.push({ path: cellPath, message: "A position covered by a spanning cell must explicitly contain null; content cannot be hidden." });
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
const object = record(input), styled = Object.hasOwn(object, "value");
|
|
17
|
-
const rowSpan = styled ? object.rowSpan ?? 1 : 1, colSpan = styled ? object.colSpan ?? 1 : 1;
|
|
18
|
-
if (!Number.isSafeInteger(rowSpan) || !Number.isSafeInteger(colSpan) || rowSpan < 1 || colSpan < 1 || r + rowSpan > rowCount || c + colSpan > columnCount || hasHeaders && r === 0 && rowSpan > 1) {
|
|
19
|
-
issues.push({ path: cellPath, message: "Cell spans must be positive integers within the table grid and cannot cross from headers into body rows." });
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
const cell = { input, value: styled ? object.value : input, style: styled ? record(object.style) : {}, row: r, column: c, rowSpan, colSpan, header: hasHeaders && r === 0, path: cellPath, valuePath: styled ? cellPath + ".value" : cellPath };
|
|
23
|
-
rows2[r].push(cell);
|
|
24
|
-
for (let y = r; y < r + rowSpan; y++) for (let x = c; x < c + colSpan; x++) {
|
|
25
|
-
if (owners[y][x]) issues.push({ path: cellPath, message: "Cell spans cannot overlap another spanning cell." });
|
|
26
|
-
else owners[y][x] = cell;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return { rows: rows2, columnCount, rowCount, hasHeaders, owners, issues };
|
|
30
|
-
}
|
|
31
|
-
function tableRowBoundaries(value) {
|
|
32
|
-
const grid = tableGrid(value), offset = Number(grid.hasHeaders), count = grid.rowCount - offset;
|
|
33
|
-
const allowed = new Set(Array.from({ length: count + 1 }, (_, i) => i));
|
|
34
|
-
for (const row of grid.rows) for (const cell of row) if (!cell.header) for (let r = 1; r < cell.rowSpan; r++) allowed.delete(cell.row - offset + r);
|
|
35
|
-
return [...allowed];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// src/composition.ts
|
|
39
|
-
var MAX_COMPOSITION_DEPTH = 32;
|
|
40
|
-
function resolveFontFamilies(input) {
|
|
41
|
-
const scheme = record2(input);
|
|
42
|
-
const family = (value) => typeof value === "string" ? value : record2(value).family;
|
|
43
|
-
return {
|
|
44
|
-
heading: family(scheme.heading) ?? scheme.major ?? scheme.minor ?? "Roboto",
|
|
45
|
-
body: family(scheme.body) ?? scheme.minor ?? scheme.major ?? "Roboto",
|
|
46
|
-
code: family(scheme.code) ?? "Roboto Mono"
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function resolveTextStyle(style, measurement) {
|
|
50
|
-
return measurement?.resolveStyle ? measurement.resolveStyle(style) : style;
|
|
51
|
-
}
|
|
52
|
-
function textWidthMeasurer(style, measurement) {
|
|
53
|
-
if (!measurement) return measureText;
|
|
54
|
-
return (text, size) => {
|
|
55
|
-
const width = measurement.measure(text, size, style);
|
|
56
|
-
if (!Number.isFinite(width) || width < 0) throw new RangeError("Text measurement must return a finite, nonnegative width.");
|
|
57
|
-
return width;
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
var OPFCompositionError = class extends Error {
|
|
61
|
-
constructor(diagnostics, explanation) {
|
|
62
|
-
super("Slide content does not fit its composition.");
|
|
63
|
-
this.diagnostics = diagnostics;
|
|
64
|
-
this.name = "OPFCompositionError";
|
|
65
|
-
if (explanation) this.explanation = explanation;
|
|
66
|
-
}
|
|
67
|
-
diagnostics;
|
|
68
|
-
code = "layout-overflow";
|
|
69
|
-
explanation;
|
|
70
|
-
};
|
|
71
|
-
var fields = ["text", "items", "bullets", "image", "video", "chart", "table", "code", "metric", "quote", "timeline"];
|
|
72
|
-
var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
|
|
73
|
-
var rows = ["top", "middle", "bottom"];
|
|
74
|
-
var columns = ["left", "center", "right"];
|
|
75
|
-
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
76
|
-
var kind = (field) => field === "items" || field === "bullets" ? "list" : field;
|
|
77
|
-
var round = (value) => Math.round(value * 1e6) / 1e6 || value;
|
|
78
|
-
var textSegments = new Intl.Segmenter("und", { granularity: "grapheme" });
|
|
79
|
-
function measureText(text, fontSize) {
|
|
80
|
-
let units = 0;
|
|
81
|
-
for (const character of text) {
|
|
82
|
-
if (/\p{Mark}|\u200d|\ufe0f/u.test(character)) continue;
|
|
83
|
-
units += character === " " ? 0.32 : /[A-Z0-9]/.test(character) ? 0.62 : character.codePointAt(0) > 11904 ? 1 : 0.54;
|
|
84
|
-
}
|
|
85
|
-
return units * fontSize;
|
|
86
|
-
}
|
|
87
|
-
function wrapText(text, width, fontSize, measure = measureText) {
|
|
88
|
-
const result = [];
|
|
89
|
-
for (const paragraph of text.replace(/\r\n?/g, "\n").split("\n")) {
|
|
90
|
-
let line = "";
|
|
91
|
-
for (const word of paragraph.split(/\s+/u).filter(Boolean)) {
|
|
92
|
-
if (line && measure(`${line} ${word}`, fontSize) > width) {
|
|
93
|
-
result.push(line);
|
|
94
|
-
line = "";
|
|
95
|
-
}
|
|
96
|
-
if (measure(word, fontSize) <= width) {
|
|
97
|
-
line += (line ? " " : "") + word;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
for (const { segment: character } of textSegments.segment(word)) {
|
|
101
|
-
if (line && measure(line + character, fontSize) > width) {
|
|
102
|
-
result.push(line);
|
|
103
|
-
line = "";
|
|
104
|
-
}
|
|
105
|
-
line += character;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
result.push(line);
|
|
109
|
-
}
|
|
110
|
-
return result;
|
|
111
|
-
}
|
|
112
|
-
function fitText(text, box, requestedSize = 25, minFontSize = 16, measure = measureText) {
|
|
113
|
-
if (![box.width, box.height, requestedSize, minFontSize].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || requestedSize <= 0 || minFontSize <= 0) {
|
|
114
|
-
throw new RangeError("Text dimensions and font sizes must be finite and positive.");
|
|
115
|
-
}
|
|
116
|
-
const minimum = Math.min(minFontSize, requestedSize);
|
|
117
|
-
let fontSize = requestedSize;
|
|
118
|
-
let lines = wrapText(text, box.width, fontSize, measure);
|
|
119
|
-
while (fontSize > minimum && (lines.length * fontSize * 1.22 > box.height || lines.some((line) => measure(line, fontSize) > box.width))) {
|
|
120
|
-
fontSize = Math.max(minimum, fontSize - 1);
|
|
121
|
-
lines = wrapText(text, box.width, fontSize, measure);
|
|
122
|
-
}
|
|
123
|
-
return { lines, fontSize, lineHeight: fontSize * 1.22, overflow: lines.length * fontSize * 1.22 > box.height + 0.01 || lines.some((line) => measure(line, fontSize) > box.width + 0.01) };
|
|
124
|
-
}
|
|
125
|
-
function layoutQuote(value, box, options = {}) {
|
|
126
|
-
const shorthand = typeof value === "string";
|
|
127
|
-
const quote = shorthand ? { text: value } : value;
|
|
128
|
-
if (!quote || Array.isArray(quote) || typeof quote.text !== "string" || [quote.attribution, quote.source].some((field) => field !== void 0 && typeof field !== "string")) {
|
|
129
|
-
throw new TypeError("Quote content must be a string or a text object with optional string attribution/source.");
|
|
130
|
-
}
|
|
131
|
-
const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
|
|
132
|
-
if (![box.x, box.y, box.width, box.height, scale, minimum].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0) {
|
|
133
|
-
throw new RangeError("Quote dimensions, scale and minimum font size must be finite and positive.");
|
|
134
|
-
}
|
|
135
|
-
if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid quote overflow policy.");
|
|
136
|
-
const path = options.path ?? "quote";
|
|
137
|
-
const diagnostics = [], parts = [];
|
|
138
|
-
const report = (reason, diagnosticPath, roles, message) => diagnostics.push({ code: "text-overflow", reason, path: diagnosticPath, parts: roles, message });
|
|
139
|
-
const source = (sourcePath, text, outputStart) => ({ path: sourcePath, start: 0, end: text.length, outputStart, outputEnd: outputStart + text.length });
|
|
140
|
-
const add = (role, text, sources, area, fontSize, fontFamily, fontWeight, partPath) => {
|
|
141
|
-
const requestedStyle = { fontFamily, fontWeight, italic: false, path: partPath };
|
|
142
|
-
const style = resolveTextStyle({ ...requestedStyle }, options.textMeasurement);
|
|
143
|
-
const requestedFontSize = Math.max(fontSize * scale, minimum);
|
|
144
|
-
const part = { role, path: partPath, text, sources, box: area, requestedFontSize, minFontSize: minimum, requestedStyle, style };
|
|
145
|
-
parts.push(part);
|
|
146
|
-
return part;
|
|
147
|
-
};
|
|
148
|
-
let footer = "";
|
|
149
|
-
const footerSources = [];
|
|
150
|
-
for (const field of ["attribution", "source"]) {
|
|
151
|
-
const text = quote[field];
|
|
152
|
-
if (!text) continue;
|
|
153
|
-
if (footer) footer += " - ";
|
|
154
|
-
footerSources.push(source(`${path}.${field}`, text, footer.length));
|
|
155
|
-
footer += text;
|
|
156
|
-
}
|
|
157
|
-
const bodyPath = shorthand ? path : `${path}.text`;
|
|
158
|
-
const body = add(
|
|
159
|
-
"body",
|
|
160
|
-
`"${quote.text}"`,
|
|
161
|
-
[source(bodyPath, quote.text, 1)],
|
|
162
|
-
{ x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - (footer ? 94 : 36) },
|
|
163
|
-
28,
|
|
164
|
-
options.fonts?.heading ?? "sans-serif",
|
|
165
|
-
600,
|
|
166
|
-
bodyPath
|
|
167
|
-
);
|
|
168
|
-
const attribution = footer ? add(
|
|
169
|
-
"footer",
|
|
170
|
-
footer,
|
|
171
|
-
footerSources,
|
|
172
|
-
{ x: box.x + 18, y: box.y + box.height - 58, width: box.width - 36, height: 40 },
|
|
173
|
-
17,
|
|
174
|
-
options.fonts?.body ?? "sans-serif",
|
|
175
|
-
500,
|
|
176
|
-
path
|
|
177
|
-
) : void 0;
|
|
178
|
-
const usable = (area) => [area.x, area.y, area.width, area.height].every(Number.isFinite) && area.width > 0 && area.height > 0;
|
|
179
|
-
const fit = (part, area, size = part.requestedFontSize, floor = minimum) => usable(area) ? fitText(part.text, area, size, floor, textWidthMeasurer(part.style, options.textMeasurement)) : void 0;
|
|
180
|
-
const inner = { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - 36 };
|
|
181
|
-
if (!attribution) body.fit = fit(body, body.box);
|
|
182
|
-
else if (usable(inner) && inner.height > 18) {
|
|
183
|
-
const available = inner.height - 18;
|
|
184
|
-
const preferredBody = fit(body, inner, body.requestedFontSize, body.requestedFontSize);
|
|
185
|
-
const minimumBody = body.requestedFontSize === minimum ? preferredBody : fit(body, inner, minimum, minimum);
|
|
186
|
-
const preferredBodyHeight = preferredBody.lines.length * preferredBody.lineHeight;
|
|
187
|
-
const minimumBodyHeight = minimumBody.lines.length * minimumBody.lineHeight;
|
|
188
|
-
let selected;
|
|
189
|
-
for (const size of /* @__PURE__ */ new Set([attribution.requestedFontSize, minimum])) {
|
|
190
|
-
const natural = fit(attribution, inner, size, size);
|
|
191
|
-
const naturalHeight = natural.lines.length * natural.lineHeight;
|
|
192
|
-
const preferredHeight = Math.max(40, naturalHeight);
|
|
193
|
-
const bodyReservation = Math.min(minimumBodyHeight, Math.max(minimumBody.lineHeight, available - natural.lineHeight));
|
|
194
|
-
const footerHeight = preferredBodyHeight + preferredHeight <= available + 0.01 ? preferredHeight : Math.min(naturalHeight, Math.max(0, available - bodyReservation));
|
|
195
|
-
const bodyBox = { ...inner, height: available - footerHeight };
|
|
196
|
-
const footerBox = { ...inner, y: inner.y + inner.height - footerHeight, height: footerHeight };
|
|
197
|
-
const bodyFit = fit(body, bodyBox);
|
|
198
|
-
const footerFit = usable(footerBox) ? { ...natural, overflow: natural.overflow || naturalHeight > footerHeight + 0.01 } : void 0;
|
|
199
|
-
const overflow = !bodyFit || !footerFit || bodyFit.overflow || footerFit.overflow;
|
|
200
|
-
const score = (body.requestedFontSize - (bodyFit?.fontSize ?? minimum) + attribution.requestedFontSize - size) / scale;
|
|
201
|
-
if (!selected || !overflow && (selected.overflow || score < selected.score) || overflow && selected.overflow) {
|
|
202
|
-
selected = { bodyBox, footerBox, bodyFit, footerFit, score, overflow };
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
if (selected) {
|
|
206
|
-
body.box = selected.bodyBox;
|
|
207
|
-
body.fit = selected.bodyFit;
|
|
208
|
-
attribution.box = selected.footerBox;
|
|
209
|
-
attribution.fit = selected.footerFit;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
for (const part of parts) {
|
|
213
|
-
const area = part.box;
|
|
214
|
-
if (!part.fit) {
|
|
215
|
-
report("invalid-part-box", part.path, [part.role], `Quote ${part.role} has no usable space after its insets; increase the cell size or change the arrangement.`);
|
|
216
|
-
} else if (part.fit.overflow) {
|
|
217
|
-
report("text-fit", part.path, [part.role], `Quote ${part.role} exceeds its available space at the readability floor; increase its space or change the arrangement.`);
|
|
218
|
-
}
|
|
219
|
-
if (usable(area) && (area.x < box.x || area.y < box.y || area.x + area.width > box.x + box.width + 0.01 || area.y + area.height > box.y + box.height + 0.01)) {
|
|
220
|
-
report("part-outside-cell", part.path, [part.role], `Quote ${part.role} extends outside its cell; increase the cell size or change the arrangement.`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
if (body?.fit && attribution?.fit && body.box.y + body.fit.lines.length * body.fit.lineHeight > attribution.box.y + 0.01 && attribution.box.y + attribution.fit.lines.length * attribution.fit.lineHeight > body.box.y + 0.01) {
|
|
224
|
-
report("part-overlap", path, ["body", "footer"], "Quote body and footer line boxes overlap; do not accept this layout without more space.");
|
|
225
|
-
}
|
|
226
|
-
if (diagnostics.length && options.overflow === "error") throw new OPFCompositionError(diagnostics);
|
|
227
|
-
return { algorithm: "quote-flow-v1", textMeasurement: options.textMeasurement ? "provided" : "estimated", parts, diagnostics, overflow: diagnostics.length > 0 };
|
|
228
|
-
}
|
|
229
|
-
function fitRichText(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
|
|
230
|
-
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.");
|
|
231
|
-
const layout = richTextLayouter(input, box, requestedSize, options), minimum = Math.min(requestedSize, minFontSize);
|
|
232
|
-
let size = requestedSize, result = layout(size);
|
|
233
|
-
while (result.overflow && size > minimum) {
|
|
234
|
-
size = Math.max(minimum, size - 1);
|
|
235
|
-
result = layout(size);
|
|
236
|
-
}
|
|
237
|
-
return result;
|
|
238
|
-
}
|
|
239
|
-
function richTextLayouter(input, box, requestedSize, options) {
|
|
240
|
-
let offset = 0;
|
|
241
|
-
const source = input.map((value, runIndex) => {
|
|
242
|
-
const run = typeof value === "string" ? { text: value } : value;
|
|
243
|
-
if (typeof run?.text !== "string" || run.fontSize !== void 0 && (!Number.isFinite(run.fontSize) || run.fontSize <= 0)) throw new RangeError("Rich text runs need text and a positive finite font size.");
|
|
244
|
-
const start = offset;
|
|
245
|
-
offset += run.text.length;
|
|
246
|
-
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);
|
|
247
|
-
return { run, runIndex, start, end: offset, style };
|
|
248
|
-
});
|
|
249
|
-
const whole = source.map((entry) => entry.run.text).join("");
|
|
250
|
-
const layout = (fontSize) => {
|
|
251
|
-
const ratio = fontSize / requestedSize;
|
|
252
|
-
const fragments = (start, end) => {
|
|
253
|
-
let x = 0;
|
|
254
|
-
const result = [];
|
|
255
|
-
for (const entry of source) {
|
|
256
|
-
const a = Math.max(start, entry.start), b = Math.min(end, entry.end);
|
|
257
|
-
if (b <= a) continue;
|
|
258
|
-
const text = whole.slice(a, b), normalSize = (entry.run.fontSize !== void 0 ? entry.run.fontSize * 4 / 3 : requestedSize) * ratio;
|
|
259
|
-
const script = entry.run.superscript || entry.run.subscript;
|
|
260
|
-
const size = normalSize * (script ? 0.7 : 1), baselineShift = entry.run.superscript ? -normalSize * 0.35 : entry.run.subscript ? normalSize * 0.2 : 0;
|
|
261
|
-
const width2 = textWidthMeasurer(entry.style, options.textMeasurement)(text, size);
|
|
262
|
-
result.push({ text, runIndex: entry.runIndex, start: a - entry.start, end: b - entry.start, x, width: width2, fontSize: size, baselineShift, style: entry.style, run: entry.run });
|
|
263
|
-
x += width2;
|
|
264
|
-
}
|
|
265
|
-
return result;
|
|
266
|
-
};
|
|
267
|
-
const width = (start, end) => fragments(start, end).reduce((sum, fragment) => sum + fragment.width, 0);
|
|
268
|
-
const ranges = [];
|
|
269
|
-
let lineStart = 0, lineEnd = 0;
|
|
270
|
-
const push = (end) => {
|
|
271
|
-
ranges.push({ start: lineStart, end });
|
|
272
|
-
lineStart = end;
|
|
273
|
-
lineEnd = end;
|
|
274
|
-
};
|
|
275
|
-
for (const match of whole.matchAll(/\r\n|\r|\n|[^\S\r\n]+|[^\s]+/gu)) {
|
|
276
|
-
const start = match.index, end = start + match[0].length;
|
|
277
|
-
if (/^[\r\n]/.test(match[0])) {
|
|
278
|
-
push(start);
|
|
279
|
-
lineStart = end;
|
|
280
|
-
lineEnd = end;
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
if (width(lineStart, end) <= box.width) {
|
|
284
|
-
lineEnd = end;
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
if (lineEnd > lineStart) push(start);
|
|
288
|
-
if (width(start, end) <= box.width) {
|
|
289
|
-
lineEnd = end;
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
for (const { segment, index } of textSegments.segment(match[0])) {
|
|
293
|
-
const next = start + index + segment.length;
|
|
294
|
-
if (lineEnd > lineStart && width(lineStart, next) > box.width) push(start + index);
|
|
295
|
-
lineEnd = next;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
ranges.push({ start: lineStart, end: lineEnd });
|
|
299
|
-
let y = 0;
|
|
300
|
-
const richLines = ranges.map((range) => {
|
|
301
|
-
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));
|
|
302
|
-
const height = ascent + descent, line = { fragments: parts, width: parts.reduce((sum, p) => sum + p.width, 0), y, baseline: y + ascent, height };
|
|
303
|
-
y += height;
|
|
304
|
-
return line;
|
|
305
|
-
});
|
|
306
|
-
if (options.uniformLineHeight) {
|
|
307
|
-
const height = Math.max(...richLines.map((line) => line.height));
|
|
308
|
-
y = 0;
|
|
309
|
-
for (const line of richLines) {
|
|
310
|
-
line.baseline += y - line.y;
|
|
311
|
-
line.y = y;
|
|
312
|
-
line.height = height;
|
|
313
|
-
y += height;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
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) };
|
|
317
|
-
};
|
|
318
|
-
return layout;
|
|
319
|
-
}
|
|
320
|
-
function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
|
|
321
|
-
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.");
|
|
322
|
-
const source = input.map((value, index) => {
|
|
323
|
-
const object = typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
324
|
-
const text = object ? object.text : value, description = object?.description, level = object?.level ?? 0;
|
|
325
|
-
if (!Number.isInteger(level) || level < 0) throw new RangeError("List levels must be nonnegative integers.");
|
|
326
|
-
const runs = (value2) => typeof value2 === "string" ? [value2] : value2;
|
|
327
|
-
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.");
|
|
328
|
-
const base = options.style.path ? `${options.style.path}.${index}` : void 0;
|
|
329
|
-
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) };
|
|
330
|
-
});
|
|
331
|
-
const layout = (fontSize) => {
|
|
332
|
-
let y = box.y, overflow = false;
|
|
333
|
-
const entries = [], lines = [];
|
|
334
|
-
for (const item of source) {
|
|
335
|
-
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);
|
|
336
|
-
if (offset + indent >= box.width) overflow = true;
|
|
337
|
-
const textBox = { x, y, width, height: box.height };
|
|
338
|
-
const style = { ...options.style, path: item.textPath };
|
|
339
|
-
const text = richTextLayouter(item.runs, textBox, requestedSize, { ...options, style })(fontSize);
|
|
340
|
-
textBox.height = text.height;
|
|
341
|
-
lines.push(...text.lines);
|
|
342
|
-
y += text.height;
|
|
343
|
-
let description, descriptionBox;
|
|
344
|
-
if (item.descriptionRuns !== void 0) {
|
|
345
|
-
y += fontSize * 0.12;
|
|
346
|
-
descriptionBox = { x, y, width, height: box.height };
|
|
347
|
-
description = richTextLayouter(item.descriptionRuns, descriptionBox, requestedSize * 0.82, { ...options, style: { ...options.style, path: item.descriptionPath } })(fontSize * 0.82);
|
|
348
|
-
descriptionBox.height = description.height;
|
|
349
|
-
lines.push(...description.lines);
|
|
350
|
-
y += description.height;
|
|
351
|
-
}
|
|
352
|
-
overflow ||= text.overflow || !!description?.overflow;
|
|
353
|
-
entries.push({
|
|
354
|
-
index: item.index,
|
|
355
|
-
level: item.level,
|
|
356
|
-
value: item.value,
|
|
357
|
-
descriptionValue: item.descriptionValue,
|
|
358
|
-
textPath: item.textPath,
|
|
359
|
-
descriptionPath: item.descriptionPath,
|
|
360
|
-
text,
|
|
361
|
-
textBox,
|
|
362
|
-
description,
|
|
363
|
-
descriptionBox,
|
|
364
|
-
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 }
|
|
365
|
-
});
|
|
366
|
-
if (item.index < source.length - 1) y += fontSize * 0.28;
|
|
367
|
-
}
|
|
368
|
-
const height = y - box.y;
|
|
369
|
-
return { listEntries: entries, lines, fontSize, lineHeight: fontSize * 1.22, height, overflow: overflow || height > box.height + 0.01 };
|
|
370
|
-
};
|
|
371
|
-
const minimum = Math.min(requestedSize, minFontSize);
|
|
372
|
-
let size = requestedSize, result = layout(size);
|
|
373
|
-
while (result.overflow && size > minimum) {
|
|
374
|
-
size = Math.max(minimum, size - 1);
|
|
375
|
-
result = layout(size);
|
|
376
|
-
}
|
|
377
|
-
return result;
|
|
378
|
-
}
|
|
379
|
-
function contentText(field, value) {
|
|
380
|
-
if (field === "text" || headings.has(field)) return flatten(value);
|
|
381
|
-
if (field === "items" || field === "bullets") return (Array.isArray(value) ? value : [value]).map((item) => {
|
|
382
|
-
const entry = record2(item);
|
|
383
|
-
return `\u2022 ${flatten(item)}${entry.description ? `
|
|
384
|
-
${flatten(entry.description)}` : ""}`;
|
|
385
|
-
}).join("\n");
|
|
386
|
-
if (field === "code") return flatten(record2(value).source ?? value);
|
|
387
|
-
if (field === "quote") return flatten(record2(value).text ?? value);
|
|
388
|
-
return void 0;
|
|
389
|
-
}
|
|
390
|
-
function layoutTable(value, box, options = {}) {
|
|
391
|
-
const scale = options.scale ?? 1, requested = 15 * scale, minimum = (options.minFontSize ?? 16) * scale;
|
|
392
|
-
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.");
|
|
393
|
-
const grid = tableGrid(value, options.path ?? "table");
|
|
394
|
-
if (grid.issues.length) throw new RangeError(`${grid.issues[0].path}: ${grid.issues[0].message}`);
|
|
395
|
-
const columnCount = grid.columnCount, cellWidth = box.width / columnCount;
|
|
396
|
-
const cells = grid.rows.flat().map((cell) => {
|
|
397
|
-
const padding = { top: 8, right: 10, bottom: 4, left: 10, ...cell.style.padding };
|
|
398
|
-
const width = cellWidth * cell.colSpan - (padding.left + padding.right) * scale;
|
|
399
|
-
const style = { fontFamily: options.fontFamily ?? "sans-serif", fontWeight: cell.header ? 700 : 400, italic: false, path: cell.valuePath };
|
|
400
|
-
return { ...cell, padding, width, textStyle: style };
|
|
401
|
-
});
|
|
402
|
-
const fitCell = (cell, height, min) => {
|
|
403
|
-
const textBox = { width: Math.max(scale, cell.width), height: Math.max(scale, height) };
|
|
404
|
-
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));
|
|
405
|
-
};
|
|
406
|
-
const textHeight = (fit) => "height" in fit ? fit.height : fit.lines.length * fit.lineHeight;
|
|
407
|
-
const required = (cell, size) => textHeight(fitCell(cell, scale, size)) + (cell.padding.top + cell.padding.bottom) * scale;
|
|
408
|
-
const natural = Array(grid.rowCount).fill(0), needed = Array(grid.rowCount).fill(0);
|
|
409
|
-
for (const cell of cells) if (cell.rowSpan === 1) {
|
|
410
|
-
natural[cell.row] = Math.max(natural[cell.row], required(cell, requested));
|
|
411
|
-
needed[cell.row] = Math.max(needed[cell.row], required(cell, minimum));
|
|
412
|
-
}
|
|
413
|
-
for (const cell of cells) if (cell.rowSpan > 1) for (const [heights2, size] of [[natural, requested], [needed, minimum]]) {
|
|
414
|
-
const current = heights2.slice(cell.row, cell.row + cell.rowSpan).reduce((a, b) => a + b, 0);
|
|
415
|
-
const extra = Math.max(0, required(cell, size) - current) / cell.rowSpan;
|
|
416
|
-
for (let r = cell.row; r < cell.row + cell.rowSpan; r++) heights2[r] += extra;
|
|
417
|
-
}
|
|
418
|
-
const preferred = natural.map((height) => Math.max(54 * scale, height));
|
|
419
|
-
for (let r = 0; r < needed.length; r++) needed[r] = Math.min(preferred[r], needed[r]);
|
|
420
|
-
const sum = (values) => values.reduce((a, b) => a + b, 0);
|
|
421
|
-
const preferredTotal = sum(preferred), naturalTotal = sum(natural), neededTotal = sum(needed);
|
|
422
|
-
const heights = preferred.map((height, r) => preferredTotal <= box.height ? height : naturalTotal <= box.height ? natural[r] + (height - natural[r]) * (box.height - naturalTotal) / Math.max(Number.EPSILON, preferredTotal - naturalTotal) : neededTotal <= box.height ? needed[r] + (natural[r] - needed[r]) * (box.height - neededTotal) / Math.max(Number.EPSILON, naturalTotal - neededTotal) : needed[r] * box.height / neededTotal);
|
|
423
|
-
const ys = [box.y];
|
|
424
|
-
for (const height of heights) ys.push(ys.at(-1) + height);
|
|
425
|
-
let overflow = false;
|
|
426
|
-
const rows2 = heights.map((height, r) => ({ box: { x: box.x, y: ys[r], width: box.width, height }, cells: [] }));
|
|
427
|
-
for (const cell of cells) {
|
|
428
|
-
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] };
|
|
429
|
-
const available = cellBox.height - (cell.padding.top + cell.padding.bottom) * scale;
|
|
430
|
-
const fit = fitCell(cell, available, minimum), height = textHeight(fit);
|
|
431
|
-
const offset = cell.style.verticalAlign === "bottom" ? Math.max(0, available - height) : cell.style.verticalAlign === "middle" ? Math.max(0, (available - height) / 2) : 0;
|
|
432
|
-
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) };
|
|
433
|
-
overflow ||= cell.width <= 0 || available <= 0 || fit.overflow;
|
|
434
|
-
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 });
|
|
435
|
-
}
|
|
436
|
-
return { rows: rows2, columnCount, height: sum(heights), overflow };
|
|
437
|
-
}
|
|
438
|
-
function tableOverflows(value, box, scale, settings, options, path) {
|
|
439
|
-
return box.width <= 0 || box.height <= 0 || layoutTable(value, box, { scale, minFontSize: settings.minFontSize, fontFamily: options.fonts?.body, textMeasurement: options.textMeasurement, path }).overflow;
|
|
440
|
-
}
|
|
441
|
-
function flatten(value) {
|
|
442
|
-
if (value == null) return "";
|
|
443
|
-
if (typeof value !== "object") return String(value);
|
|
444
|
-
if (Array.isArray(value)) return value.map(flatten).join("");
|
|
445
|
-
const item = record2(value);
|
|
446
|
-
return flatten(item.text ?? item.value ?? item.source ?? JSON.stringify(value));
|
|
447
|
-
}
|
|
448
|
-
function regionParts(key) {
|
|
449
|
-
const span = (part, order) => {
|
|
450
|
-
const indices = part.split("+").map((value) => order.indexOf(value));
|
|
451
|
-
return indices.every((index) => index >= 0) ? indices : void 0;
|
|
452
|
-
};
|
|
453
|
-
const parts = key.split(":");
|
|
454
|
-
if (parts.length === 2) {
|
|
455
|
-
const r = span(parts[0], rows), c = span(parts[1], columns);
|
|
456
|
-
if (r && c) return [r, c];
|
|
457
|
-
} else if (parts.length === 1) {
|
|
458
|
-
const r = span(key, rows), c = span(key, columns);
|
|
459
|
-
if (r) return [r, [0, 1, 2]];
|
|
460
|
-
if (c) return [[0, 1, 2], c];
|
|
461
|
-
}
|
|
462
|
-
return void 0;
|
|
463
|
-
}
|
|
464
|
-
function regionBox(parts, area, requestedGap) {
|
|
465
|
-
const gap = Math.min(requestedGap, area.width / 6, area.height / 6);
|
|
466
|
-
const cw = (area.width - gap * 2) / 3, ch = (area.height - gap * 2) / 3;
|
|
467
|
-
const [r, c] = parts;
|
|
468
|
-
return { x: area.x + Math.min(...c) * (cw + gap), y: area.y + Math.min(...r) * (ch + gap), width: (Math.max(...c) - Math.min(...c) + 1) * (cw + gap) - gap, height: (Math.max(...r) - Math.min(...r) + 1) * (ch + gap) - gap };
|
|
469
|
-
}
|
|
470
|
-
function gridGeometry(count, area, cols, gap, weights, vertical) {
|
|
471
|
-
const rowCount = Math.ceil(count / cols);
|
|
472
|
-
gap = Math.min(gap, area.width / (cols * 2), area.height / (rowCount * 2));
|
|
473
|
-
const tracks = (length, total, weighted) => {
|
|
474
|
-
const values = Array.from({ length }, (_, i) => weighted ? weights[i] ?? 1 : 1);
|
|
475
|
-
const sum = values.reduce((a, b) => a + b, 0);
|
|
476
|
-
let cursor = 0;
|
|
477
|
-
return values.map((weight) => {
|
|
478
|
-
const size = (total - gap * (length - 1)) * weight / sum;
|
|
479
|
-
const track = { offset: cursor, size };
|
|
480
|
-
cursor += size + gap;
|
|
481
|
-
return track;
|
|
482
|
-
});
|
|
483
|
-
};
|
|
484
|
-
const xs = tracks(cols, area.width, !vertical), ys = tracks(rowCount, area.height, vertical);
|
|
485
|
-
const boxes = Array.from({ length: count }, (_, i) => ({ x: area.x + xs[i % cols].offset, y: area.y + ys[Math.floor(i / cols)].offset, width: xs[i % cols].size, height: ys[Math.floor(i / cols)].size }));
|
|
486
|
-
return { boxes, columns: xs, rows: ys, gap };
|
|
487
|
-
}
|
|
488
|
-
function gridBoxes(count, area, cols, gap, weights, vertical) {
|
|
489
|
-
return gridGeometry(count, area, cols, gap, weights, vertical).boxes;
|
|
490
|
-
}
|
|
491
|
-
function assertComposition(value) {
|
|
492
|
-
if (value.mode !== void 0 && !["auto", "grid", "row", "column"].includes(value.mode)) throw new RangeError("Unknown composition mode.");
|
|
493
|
-
for (const [key, min, max] of [["gap", 0, 0.1], ["padding", 0, 0.2], ["minFontSize", 8, 32], ["columns", 1, 12]]) {
|
|
494
|
-
const number = value[key];
|
|
495
|
-
if (number !== void 0 && (!Number.isFinite(number) || number < min || number > max || key === "columns" && !Number.isInteger(number))) throw new RangeError(`Invalid composition.${key}.`);
|
|
496
|
-
}
|
|
497
|
-
if (value.weights !== void 0 && (!Array.isArray(value.weights) || !value.weights.length || value.weights.length > 12 || value.weights.some((weight) => !Number.isFinite(weight) || weight <= 0 || weight > 100))) throw new RangeError("Invalid composition.weights.");
|
|
498
|
-
if (value.overflow !== void 0 && !["warn", "error"].includes(value.overflow)) throw new RangeError("Invalid composition.overflow.");
|
|
499
|
-
}
|
|
500
|
-
function composeSlide(input, options = {}) {
|
|
501
|
-
const slide = record2(input), layout = record2(options.layout);
|
|
502
|
-
const width = options.width ?? 1280, height = options.height ?? 720;
|
|
503
|
-
if (![width, height].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
|
|
504
|
-
const scale = Math.min(width, height) / 720;
|
|
505
|
-
const composition = { ...record2(layout.composition), ...record2(slide.composition) };
|
|
506
|
-
assertComposition(composition);
|
|
507
|
-
const padding = (composition.padding ?? 0.08) * Math.min(width, height);
|
|
508
|
-
const gap = (composition.gap ?? 1 / 30) * Math.min(width, height);
|
|
509
|
-
const minSize = (composition.minFontSize ?? 16) * scale;
|
|
510
|
-
const styleFor = (field, path2) => resolveTextStyle({
|
|
511
|
-
fontFamily: (field === "title" ? options.fonts?.heading : field === "code" ? options.fonts?.code : options.fonts?.body) ?? (field === "code" ? "monospace" : "sans-serif"),
|
|
512
|
-
fontWeight: field === "title" ? 700 : 400,
|
|
513
|
-
path: path2
|
|
514
|
-
}, options.textMeasurement);
|
|
515
|
-
const widthFor = (field, path2) => textWidthMeasurer(styleFor(field, path2), options.textMeasurement);
|
|
516
|
-
const fitContent = (field, value, text, box, size, minimum, path2) => field === "text" && Array.isArray(value) ? fitRichText(value, box, size, minimum, { style: styleFor(field, path2), textMeasurement: options.textMeasurement }) : field === "items" || field === "bullets" ? fitList(value, box, size, minimum, { style: styleFor(field, path2), textMeasurement: options.textMeasurement }) : fitText(text, box, size, minimum, widthFor(field, path2));
|
|
517
|
-
const path = `slides.${options.slideIndex ?? 0}`;
|
|
518
|
-
const items = [], diagnostics = [];
|
|
519
|
-
let y = padding;
|
|
520
|
-
for (const field of ["tag", "title", "subtitle"]) {
|
|
521
|
-
if (!slide[field]) continue;
|
|
522
|
-
const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale;
|
|
523
|
-
const maxHeight = height * (field === "title" ? 0.26 : field === "subtitle" ? 0.12 : 0.045);
|
|
524
|
-
const box = { x: padding, y, width: width - padding * 2, height: maxHeight };
|
|
525
|
-
const text = fitText(String(slide[field]), box, requested, minSize, widthFor(field, `${path}.${field}`));
|
|
526
|
-
box.height = Math.min(maxHeight, text.lines.length * text.lineHeight);
|
|
527
|
-
items.push({ path: `${path}.${field}`, field, type: "text", value: slide[field], payload: { text: slide[field] }, box, text, textStyle: styleFor(field, `${path}.${field}`), composition });
|
|
528
|
-
y += box.height + gap * 0.5;
|
|
529
|
-
}
|
|
530
|
-
if (items.length) y += gap * 0.5;
|
|
531
|
-
const contentBox = { x: padding, y, width: width - padding * 2, height: Math.max(scale, height - padding - y) };
|
|
532
|
-
const collect = (host, basePath, depth = 0, ancestors = []) => {
|
|
533
|
-
if (Array.isArray(host.blocks)) {
|
|
534
|
-
if (depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(host)) throw new RangeError(`Content groups must be acyclic and nest at most ${MAX_COMPOSITION_DEPTH} levels.`);
|
|
535
|
-
const settings = record2(host.composition);
|
|
536
|
-
assertComposition(settings);
|
|
537
|
-
return [{
|
|
538
|
-
field: "blocks",
|
|
539
|
-
type: "group",
|
|
540
|
-
value: host.blocks,
|
|
541
|
-
path: basePath,
|
|
542
|
-
payload: host,
|
|
543
|
-
composition: settings,
|
|
544
|
-
children: host.blocks.flatMap((block, index) => collect(record2(block), `${basePath}.blocks.${index}`, depth + 1, [...ancestors, host]))
|
|
545
|
-
}];
|
|
546
|
-
}
|
|
547
|
-
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] } }));
|
|
548
|
-
};
|
|
549
|
-
const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
|
|
550
|
-
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(slide, path);
|
|
551
|
-
const groups = [], flows = [];
|
|
552
|
-
const decisions = options.explain ? [] : void 0;
|
|
553
|
-
const inheritedSettings = (parent, own = {}) => ({
|
|
554
|
-
minFontSize: parent.minFontSize,
|
|
555
|
-
...own,
|
|
556
|
-
overflow: parent.overflow === "error" ? "error" : own.overflow ?? parent.overflow
|
|
557
|
-
});
|
|
558
|
-
const inset = (box, settings) => {
|
|
559
|
-
const amount = (settings.padding ?? 0) * Math.min(box.width, box.height);
|
|
560
|
-
return { x: box.x + amount, y: box.y + amount, width: box.width - amount * 2, height: box.height - amount * 2 };
|
|
561
|
-
};
|
|
562
|
-
const quoteBox = (box) => ({ x: round(box.x), y: round(box.y), width: round(box.width), height: round(box.height) });
|
|
563
|
-
const measureQuote = (node, box, settings) => layoutQuote(node.value, quoteBox(box), {
|
|
564
|
-
fonts: options.fonts,
|
|
565
|
-
textMeasurement: options.textMeasurement,
|
|
566
|
-
scale,
|
|
567
|
-
minFontSize: settings.minFontSize,
|
|
568
|
-
path: node.path
|
|
569
|
-
});
|
|
570
|
-
const leafScore = (node, box, settings, penalties) => {
|
|
571
|
-
const text = contentText(node.field, node.value);
|
|
572
|
-
let score = Math.abs(Math.log(box.width / box.height / 1.6));
|
|
573
|
-
if (penalties) penalties.cellProportions += score;
|
|
574
|
-
if (node.field === "quote") {
|
|
575
|
-
const quote = measureQuote(node, box, settings);
|
|
576
|
-
const reduction = quote.parts.reduce((sum, part) => sum + (part.fit ? (part.requestedFontSize - part.fit.fontSize) / scale : 0), 0);
|
|
577
|
-
score += reduction + (quote.overflow ? 1e3 : 0);
|
|
578
|
-
if (penalties) {
|
|
579
|
-
penalties.fontReduction += reduction;
|
|
580
|
-
penalties.textOverflow += quote.overflow ? 1e3 : 0;
|
|
581
|
-
}
|
|
582
|
-
} else if (text) {
|
|
583
|
-
const fit = fitContent(node.field, node.value, text, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path);
|
|
584
|
-
const reduction = (25 * scale - fit.fontSize) / scale;
|
|
585
|
-
score += reduction + (fit.overflow ? 1e3 : 0);
|
|
586
|
-
if (penalties) {
|
|
587
|
-
penalties.fontReduction += reduction;
|
|
588
|
-
penalties.textOverflow += fit.overflow ? 1e3 : 0;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
if (node.field === "table" && tableOverflows(node.value, box, scale, settings, options, node.path)) {
|
|
592
|
-
score += 1e3;
|
|
593
|
-
if (penalties) penalties.tableOverflow += 1e3;
|
|
594
|
-
}
|
|
595
|
-
if (box.width < 100 * scale || box.height < 60 * scale) {
|
|
596
|
-
score += 100;
|
|
597
|
-
if (penalties) penalties.smallCells += 100;
|
|
598
|
-
}
|
|
599
|
-
return score;
|
|
600
|
-
};
|
|
601
|
-
const modeFor = (settings) => settings.mode ?? "auto";
|
|
602
|
-
const defaultColumns = (count, area, settings) => {
|
|
603
|
-
const mode = modeFor(settings);
|
|
604
|
-
return mode === "column" ? 1 : mode === "row" ? Math.max(1, count) : settings.columns ?? Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count * area.width / area.height / 1.6))));
|
|
605
|
-
};
|
|
606
|
-
const scoreNode = (node, box, settings, penalties) => {
|
|
607
|
-
if (!node.children) return leafScore(node, box, settings, penalties);
|
|
608
|
-
const own = inheritedSettings(settings, node.composition), area = inset(box, own);
|
|
609
|
-
const cols = defaultColumns(node.children.length, area, own);
|
|
610
|
-
const boxes = gridBoxes(node.children.length, area, cols, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), own.weights ?? [], modeFor(own) === "column");
|
|
611
|
-
return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own, penalties), 0);
|
|
612
|
-
};
|
|
613
|
-
const arrange = (nodes, area, settings, reserved = 0, gapOverride, containerPath = path) => {
|
|
614
|
-
const count = Math.max(nodes.length, reserved);
|
|
615
|
-
if (!count) return;
|
|
616
|
-
const mode = modeFor(settings), localGap = (settings.gap ?? 1 / 30) * Math.min(area.width, area.height);
|
|
617
|
-
const actualGap = gapOverride ?? (settings === rootSettings ? gap : localGap);
|
|
618
|
-
let cols = defaultColumns(count, area, settings);
|
|
619
|
-
const hasRegions = nodes.some((node) => node.region);
|
|
620
|
-
const candidates = decisions ? [] : void 0;
|
|
621
|
-
if (mode === "auto" && !hasRegions) {
|
|
622
|
-
let best = Infinity;
|
|
623
|
-
for (let candidate = 1; candidate <= Math.min(count, settings.columns ?? 6); candidate++) {
|
|
624
|
-
const boxes2 = gridBoxes(count, area, candidate, actualGap, settings.weights ?? [], false);
|
|
625
|
-
const emptySlots = (Math.ceil(count / candidate) * candidate - count) * 2;
|
|
626
|
-
const penalties = candidates ? { cellProportions: 0, fontReduction: 0, textOverflow: 0, tableOverflow: 0, smallCells: 0, emptySlots } : void 0;
|
|
627
|
-
const score = nodes.reduce((sum, node, i) => sum + scoreNode(node, boxes2[i], settings, penalties), 0) + emptySlots;
|
|
628
|
-
if (penalties && candidates) candidates.push({ columns: candidate, rows: Math.ceil(count / candidate), score, penalties });
|
|
629
|
-
if (score < best) {
|
|
630
|
-
best = score;
|
|
631
|
-
cols = candidate;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
decisions?.push({
|
|
636
|
-
path: containerPath,
|
|
637
|
-
mode: hasRegions ? "regions" : mode,
|
|
638
|
-
reason: hasRegions ? "promoted-regions" : mode === "auto" ? "lowest-score" : "configured-mode",
|
|
639
|
-
...hasRegions ? {} : { selectedColumns: cols },
|
|
640
|
-
candidates: candidates ?? []
|
|
641
|
-
});
|
|
642
|
-
const grid = gridGeometry(count, area, cols, actualGap, settings.weights ?? [], mode === "column");
|
|
643
|
-
const boxes = grid.boxes;
|
|
644
|
-
if (!nodes.some((node) => node.region)) flows.push({ path: containerPath, box: { ...area }, composition: { ...settings }, columns: grid.columns, rows: grid.rows, gap: grid.gap, itemCount: nodes.length, slotCount: count });
|
|
645
|
-
nodes.forEach((node, index) => {
|
|
646
|
-
const box = node.region ? regionBox(node.region, area, actualGap) : boxes[index];
|
|
647
|
-
if (node.children) {
|
|
648
|
-
const own = inheritedSettings(settings, node.composition), inner = inset(box, own);
|
|
649
|
-
groups.push({ path: node.path, box, contentBox: inner, composition: own });
|
|
650
|
-
arrange(node.children, inner, own, 0, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), node.path);
|
|
651
|
-
} else {
|
|
652
|
-
const textValue = contentText(node.field, node.value);
|
|
653
|
-
const quoteLayout = node.field === "quote" ? measureQuote(node, box, settings) : void 0;
|
|
654
|
-
const text = quoteLayout ? quoteLayout.parts[0]?.fit : textValue !== void 0 ? fitContent(node.field, node.value, textValue, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path) : void 0;
|
|
655
|
-
items.push({
|
|
656
|
-
path: node.path,
|
|
657
|
-
field: node.field,
|
|
658
|
-
type: node.type,
|
|
659
|
-
value: node.value,
|
|
660
|
-
payload: node.payload,
|
|
661
|
-
box: quoteLayout ? quoteBox(box) : box,
|
|
662
|
-
text,
|
|
663
|
-
textStyle: quoteLayout?.parts[0]?.style ?? styleFor(node.field, node.path),
|
|
664
|
-
composition: settings,
|
|
665
|
-
...quoteLayout ? { quoteLayout } : {}
|
|
666
|
-
});
|
|
667
|
-
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." });
|
|
668
|
-
}
|
|
669
|
-
});
|
|
670
|
-
};
|
|
671
|
-
const placeholders = Array.isArray(layout.placeholders) ? layout.placeholders.filter((p) => !headings.has(p.type)) : [];
|
|
672
|
-
const rootSettings = { ...composition, mode: composition.mode ?? (layout.slideLayoutDirection === "Vertical" ? "column" : layout.slideLayoutDirection === "Horizontal" ? "row" : "auto") };
|
|
673
|
-
arrange(pending, contentBox, rootSettings, composition.mode ? 0 : placeholders.length);
|
|
674
|
-
for (const item of items) {
|
|
675
|
-
for (const key of ["x", "y", "width", "height"]) item.box[key] = round(item.box[key]);
|
|
676
|
-
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." });
|
|
677
|
-
if (item.quoteLayout) diagnostics.push(...item.quoteLayout.diagnostics);
|
|
678
|
-
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." });
|
|
679
|
-
}
|
|
680
|
-
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]);
|
|
681
|
-
const strictPaths = new Set(items.filter((item) => item.composition.overflow === "error").map((item) => item.path));
|
|
682
|
-
const failures = diagnostics.filter((diagnostic) => {
|
|
683
|
-
let path2 = diagnostic.path;
|
|
684
|
-
while (path2) {
|
|
685
|
-
if (strictPaths.has(path2)) return true;
|
|
686
|
-
const boundary = path2.lastIndexOf(".");
|
|
687
|
-
if (boundary < 0) break;
|
|
688
|
-
path2 = path2.slice(0, boundary);
|
|
689
|
-
}
|
|
690
|
-
return false;
|
|
691
|
-
});
|
|
692
|
-
const explanation = decisions ? {
|
|
693
|
-
algorithm: "grid-score-v2",
|
|
694
|
-
textMeasurement: options.textMeasurement ? "provided" : "estimated",
|
|
695
|
-
decisions,
|
|
696
|
-
unmeasuredPayloads: items.filter((item) => !headings.has(item.field) && !item.quoteLayout && !["text", "items", "bullets", "table"].includes(item.field)).map((item) => item.path)
|
|
697
|
-
} : void 0;
|
|
698
|
-
if (failures.length) throw new OPFCompositionError(failures, explanation);
|
|
699
|
-
return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...explanation ? { explanation } : {} };
|
|
700
|
-
}
|
|
701
|
-
function resolveCanvasDimensions(input) {
|
|
702
|
-
const presets = {
|
|
703
|
-
widescreen: [40 / 3, 7.5],
|
|
704
|
-
"16:9": [40 / 3, 7.5],
|
|
705
|
-
standard: [10, 7.5],
|
|
706
|
-
"4:3": [10, 7.5],
|
|
707
|
-
"16:10": [10, 6.25],
|
|
708
|
-
letter: [11, 8.5],
|
|
709
|
-
a4: [11.69, 8.27]
|
|
710
|
-
};
|
|
711
|
-
const value = record2(input);
|
|
712
|
-
const preset = presets[typeof input === "string" ? input : value.preset] ?? presets.widescreen;
|
|
713
|
-
const width = (value.widthInches ?? preset[0]) * 96;
|
|
714
|
-
const height = (value.heightInches ?? preset[1]) * 96;
|
|
715
|
-
if (![width, height].every((n) => Number.isFinite(n) && n > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
|
|
716
|
-
return { width, height };
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutQuote, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textWidthMeasurer, wrapText };
|