@openpresentation/opf 0.6.0 → 0.8.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/dist/{catalogs-x1oFFQnd.d.ts → catalogs-BnnkUX9O.d.ts} +1 -1
- package/dist/catalogs.d.ts +2 -2
- package/dist/{chunk-KSN6RQG2.js → chunk-GQSK3T7X.js} +161 -2
- package/dist/{chunk-23MXJL2K.js → chunk-HIY26FJV.js} +10 -2
- package/dist/{chunk-C7GKRLI6.js → chunk-NEQAPMS2.js} +284 -64
- package/dist/{chunk-C6U7LLBT.js → chunk-X22DFHMW.js} +19 -14
- package/dist/composition.d.ts +142 -2
- package/dist/composition.js +1 -1
- package/dist/docs.js +42 -6
- package/dist/examples.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/pagination.js +4 -4
- package/dist/{presentation-UpSHz0aK.d.ts → presentation-BMTdX6O1.d.ts} +104 -10
- package/dist/repo-readme.js +1 -1
- package/dist/{schemas-C1DxapX2.d.ts → schemas--QRVBhU1.d.ts} +10 -0
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +1 -1
- package/dist/spec/schemas/opf.schema.json +222 -12
- package/dist/types.d.ts +3 -3
- package/dist/validator.d.ts +3 -3
- package/dist/validator.js +3 -3
- package/package.json +2 -2
|
@@ -1,8 +1,45 @@
|
|
|
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
|
+
|
|
1
38
|
// src/composition.ts
|
|
2
39
|
var MAX_COMPOSITION_DEPTH = 32;
|
|
3
40
|
function resolveFontFamilies(input) {
|
|
4
|
-
const scheme =
|
|
5
|
-
const family = (value) => typeof value === "string" ? value :
|
|
41
|
+
const scheme = record2(input);
|
|
42
|
+
const family = (value) => typeof value === "string" ? value : record2(value).family;
|
|
6
43
|
return {
|
|
7
44
|
heading: family(scheme.heading) ?? scheme.major ?? scheme.minor ?? "Roboto",
|
|
8
45
|
body: family(scheme.body) ?? scheme.minor ?? scheme.major ?? "Roboto",
|
|
@@ -21,19 +58,21 @@ function textWidthMeasurer(style, measurement) {
|
|
|
21
58
|
};
|
|
22
59
|
}
|
|
23
60
|
var OPFCompositionError = class extends Error {
|
|
24
|
-
constructor(diagnostics) {
|
|
61
|
+
constructor(diagnostics, explanation) {
|
|
25
62
|
super("Slide content does not fit its composition.");
|
|
26
63
|
this.diagnostics = diagnostics;
|
|
27
64
|
this.name = "OPFCompositionError";
|
|
65
|
+
if (explanation) this.explanation = explanation;
|
|
28
66
|
}
|
|
29
67
|
diagnostics;
|
|
30
68
|
code = "layout-overflow";
|
|
69
|
+
explanation;
|
|
31
70
|
};
|
|
32
71
|
var fields = ["text", "items", "bullets", "image", "video", "chart", "table", "code", "metric", "quote", "timeline"];
|
|
33
72
|
var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
|
|
34
73
|
var rows = ["top", "middle", "bottom"];
|
|
35
74
|
var columns = ["left", "center", "right"];
|
|
36
|
-
var
|
|
75
|
+
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
37
76
|
var kind = (field) => field === "items" || field === "bullets" ? "list" : field;
|
|
38
77
|
var round = (value) => Math.round(value * 1e6) / 1e6 || value;
|
|
39
78
|
var textSegments = new Intl.Segmenter("und", { granularity: "grapheme" });
|
|
@@ -83,6 +122,110 @@ function fitText(text, box, requestedSize = 25, minFontSize = 16, measure = meas
|
|
|
83
122
|
}
|
|
84
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) };
|
|
85
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
|
+
}
|
|
86
229
|
function fitRichText(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
|
|
87
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.");
|
|
88
231
|
const layout = richTextLayouter(input, box, requestedSize, options), minimum = Math.min(requestedSize, minFontSize);
|
|
@@ -236,53 +379,61 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
|
|
|
236
379
|
function contentText(field, value) {
|
|
237
380
|
if (field === "text" || headings.has(field)) return flatten(value);
|
|
238
381
|
if (field === "items" || field === "bullets") return (Array.isArray(value) ? value : [value]).map((item) => {
|
|
239
|
-
const entry =
|
|
382
|
+
const entry = record2(item);
|
|
240
383
|
return `\u2022 ${flatten(item)}${entry.description ? `
|
|
241
384
|
${flatten(entry.description)}` : ""}`;
|
|
242
385
|
}).join("\n");
|
|
243
|
-
if (field === "code") return flatten(
|
|
244
|
-
if (field === "quote") return flatten(
|
|
386
|
+
if (field === "code") return flatten(record2(value).source ?? value);
|
|
387
|
+
if (field === "quote") return flatten(record2(value).text ?? value);
|
|
245
388
|
return void 0;
|
|
246
389
|
}
|
|
247
390
|
function layoutTable(value, box, options = {}) {
|
|
248
391
|
const scale = options.scale ?? 1, requested = 15 * scale, minimum = (options.minFontSize ?? 16) * scale;
|
|
249
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.");
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
const columnCount =
|
|
253
|
-
const
|
|
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
|
+
});
|
|
254
402
|
const fitCell = (cell, height, min) => {
|
|
255
|
-
const textBox = { width, height: Math.max(scale, height) };
|
|
256
|
-
return Array.isArray(cell.value) ? fitRichText(cell.value, textBox, requested, min, { style: cell.
|
|
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));
|
|
257
405
|
};
|
|
258
406
|
const textHeight = (fit) => "height" in fit ? fit.height : fit.lines.length * fit.lineHeight;
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
const preferred =
|
|
271
|
-
let
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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 };
|
|
286
437
|
}
|
|
287
438
|
function tableOverflows(value, box, scale, settings, options, path) {
|
|
288
439
|
return box.width <= 0 || box.height <= 0 || layoutTable(value, box, { scale, minFontSize: settings.minFontSize, fontFamily: options.fonts?.body, textMeasurement: options.textMeasurement, path }).overflow;
|
|
@@ -291,7 +442,7 @@ function flatten(value) {
|
|
|
291
442
|
if (value == null) return "";
|
|
292
443
|
if (typeof value !== "object") return String(value);
|
|
293
444
|
if (Array.isArray(value)) return value.map(flatten).join("");
|
|
294
|
-
const item =
|
|
445
|
+
const item = record2(value);
|
|
295
446
|
return flatten(item.text ?? item.value ?? item.source ?? JSON.stringify(value));
|
|
296
447
|
}
|
|
297
448
|
function regionParts(key) {
|
|
@@ -347,11 +498,11 @@ function assertComposition(value) {
|
|
|
347
498
|
if (value.overflow !== void 0 && !["warn", "error"].includes(value.overflow)) throw new RangeError("Invalid composition.overflow.");
|
|
348
499
|
}
|
|
349
500
|
function composeSlide(input, options = {}) {
|
|
350
|
-
const slide =
|
|
501
|
+
const slide = record2(input), layout = record2(options.layout);
|
|
351
502
|
const width = options.width ?? 1280, height = options.height ?? 720;
|
|
352
503
|
if (![width, height].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
|
|
353
504
|
const scale = Math.min(width, height) / 720;
|
|
354
|
-
const composition = { ...
|
|
505
|
+
const composition = { ...record2(layout.composition), ...record2(slide.composition) };
|
|
355
506
|
assertComposition(composition);
|
|
356
507
|
const padding = (composition.padding ?? 0.08) * Math.min(width, height);
|
|
357
508
|
const gap = (composition.gap ?? 1 / 30) * Math.min(width, height);
|
|
@@ -381,7 +532,7 @@ function composeSlide(input, options = {}) {
|
|
|
381
532
|
const collect = (host, basePath, depth = 0, ancestors = []) => {
|
|
382
533
|
if (Array.isArray(host.blocks)) {
|
|
383
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.`);
|
|
384
|
-
const settings =
|
|
535
|
+
const settings = record2(host.composition);
|
|
385
536
|
assertComposition(settings);
|
|
386
537
|
return [{
|
|
387
538
|
field: "blocks",
|
|
@@ -390,14 +541,15 @@ function composeSlide(input, options = {}) {
|
|
|
390
541
|
path: basePath,
|
|
391
542
|
payload: host,
|
|
392
543
|
composition: settings,
|
|
393
|
-
children: host.blocks.flatMap((block, index) => collect(
|
|
544
|
+
children: host.blocks.flatMap((block, index) => collect(record2(block), `${basePath}.blocks.${index}`, depth + 1, [...ancestors, host]))
|
|
394
545
|
}];
|
|
395
546
|
}
|
|
396
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] } }));
|
|
397
548
|
};
|
|
398
549
|
const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
|
|
399
|
-
const pending = regions.length ? regions.flatMap((key) => collect(
|
|
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);
|
|
400
551
|
const groups = [], flows = [];
|
|
552
|
+
const decisions = options.explain ? [] : void 0;
|
|
401
553
|
const inheritedSettings = (parent, own = {}) => ({
|
|
402
554
|
minFontSize: parent.minFontSize,
|
|
403
555
|
...own,
|
|
@@ -407,15 +559,43 @@ function composeSlide(input, options = {}) {
|
|
|
407
559
|
const amount = (settings.padding ?? 0) * Math.min(box.width, box.height);
|
|
408
560
|
return { x: box.x + amount, y: box.y + amount, width: box.width - amount * 2, height: box.height - amount * 2 };
|
|
409
561
|
};
|
|
410
|
-
const
|
|
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) => {
|
|
411
571
|
const text = contentText(node.field, node.value);
|
|
412
572
|
let score = Math.abs(Math.log(box.width / box.height / 1.6));
|
|
413
|
-
if (
|
|
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) {
|
|
414
583
|
const fit = fitContent(node.field, node.value, text, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path);
|
|
415
|
-
|
|
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;
|
|
416
598
|
}
|
|
417
|
-
if (node.field === "table" && tableOverflows(node.value, box, scale, settings, options, node.path)) score += 1e3;
|
|
418
|
-
if (box.width < 100 * scale || box.height < 60 * scale) score += 100;
|
|
419
599
|
return score;
|
|
420
600
|
};
|
|
421
601
|
const modeFor = (settings) => settings.mode ?? "auto";
|
|
@@ -423,12 +603,12 @@ function composeSlide(input, options = {}) {
|
|
|
423
603
|
const mode = modeFor(settings);
|
|
424
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))));
|
|
425
605
|
};
|
|
426
|
-
const scoreNode = (node, box, settings) => {
|
|
427
|
-
if (!node.children) return leafScore(node, box, settings);
|
|
606
|
+
const scoreNode = (node, box, settings, penalties) => {
|
|
607
|
+
if (!node.children) return leafScore(node, box, settings, penalties);
|
|
428
608
|
const own = inheritedSettings(settings, node.composition), area = inset(box, own);
|
|
429
609
|
const cols = defaultColumns(node.children.length, area, own);
|
|
430
610
|
const boxes = gridBoxes(node.children.length, area, cols, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), own.weights ?? [], modeFor(own) === "column");
|
|
431
|
-
return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own), 0);
|
|
611
|
+
return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own, penalties), 0);
|
|
432
612
|
};
|
|
433
613
|
const arrange = (nodes, area, settings, reserved = 0, gapOverride, containerPath = path) => {
|
|
434
614
|
const count = Math.max(nodes.length, reserved);
|
|
@@ -436,17 +616,29 @@ function composeSlide(input, options = {}) {
|
|
|
436
616
|
const mode = modeFor(settings), localGap = (settings.gap ?? 1 / 30) * Math.min(area.width, area.height);
|
|
437
617
|
const actualGap = gapOverride ?? (settings === rootSettings ? gap : localGap);
|
|
438
618
|
let cols = defaultColumns(count, area, settings);
|
|
439
|
-
|
|
619
|
+
const hasRegions = nodes.some((node) => node.region);
|
|
620
|
+
const candidates = decisions ? [] : void 0;
|
|
621
|
+
if (mode === "auto" && !hasRegions) {
|
|
440
622
|
let best = Infinity;
|
|
441
623
|
for (let candidate = 1; candidate <= Math.min(count, settings.columns ?? 6); candidate++) {
|
|
442
624
|
const boxes2 = gridBoxes(count, area, candidate, actualGap, settings.weights ?? [], false);
|
|
443
|
-
const
|
|
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 });
|
|
444
629
|
if (score < best) {
|
|
445
630
|
best = score;
|
|
446
631
|
cols = candidate;
|
|
447
632
|
}
|
|
448
633
|
}
|
|
449
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
|
+
});
|
|
450
642
|
const grid = gridGeometry(count, area, cols, actualGap, settings.weights ?? [], mode === "column");
|
|
451
643
|
const boxes = grid.boxes;
|
|
452
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 });
|
|
@@ -458,8 +650,20 @@ function composeSlide(input, options = {}) {
|
|
|
458
650
|
arrange(node.children, inner, own, 0, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), node.path);
|
|
459
651
|
} else {
|
|
460
652
|
const textValue = contentText(node.field, node.value);
|
|
461
|
-
const
|
|
462
|
-
|
|
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
|
+
});
|
|
463
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." });
|
|
464
668
|
}
|
|
465
669
|
});
|
|
@@ -470,13 +674,29 @@ function composeSlide(input, options = {}) {
|
|
|
470
674
|
for (const item of items) {
|
|
471
675
|
for (const key of ["x", "y", "width", "height"]) item.box[key] = round(item.box[key]);
|
|
472
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." });
|
|
473
|
-
if (item.
|
|
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." });
|
|
474
679
|
}
|
|
475
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]);
|
|
476
681
|
const strictPaths = new Set(items.filter((item) => item.composition.overflow === "error").map((item) => item.path));
|
|
477
|
-
const failures = diagnostics.filter((diagnostic) =>
|
|
478
|
-
|
|
479
|
-
|
|
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 } : {} };
|
|
480
700
|
}
|
|
481
701
|
function resolveCanvasDimensions(input) {
|
|
482
702
|
const presets = {
|
|
@@ -488,7 +708,7 @@ function resolveCanvasDimensions(input) {
|
|
|
488
708
|
letter: [11, 8.5],
|
|
489
709
|
a4: [11.69, 8.27]
|
|
490
710
|
};
|
|
491
|
-
const value =
|
|
711
|
+
const value = record2(input);
|
|
492
712
|
const preset = presets[typeof input === "string" ? input : value.preset] ?? presets.widescreen;
|
|
493
713
|
const width = (value.widthInches ?? preset[0]) * 96;
|
|
494
714
|
const height = (value.heightInches ?? preset[1]) * 96;
|
|
@@ -496,4 +716,4 @@ function resolveCanvasDimensions(input) {
|
|
|
496
716
|
return { width, height };
|
|
497
717
|
}
|
|
498
718
|
|
|
499
|
-
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText };
|
|
719
|
+
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutQuote, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textWidthMeasurer, wrapText };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { assertValidPresentation } from './chunk-
|
|
2
|
-
import { resolveFontFamilies, resolveCanvasDimensions, composeSlide } from './chunk-
|
|
1
|
+
import { assertValidPresentation } from './chunk-HIY26FJV.js';
|
|
2
|
+
import { resolveFontFamilies, resolveCanvasDimensions, composeSlide, tableRowBoundaries } from './chunk-NEQAPMS2.js';
|
|
3
3
|
import { catalogs } from './chunk-TWMRZ43O.js';
|
|
4
4
|
|
|
5
5
|
// src/pagination.ts
|
|
@@ -52,7 +52,7 @@ function leafFor(path, field, value) {
|
|
|
52
52
|
const values = Array.isArray(value) ? value : value[key];
|
|
53
53
|
if (Array.isArray(values)) {
|
|
54
54
|
leaf.unit = "items";
|
|
55
|
-
leaf.boundaries = Array.from({ length: values.length + 1 }, (_, i) => i);
|
|
55
|
+
leaf.boundaries = field === "table" ? tableRowBoundaries(value) : Array.from({ length: values.length + 1 }, (_, i) => i);
|
|
56
56
|
leaf.slice = (a, b) => Array.isArray(value) ? value.slice(a, b) : { ...value, [key]: values.slice(a, b) };
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -60,27 +60,32 @@ function leafFor(path, field, value) {
|
|
|
60
60
|
}
|
|
61
61
|
function paginateSlide(input, options = {}) {
|
|
62
62
|
assertValidPresentation({ slides: [input] });
|
|
63
|
-
|
|
63
|
+
let source = clone(input);
|
|
64
64
|
const maxSlides = options.maxSlides ?? 100;
|
|
65
65
|
if (!Number.isInteger(maxSlides) || maxSlides < 1 || maxSlides > 1e4) throw new RangeError("maxSlides must be an integer between 1 and 10000.");
|
|
66
66
|
const minFontSize = options.minFontSize ?? 24;
|
|
67
67
|
if (!Number.isFinite(minFontSize) || minFontSize < 8 || minFontSize > 32) throw new RangeError("Pagination minFontSize must be between 8 and 32.");
|
|
68
68
|
const sourceIndex = options.slideIndex ?? 0, sourceBase = `slides.${sourceIndex}`;
|
|
69
|
-
const
|
|
69
|
+
const withReadability = (slide, warn = false) => {
|
|
70
70
|
const result = clone(slide);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const rootMinimum = Math.max(minFontSize, result.composition?.minFontSize ?? options.layout?.composition?.minFontSize ?? 16);
|
|
72
|
+
result.composition = { ...result.composition, minFontSize: rootMinimum };
|
|
73
|
+
const visit = (node, inheritedMinimum) => {
|
|
74
|
+
const minimum = Math.max(minFontSize, node.composition?.minFontSize ?? inheritedMinimum);
|
|
75
|
+
if (node.composition) node.composition = { ...node.composition, minFontSize: minimum, ...warn ? { overflow: "warn" } : {} };
|
|
76
|
+
if (Array.isArray(node.blocks)) node.blocks.forEach((child) => {
|
|
77
|
+
visit(child, minimum);
|
|
78
|
+
});
|
|
74
79
|
};
|
|
75
|
-
visit(result);
|
|
76
|
-
for (const [key, value] of Object.entries(result)) if (isRecord(value) && /^(top|middle|bottom|left|center|right)([+:]|$)/.test(key)) visit(value);
|
|
77
|
-
result.composition = { ...result.composition, overflow: "warn", minFontSize: Math.max(minFontSize, result.composition?.minFontSize ?? options.layout?.composition?.minFontSize ?? 16) };
|
|
80
|
+
visit(result, rootMinimum);
|
|
81
|
+
for (const [key, value] of Object.entries(result)) if (isRecord(value) && /^(top|middle|bottom|left|center|right)([+:]|$)/.test(key)) visit(value, rootMinimum);
|
|
78
82
|
return result;
|
|
79
83
|
};
|
|
84
|
+
source = withReadability(source);
|
|
80
85
|
let evaluations = 0;
|
|
81
86
|
const geometry = (slide) => {
|
|
82
87
|
if (++evaluations > 2e4) throw new OPFPaginationError("Pagination exceeded its layout evaluation limit. Split the input into smaller sections.");
|
|
83
|
-
return composeSlide(
|
|
88
|
+
return composeSlide(withReadability(slide, true), options);
|
|
84
89
|
};
|
|
85
90
|
const initial = geometry(source);
|
|
86
91
|
if (!initial.diagnostics.length) return { slides: [source], pages: [{ slideIndex: sourceIndex, mappings: initial.items.map((item) => ({ sourcePath: item.path, outputPath: item.path })) }] };
|
|
@@ -150,7 +155,7 @@ function paginateSlide(input, options = {}) {
|
|
|
150
155
|
const boundaries = leaf.boundaries ?? [0, 1];
|
|
151
156
|
const end = boundaries.at(-1);
|
|
152
157
|
let start = 0;
|
|
153
|
-
|
|
158
|
+
while (true) {
|
|
154
159
|
const make = (limit) => ({ leaf, start, end: limit, value: leaf.slice ? leaf.slice(start, limit) : leaf.value });
|
|
155
160
|
let candidate = new Map(selected).set(leaf.path, make(end));
|
|
156
161
|
let issues = diagnosticsFor(candidate);
|
|
@@ -187,7 +192,7 @@ function paginateSlide(input, options = {}) {
|
|
|
187
192
|
start = best;
|
|
188
193
|
} else if (selected.size) finish();
|
|
189
194
|
else throw new OPFPaginationError(`Content at ${leaf.path} cannot fit on an otherwise empty slide. Change its layout or split an atomic item.`, issues);
|
|
190
|
-
}
|
|
195
|
+
}
|
|
191
196
|
}
|
|
192
197
|
finish();
|
|
193
198
|
if (!slides.length) throw new OPFPaginationError("No body content can be paginated.", initial.diagnostics);
|