@openpresentation/opf 0.7.0 → 0.9.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/{chunk-IQ727X3E.js → chunk-3GPZMY2W.js} +18 -13
- package/dist/{chunk-OVPWNQIJ.js → chunk-JA5W5TVA.js} +352 -18
- package/dist/{chunk-H7UXKXEJ.js → chunk-ZGV6I23P.js} +1 -1
- package/dist/composition.d.ts +157 -3
- package/dist/composition.js +1 -1
- package/dist/docs.js +40 -4
- package/dist/examples.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/pagination.js +3 -3
- package/dist/{presentation-B19O_Uzz.d.ts → presentation-BMTdX6O1.d.ts} +4 -4
- package/dist/repo-readme.js +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/validator.d.ts +1 -1
- package/dist/validator.js +2 -2
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { assertValidPresentation } from './chunk-
|
|
2
|
-
import { resolveFontFamilies, resolveCanvasDimensions, composeSlide, tableRowBoundaries } from './chunk-
|
|
1
|
+
import { assertValidPresentation } from './chunk-ZGV6I23P.js';
|
|
2
|
+
import { resolveFontFamilies, resolveCanvasDimensions, composeSlide, tableRowBoundaries } from './chunk-JA5W5TVA.js';
|
|
3
3
|
import { catalogs } from './chunk-TWMRZ43O.js';
|
|
4
4
|
|
|
5
5
|
// src/pagination.ts
|
|
@@ -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);
|
|
@@ -58,13 +58,15 @@ function textWidthMeasurer(style, measurement) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
var OPFCompositionError = class extends Error {
|
|
61
|
-
constructor(diagnostics) {
|
|
61
|
+
constructor(diagnostics, explanation) {
|
|
62
62
|
super("Slide content does not fit its composition.");
|
|
63
63
|
this.diagnostics = diagnostics;
|
|
64
64
|
this.name = "OPFCompositionError";
|
|
65
|
+
if (explanation) this.explanation = explanation;
|
|
65
66
|
}
|
|
66
67
|
diagnostics;
|
|
67
68
|
code = "layout-overflow";
|
|
69
|
+
explanation;
|
|
68
70
|
};
|
|
69
71
|
var fields = ["text", "items", "bullets", "image", "video", "chart", "table", "code", "metric", "quote", "timeline"];
|
|
70
72
|
var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
|
|
@@ -120,6 +122,258 @@ function fitText(text, box, requestedSize = 25, minFontSize = 16, measure = meas
|
|
|
120
122
|
}
|
|
121
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) };
|
|
122
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 fitCodeText(text, box, size, minimum, step, measure) {
|
|
230
|
+
const layout = (fontSize) => {
|
|
231
|
+
const tabWidth = measure(" ", fontSize) * 4;
|
|
232
|
+
if (!Number.isFinite(tabWidth) || text.includes(" ") && tabWidth <= 0) throw new RangeError("Code tabs require a positive finite measured space advance.");
|
|
233
|
+
const measureLine = (start2, end2, record3 = false) => {
|
|
234
|
+
let x = 0, offset = start2;
|
|
235
|
+
const segments = [];
|
|
236
|
+
for (const [index, segment] of text.slice(start2, end2).split(" ").entries()) {
|
|
237
|
+
if (index) {
|
|
238
|
+
const next = (Math.floor(x / tabWidth + 1e-9) + 1) * tabWidth;
|
|
239
|
+
if (record3) segments.push({ kind: "tab", start: offset, end: offset + 1, x, width: next - x });
|
|
240
|
+
x = next;
|
|
241
|
+
offset++;
|
|
242
|
+
}
|
|
243
|
+
if (segment) {
|
|
244
|
+
const advance = measure(segment, fontSize);
|
|
245
|
+
if (record3) segments.push({ kind: "text", start: offset, end: offset + segment.length, x, width: advance });
|
|
246
|
+
x += advance;
|
|
247
|
+
offset += segment.length;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { width: x, segments };
|
|
251
|
+
};
|
|
252
|
+
const width = (start2, end2) => measureLine(start2, end2).width;
|
|
253
|
+
const sourceLines = [];
|
|
254
|
+
let start = 0, end = 0;
|
|
255
|
+
const push = (last, nextStart, boundary) => {
|
|
256
|
+
sourceLines.push({ start, end: last, nextStart, boundary, ...measureLine(start, last, true) });
|
|
257
|
+
start = nextStart;
|
|
258
|
+
end = nextStart;
|
|
259
|
+
};
|
|
260
|
+
for (const token of text.matchAll(/\r\n|\r|\n|[^\S\r\n]+|[^\s]+/gu)) {
|
|
261
|
+
if (token.index === void 0) throw new Error("Missing code token source offset.");
|
|
262
|
+
const a = token.index, b = a + token[0].length;
|
|
263
|
+
if (/^[\r\n]/.test(token[0])) {
|
|
264
|
+
push(a, b, "hard");
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (width(start, b) <= box.width + 0.01) {
|
|
268
|
+
end = b;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (end > start && /\S/u.test(text.slice(start, end))) push(a, a, "soft");
|
|
272
|
+
if (width(start, b) <= box.width + 0.01) {
|
|
273
|
+
end = b;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
for (const segment of textSegments.segment(token[0])) {
|
|
277
|
+
const next = a + segment.index + segment.segment.length;
|
|
278
|
+
if (end > start && width(start, next) > box.width + 0.01) push(a + segment.index, a + segment.index, "soft");
|
|
279
|
+
end = next;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
push(end, end, "end");
|
|
283
|
+
const lineHeight = fontSize * 1.22;
|
|
284
|
+
return {
|
|
285
|
+
lines: sourceLines.map((line) => text.slice(line.start, line.end)),
|
|
286
|
+
sourceLines,
|
|
287
|
+
fontSize,
|
|
288
|
+
lineHeight,
|
|
289
|
+
tabSize: 4,
|
|
290
|
+
tabWidth,
|
|
291
|
+
overflow: sourceLines.length * lineHeight > box.height + 0.01 || sourceLines.some((line) => line.width > box.width + 0.01)
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
let result = layout(size);
|
|
295
|
+
while (result.overflow && size > minimum) {
|
|
296
|
+
size = Math.max(minimum, size - step);
|
|
297
|
+
result = layout(size);
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
function layoutCode(value, box, options = {}) {
|
|
302
|
+
const shorthand = typeof value === "string", code = shorthand ? { source: value } : value;
|
|
303
|
+
if (!code || Array.isArray(code) || typeof code.source !== "string" || [code.language, code.filename].some((field) => field !== void 0 && typeof field !== "string")) {
|
|
304
|
+
throw new TypeError("Code content must be a string or source object with optional string language/filename.");
|
|
305
|
+
}
|
|
306
|
+
const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
|
|
307
|
+
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) {
|
|
308
|
+
throw new RangeError("Code dimensions, scale and minimum font size must be finite and positive.");
|
|
309
|
+
}
|
|
310
|
+
if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid code overflow policy.");
|
|
311
|
+
const path = options.path ?? "code", inner = { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - 36 };
|
|
312
|
+
const parts = [], diagnostics = [];
|
|
313
|
+
const add = (role, text, partPath, generated = false) => {
|
|
314
|
+
const requestedStyle = { fontFamily: options.fonts?.code ?? "monospace", fontWeight: role === "body" ? 400 : 700, italic: false, path: partPath };
|
|
315
|
+
const part = {
|
|
316
|
+
role,
|
|
317
|
+
path: partPath,
|
|
318
|
+
text,
|
|
319
|
+
sources: generated ? [] : [{ path: partPath, start: 0, end: text.length }],
|
|
320
|
+
generated,
|
|
321
|
+
box: { ...inner },
|
|
322
|
+
requestedFontSize: Math.max((role === "body" ? 18 : 14) * scale, minimum),
|
|
323
|
+
minFontSize: minimum,
|
|
324
|
+
requestedStyle,
|
|
325
|
+
style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement)
|
|
326
|
+
};
|
|
327
|
+
parts.push(part);
|
|
328
|
+
return part;
|
|
329
|
+
};
|
|
330
|
+
if (code.filename) add("filename", code.filename, `${path}.filename`);
|
|
331
|
+
if (code.language) add("language", code.language, `${path}.language`);
|
|
332
|
+
if (!parts.length) add("language", "code", path, true);
|
|
333
|
+
const body = add("body", code.source, shorthand ? path : `${path}.source`), headers = parts.filter((part) => part !== body);
|
|
334
|
+
const usable = (area) => [area.x, area.y, area.width, area.height].every(Number.isFinite) && area.width > 0 && area.height > 0;
|
|
335
|
+
const fit = (part, area, size = part.requestedFontSize, floor = minimum) => usable(area) ? fitCodeText(part.text, area, size, floor, scale, textWidthMeasurer(part.style, options.textMeasurement)) : void 0;
|
|
336
|
+
if (usable(inner)) {
|
|
337
|
+
const combinations = headers.reduce((choices, part) => choices.flatMap((choice) => [.../* @__PURE__ */ new Set([part.requestedFontSize, minimum])].map((size) => [...choice, { part, size }])), [[]]);
|
|
338
|
+
let selected;
|
|
339
|
+
for (const combination of combinations) {
|
|
340
|
+
let y = inner.y;
|
|
341
|
+
const allocations = [];
|
|
342
|
+
for (const [index, { part, size }] of combination.entries()) {
|
|
343
|
+
const measured = fitCodeText(part.text, inner, size, size, scale, textWidthMeasurer(part.style, options.textMeasurement));
|
|
344
|
+
const area2 = { ...inner, y, height: measured.lines.length * measured.lineHeight };
|
|
345
|
+
allocations.push({ part, box: area2, fit: usable(area2) ? { ...measured, overflow: measured.sourceLines.some((line) => line.width > area2.width + 0.01) } : void 0 });
|
|
346
|
+
y += area2.height + (index < headers.length - 1 ? 8 : 12);
|
|
347
|
+
}
|
|
348
|
+
const area = { ...inner, y, height: inner.y + inner.height - y };
|
|
349
|
+
allocations.push({ part: body, box: area, fit: fit(body, area) });
|
|
350
|
+
const overflow = allocations.some(({ box: box2, fit: fit2 }) => !fit2 || fit2.overflow || !usable(box2) || box2.y + box2.height > inner.y + inner.height + 0.01);
|
|
351
|
+
const score = allocations.reduce((sum, { part, fit: fit2 }) => sum + (part.requestedFontSize - (fit2?.fontSize ?? minimum)) / scale, 0);
|
|
352
|
+
if (!selected || !overflow && (selected.overflow || score < selected.score) || overflow && selected.overflow) selected = { allocations, score, overflow };
|
|
353
|
+
}
|
|
354
|
+
if (selected) for (const { part, box: box2, fit: fit2 } of selected.allocations) {
|
|
355
|
+
part.box = box2;
|
|
356
|
+
part.fit = fit2;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const report = (reason, part, roles, message) => diagnostics.push({ code: "text-overflow", reason, path: part.path, parts: roles, message });
|
|
360
|
+
for (const part of parts) {
|
|
361
|
+
if (!part.fit) report("invalid-part-box", part, [part.role], `Code ${part.role} has no usable space after metadata and insets; increase the cell or change the arrangement.`);
|
|
362
|
+
else if (part.fit.overflow) report("text-fit", part, [part.role], `Code ${part.role} exceeds its space at the readability floor; increase its space or change the arrangement.`);
|
|
363
|
+
const area = part.box;
|
|
364
|
+
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)) {
|
|
365
|
+
report("part-outside-cell", part, [part.role], `Code ${part.role} extends outside its cell; increase the cell or change the arrangement.`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const [index, first] of parts.entries()) {
|
|
369
|
+
const second = parts[index + 1];
|
|
370
|
+
if (first.fit && second?.fit && first.box.y + first.fit.lines.length * first.fit.lineHeight > second.box.y + 0.01) {
|
|
371
|
+
report("part-overlap", first, [first.role, second.role], "Code part line boxes overlap; do not accept this layout without more space.");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (diagnostics.length && options.overflow === "error") throw new OPFCompositionError(diagnostics);
|
|
375
|
+
return { algorithm: "code-flow-v1", textMeasurement: options.textMeasurement ? "provided" : "estimated", parts, diagnostics, overflow: diagnostics.length > 0 };
|
|
376
|
+
}
|
|
123
377
|
function fitRichText(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
|
|
124
378
|
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.");
|
|
125
379
|
const layout = richTextLayouter(input, box, requestedSize, options), minimum = Math.min(requestedSize, minFontSize);
|
|
@@ -443,6 +697,7 @@ function composeSlide(input, options = {}) {
|
|
|
443
697
|
const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
|
|
444
698
|
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);
|
|
445
699
|
const groups = [], flows = [];
|
|
700
|
+
const decisions = options.explain ? [] : void 0;
|
|
446
701
|
const inheritedSettings = (parent, own = {}) => ({
|
|
447
702
|
minFontSize: parent.minFontSize,
|
|
448
703
|
...own,
|
|
@@ -452,15 +707,50 @@ function composeSlide(input, options = {}) {
|
|
|
452
707
|
const amount = (settings.padding ?? 0) * Math.min(box.width, box.height);
|
|
453
708
|
return { x: box.x + amount, y: box.y + amount, width: box.width - amount * 2, height: box.height - amount * 2 };
|
|
454
709
|
};
|
|
455
|
-
const
|
|
710
|
+
const acceptedBox = (box) => ({ x: round(box.x), y: round(box.y), width: round(box.width), height: round(box.height) });
|
|
711
|
+
const measureQuote = (node, box, settings) => layoutQuote(node.value, acceptedBox(box), {
|
|
712
|
+
fonts: options.fonts,
|
|
713
|
+
textMeasurement: options.textMeasurement,
|
|
714
|
+
scale,
|
|
715
|
+
minFontSize: settings.minFontSize,
|
|
716
|
+
path: node.path
|
|
717
|
+
});
|
|
718
|
+
const measureCode = (node, box, settings) => layoutCode(node.value, acceptedBox(box), {
|
|
719
|
+
fonts: options.fonts,
|
|
720
|
+
textMeasurement: options.textMeasurement,
|
|
721
|
+
scale,
|
|
722
|
+
minFontSize: settings.minFontSize,
|
|
723
|
+
path: node.path
|
|
724
|
+
});
|
|
725
|
+
const leafScore = (node, box, settings, penalties) => {
|
|
456
726
|
const text = contentText(node.field, node.value);
|
|
457
727
|
let score = Math.abs(Math.log(box.width / box.height / 1.6));
|
|
458
|
-
if (
|
|
728
|
+
if (penalties) penalties.cellProportions += score;
|
|
729
|
+
if (node.field === "quote" || node.field === "code") {
|
|
730
|
+
const internal = node.field === "quote" ? measureQuote(node, box, settings) : measureCode(node, box, settings);
|
|
731
|
+
const reduction = internal.parts.reduce((sum, part) => sum + (part.fit ? (part.requestedFontSize - part.fit.fontSize) / scale : 0), 0);
|
|
732
|
+
score += reduction + (internal.overflow ? 1e3 : 0);
|
|
733
|
+
if (penalties) {
|
|
734
|
+
penalties.fontReduction += reduction;
|
|
735
|
+
penalties.textOverflow += internal.overflow ? 1e3 : 0;
|
|
736
|
+
}
|
|
737
|
+
} else if (text) {
|
|
459
738
|
const fit = fitContent(node.field, node.value, text, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path);
|
|
460
|
-
|
|
739
|
+
const reduction = (25 * scale - fit.fontSize) / scale;
|
|
740
|
+
score += reduction + (fit.overflow ? 1e3 : 0);
|
|
741
|
+
if (penalties) {
|
|
742
|
+
penalties.fontReduction += reduction;
|
|
743
|
+
penalties.textOverflow += fit.overflow ? 1e3 : 0;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (node.field === "table" && tableOverflows(node.value, box, scale, settings, options, node.path)) {
|
|
747
|
+
score += 1e3;
|
|
748
|
+
if (penalties) penalties.tableOverflow += 1e3;
|
|
749
|
+
}
|
|
750
|
+
if (box.width < 100 * scale || box.height < 60 * scale) {
|
|
751
|
+
score += 100;
|
|
752
|
+
if (penalties) penalties.smallCells += 100;
|
|
461
753
|
}
|
|
462
|
-
if (node.field === "table" && tableOverflows(node.value, box, scale, settings, options, node.path)) score += 1e3;
|
|
463
|
-
if (box.width < 100 * scale || box.height < 60 * scale) score += 100;
|
|
464
754
|
return score;
|
|
465
755
|
};
|
|
466
756
|
const modeFor = (settings) => settings.mode ?? "auto";
|
|
@@ -468,12 +758,12 @@ function composeSlide(input, options = {}) {
|
|
|
468
758
|
const mode = modeFor(settings);
|
|
469
759
|
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))));
|
|
470
760
|
};
|
|
471
|
-
const scoreNode = (node, box, settings) => {
|
|
472
|
-
if (!node.children) return leafScore(node, box, settings);
|
|
761
|
+
const scoreNode = (node, box, settings, penalties) => {
|
|
762
|
+
if (!node.children) return leafScore(node, box, settings, penalties);
|
|
473
763
|
const own = inheritedSettings(settings, node.composition), area = inset(box, own);
|
|
474
764
|
const cols = defaultColumns(node.children.length, area, own);
|
|
475
765
|
const boxes = gridBoxes(node.children.length, area, cols, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), own.weights ?? [], modeFor(own) === "column");
|
|
476
|
-
return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own), 0);
|
|
766
|
+
return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own, penalties), 0);
|
|
477
767
|
};
|
|
478
768
|
const arrange = (nodes, area, settings, reserved = 0, gapOverride, containerPath = path) => {
|
|
479
769
|
const count = Math.max(nodes.length, reserved);
|
|
@@ -481,17 +771,29 @@ function composeSlide(input, options = {}) {
|
|
|
481
771
|
const mode = modeFor(settings), localGap = (settings.gap ?? 1 / 30) * Math.min(area.width, area.height);
|
|
482
772
|
const actualGap = gapOverride ?? (settings === rootSettings ? gap : localGap);
|
|
483
773
|
let cols = defaultColumns(count, area, settings);
|
|
484
|
-
|
|
774
|
+
const hasRegions = nodes.some((node) => node.region);
|
|
775
|
+
const candidates = decisions ? [] : void 0;
|
|
776
|
+
if (mode === "auto" && !hasRegions) {
|
|
485
777
|
let best = Infinity;
|
|
486
778
|
for (let candidate = 1; candidate <= Math.min(count, settings.columns ?? 6); candidate++) {
|
|
487
779
|
const boxes2 = gridBoxes(count, area, candidate, actualGap, settings.weights ?? [], false);
|
|
488
|
-
const
|
|
780
|
+
const emptySlots = (Math.ceil(count / candidate) * candidate - count) * 2;
|
|
781
|
+
const penalties = candidates ? { cellProportions: 0, fontReduction: 0, textOverflow: 0, tableOverflow: 0, smallCells: 0, emptySlots } : void 0;
|
|
782
|
+
const score = nodes.reduce((sum, node, i) => sum + scoreNode(node, boxes2[i], settings, penalties), 0) + emptySlots;
|
|
783
|
+
if (penalties && candidates) candidates.push({ columns: candidate, rows: Math.ceil(count / candidate), score, penalties });
|
|
489
784
|
if (score < best) {
|
|
490
785
|
best = score;
|
|
491
786
|
cols = candidate;
|
|
492
787
|
}
|
|
493
788
|
}
|
|
494
789
|
}
|
|
790
|
+
decisions?.push({
|
|
791
|
+
path: containerPath,
|
|
792
|
+
mode: hasRegions ? "regions" : mode,
|
|
793
|
+
reason: hasRegions ? "promoted-regions" : mode === "auto" ? "lowest-score" : "configured-mode",
|
|
794
|
+
...hasRegions ? {} : { selectedColumns: cols },
|
|
795
|
+
candidates: candidates ?? []
|
|
796
|
+
});
|
|
495
797
|
const grid = gridGeometry(count, area, cols, actualGap, settings.weights ?? [], mode === "column");
|
|
496
798
|
const boxes = grid.boxes;
|
|
497
799
|
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 });
|
|
@@ -503,8 +805,23 @@ function composeSlide(input, options = {}) {
|
|
|
503
805
|
arrange(node.children, inner, own, 0, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), node.path);
|
|
504
806
|
} else {
|
|
505
807
|
const textValue = contentText(node.field, node.value);
|
|
506
|
-
const
|
|
507
|
-
|
|
808
|
+
const quoteLayout = node.field === "quote" ? measureQuote(node, box, settings) : void 0;
|
|
809
|
+
const codeLayout = node.field === "code" ? measureCode(node, box, settings) : void 0;
|
|
810
|
+
const internal = quoteLayout ?? codeLayout, body = internal?.parts.find((part) => part.role === "body");
|
|
811
|
+
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;
|
|
812
|
+
items.push({
|
|
813
|
+
path: node.path,
|
|
814
|
+
field: node.field,
|
|
815
|
+
type: node.type,
|
|
816
|
+
value: node.value,
|
|
817
|
+
payload: node.payload,
|
|
818
|
+
box: internal ? acceptedBox(box) : box,
|
|
819
|
+
text,
|
|
820
|
+
textStyle: body?.style ?? styleFor(node.field, node.path),
|
|
821
|
+
composition: settings,
|
|
822
|
+
...quoteLayout ? { quoteLayout } : {},
|
|
823
|
+
...codeLayout ? { codeLayout } : {}
|
|
824
|
+
});
|
|
508
825
|
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." });
|
|
509
826
|
}
|
|
510
827
|
});
|
|
@@ -515,13 +832,30 @@ function composeSlide(input, options = {}) {
|
|
|
515
832
|
for (const item of items) {
|
|
516
833
|
for (const key of ["x", "y", "width", "height"]) item.box[key] = round(item.box[key]);
|
|
517
834
|
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." });
|
|
518
|
-
if (item.
|
|
835
|
+
if (item.quoteLayout) diagnostics.push(...item.quoteLayout.diagnostics);
|
|
836
|
+
else if (item.codeLayout) diagnostics.push(...item.codeLayout.diagnostics);
|
|
837
|
+
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." });
|
|
519
838
|
}
|
|
520
839
|
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]);
|
|
521
840
|
const strictPaths = new Set(items.filter((item) => item.composition.overflow === "error").map((item) => item.path));
|
|
522
|
-
const failures = diagnostics.filter((diagnostic) =>
|
|
523
|
-
|
|
524
|
-
|
|
841
|
+
const failures = diagnostics.filter((diagnostic) => {
|
|
842
|
+
let path2 = diagnostic.path;
|
|
843
|
+
while (path2) {
|
|
844
|
+
if (strictPaths.has(path2)) return true;
|
|
845
|
+
const boundary = path2.lastIndexOf(".");
|
|
846
|
+
if (boundary < 0) break;
|
|
847
|
+
path2 = path2.slice(0, boundary);
|
|
848
|
+
}
|
|
849
|
+
return false;
|
|
850
|
+
});
|
|
851
|
+
const explanation = decisions ? {
|
|
852
|
+
algorithm: "grid-score-v3",
|
|
853
|
+
textMeasurement: options.textMeasurement ? "provided" : "estimated",
|
|
854
|
+
decisions,
|
|
855
|
+
unmeasuredPayloads: items.filter((item) => !headings.has(item.field) && !item.quoteLayout && !item.codeLayout && !["text", "items", "bullets", "table"].includes(item.field)).map((item) => item.path)
|
|
856
|
+
} : void 0;
|
|
857
|
+
if (failures.length) throw new OPFCompositionError(failures, explanation);
|
|
858
|
+
return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...explanation ? { explanation } : {} };
|
|
525
859
|
}
|
|
526
860
|
function resolveCanvasDimensions(input) {
|
|
527
861
|
const presets = {
|
|
@@ -541,4 +875,4 @@ function resolveCanvasDimensions(input) {
|
|
|
541
875
|
return { width, height };
|
|
542
876
|
}
|
|
543
877
|
|
|
544
|
-
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textWidthMeasurer, wrapText };
|
|
878
|
+
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutCode, layoutQuote, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textWidthMeasurer, wrapText };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_COMPOSITION_DEPTH, tableGrid } from './chunk-
|
|
1
|
+
import { MAX_COMPOSITION_DEPTH, tableGrid } from './chunk-JA5W5TVA.js';
|
|
2
2
|
import { schemas } from './chunk-GQSK3T7X.js';
|
|
3
3
|
import { catalogSchemaNames } from './chunk-TWMRZ43O.js';
|
|
4
4
|
import Ajv2020 from 'ajv/dist/2020.js';
|