@openpresentation/opf 0.7.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/{chunk-H7UXKXEJ.js → chunk-HIY26FJV.js} +1 -1
- package/dist/{chunk-OVPWNQIJ.js → chunk-NEQAPMS2.js} +193 -18
- package/dist/{chunk-IQ727X3E.js → chunk-X22DFHMW.js} +18 -13
- package/dist/composition.d.ts +92 -2
- 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,4 +1,4 @@
|
|
|
1
|
-
import { MAX_COMPOSITION_DEPTH, tableGrid } from './chunk-
|
|
1
|
+
import { MAX_COMPOSITION_DEPTH, tableGrid } from './chunk-NEQAPMS2.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';
|
|
@@ -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,110 @@ 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
|
+
}
|
|
123
229
|
function fitRichText(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
|
|
124
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.");
|
|
125
231
|
const layout = richTextLayouter(input, box, requestedSize, options), minimum = Math.min(requestedSize, minFontSize);
|
|
@@ -443,6 +549,7 @@ function composeSlide(input, options = {}) {
|
|
|
443
549
|
const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
|
|
444
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);
|
|
445
551
|
const groups = [], flows = [];
|
|
552
|
+
const decisions = options.explain ? [] : void 0;
|
|
446
553
|
const inheritedSettings = (parent, own = {}) => ({
|
|
447
554
|
minFontSize: parent.minFontSize,
|
|
448
555
|
...own,
|
|
@@ -452,15 +559,43 @@ function composeSlide(input, options = {}) {
|
|
|
452
559
|
const amount = (settings.padding ?? 0) * Math.min(box.width, box.height);
|
|
453
560
|
return { x: box.x + amount, y: box.y + amount, width: box.width - amount * 2, height: box.height - amount * 2 };
|
|
454
561
|
};
|
|
455
|
-
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) => {
|
|
456
571
|
const text = contentText(node.field, node.value);
|
|
457
572
|
let score = Math.abs(Math.log(box.width / box.height / 1.6));
|
|
458
|
-
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) {
|
|
459
583
|
const fit = fitContent(node.field, node.value, text, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path);
|
|
460
|
-
|
|
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;
|
|
461
598
|
}
|
|
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
599
|
return score;
|
|
465
600
|
};
|
|
466
601
|
const modeFor = (settings) => settings.mode ?? "auto";
|
|
@@ -468,12 +603,12 @@ function composeSlide(input, options = {}) {
|
|
|
468
603
|
const mode = modeFor(settings);
|
|
469
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))));
|
|
470
605
|
};
|
|
471
|
-
const scoreNode = (node, box, settings) => {
|
|
472
|
-
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);
|
|
473
608
|
const own = inheritedSettings(settings, node.composition), area = inset(box, own);
|
|
474
609
|
const cols = defaultColumns(node.children.length, area, own);
|
|
475
610
|
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);
|
|
611
|
+
return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own, penalties), 0);
|
|
477
612
|
};
|
|
478
613
|
const arrange = (nodes, area, settings, reserved = 0, gapOverride, containerPath = path) => {
|
|
479
614
|
const count = Math.max(nodes.length, reserved);
|
|
@@ -481,17 +616,29 @@ function composeSlide(input, options = {}) {
|
|
|
481
616
|
const mode = modeFor(settings), localGap = (settings.gap ?? 1 / 30) * Math.min(area.width, area.height);
|
|
482
617
|
const actualGap = gapOverride ?? (settings === rootSettings ? gap : localGap);
|
|
483
618
|
let cols = defaultColumns(count, area, settings);
|
|
484
|
-
|
|
619
|
+
const hasRegions = nodes.some((node) => node.region);
|
|
620
|
+
const candidates = decisions ? [] : void 0;
|
|
621
|
+
if (mode === "auto" && !hasRegions) {
|
|
485
622
|
let best = Infinity;
|
|
486
623
|
for (let candidate = 1; candidate <= Math.min(count, settings.columns ?? 6); candidate++) {
|
|
487
624
|
const boxes2 = gridBoxes(count, area, candidate, actualGap, settings.weights ?? [], false);
|
|
488
|
-
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 });
|
|
489
629
|
if (score < best) {
|
|
490
630
|
best = score;
|
|
491
631
|
cols = candidate;
|
|
492
632
|
}
|
|
493
633
|
}
|
|
494
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
|
+
});
|
|
495
642
|
const grid = gridGeometry(count, area, cols, actualGap, settings.weights ?? [], mode === "column");
|
|
496
643
|
const boxes = grid.boxes;
|
|
497
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 });
|
|
@@ -503,8 +650,20 @@ function composeSlide(input, options = {}) {
|
|
|
503
650
|
arrange(node.children, inner, own, 0, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), node.path);
|
|
504
651
|
} else {
|
|
505
652
|
const textValue = contentText(node.field, node.value);
|
|
506
|
-
const
|
|
507
|
-
|
|
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
|
+
});
|
|
508
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." });
|
|
509
668
|
}
|
|
510
669
|
});
|
|
@@ -515,13 +674,29 @@ function composeSlide(input, options = {}) {
|
|
|
515
674
|
for (const item of items) {
|
|
516
675
|
for (const key of ["x", "y", "width", "height"]) item.box[key] = round(item.box[key]);
|
|
517
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." });
|
|
518
|
-
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." });
|
|
519
679
|
}
|
|
520
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]);
|
|
521
681
|
const strictPaths = new Set(items.filter((item) => item.composition.overflow === "error").map((item) => item.path));
|
|
522
|
-
const failures = diagnostics.filter((diagnostic) =>
|
|
523
|
-
|
|
524
|
-
|
|
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 } : {} };
|
|
525
700
|
}
|
|
526
701
|
function resolveCanvasDimensions(input) {
|
|
527
702
|
const presets = {
|
|
@@ -541,4 +716,4 @@ function resolveCanvasDimensions(input) {
|
|
|
541
716
|
return { width, height };
|
|
542
717
|
}
|
|
543
718
|
|
|
544
|
-
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, 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, tableRowBoundaries } 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
|
|
@@ -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);
|
package/dist/composition.d.ts
CHANGED
|
@@ -97,6 +97,8 @@ interface ComposedItem {
|
|
|
97
97
|
box: LayoutBox;
|
|
98
98
|
text?: TextFit | RichTextFit | ListFit;
|
|
99
99
|
textStyle?: TextStyle;
|
|
100
|
+
/** Complete accepted quote internals; consumers must reuse these fits and styles. */
|
|
101
|
+
quoteLayout?: QuoteLayout;
|
|
100
102
|
/** Effective container settings, including inherited readability constraints. */
|
|
101
103
|
composition: Composition;
|
|
102
104
|
}
|
|
@@ -121,6 +123,37 @@ interface ComposedFlow {
|
|
|
121
123
|
itemCount: number;
|
|
122
124
|
slotCount: number;
|
|
123
125
|
}
|
|
126
|
+
/** Additive penalties in grid-score-v2; lower is preferred. These are not quality percentages. */
|
|
127
|
+
interface CompositionPenalties {
|
|
128
|
+
cellProportions: number;
|
|
129
|
+
fontReduction: number;
|
|
130
|
+
textOverflow: number;
|
|
131
|
+
tableOverflow: number;
|
|
132
|
+
smallCells: number;
|
|
133
|
+
emptySlots: number;
|
|
134
|
+
}
|
|
135
|
+
interface CompositionCandidate {
|
|
136
|
+
columns: number;
|
|
137
|
+
rows: number;
|
|
138
|
+
score: number;
|
|
139
|
+
penalties: CompositionPenalties;
|
|
140
|
+
}
|
|
141
|
+
interface CompositionDecision {
|
|
142
|
+
path: string;
|
|
143
|
+
mode: NonNullable<Composition['mode']> | 'regions';
|
|
144
|
+
reason: 'lowest-score' | 'configured-mode' | 'promoted-regions';
|
|
145
|
+
selectedColumns?: number;
|
|
146
|
+
/** Only candidates actually evaluated by automatic selection, in tie-break order. */
|
|
147
|
+
candidates: CompositionCandidate[];
|
|
148
|
+
}
|
|
149
|
+
interface CompositionExplanation {
|
|
150
|
+
algorithm: 'grid-score-v2';
|
|
151
|
+
/** Provided widths do not establish shaping, glyph coverage or native fidelity. */
|
|
152
|
+
textMeasurement: 'estimated' | 'provided';
|
|
153
|
+
decisions: CompositionDecision[];
|
|
154
|
+
/** Payloads whose complete internal fit is not covered by this scoring model. */
|
|
155
|
+
unmeasuredPayloads: string[];
|
|
156
|
+
}
|
|
124
157
|
interface SlideComposition {
|
|
125
158
|
width: number;
|
|
126
159
|
height: number;
|
|
@@ -130,6 +163,7 @@ interface SlideComposition {
|
|
|
130
163
|
flows: ComposedFlow[];
|
|
131
164
|
diagnostics: LayoutDiagnostic[];
|
|
132
165
|
composition: Composition;
|
|
166
|
+
explanation?: CompositionExplanation;
|
|
133
167
|
}
|
|
134
168
|
interface ComposeSlideOptions {
|
|
135
169
|
fonts?: Partial<FontFamilies>;
|
|
@@ -138,16 +172,72 @@ interface ComposeSlideOptions {
|
|
|
138
172
|
height?: number;
|
|
139
173
|
slideIndex?: number;
|
|
140
174
|
layout?: Record<string, unknown>;
|
|
175
|
+
/** Return candidate scores and coverage without changing the selected geometry. */
|
|
176
|
+
explain?: boolean;
|
|
141
177
|
}
|
|
142
178
|
declare class OPFCompositionError extends Error {
|
|
143
179
|
readonly diagnostics: LayoutDiagnostic[];
|
|
144
180
|
readonly code = "layout-overflow";
|
|
145
|
-
|
|
181
|
+
readonly explanation?: CompositionExplanation;
|
|
182
|
+
constructor(diagnostics: LayoutDiagnostic[], explanation?: CompositionExplanation);
|
|
146
183
|
}
|
|
147
184
|
/** Deterministic estimate, not a font shaping engine. Preserves explicit line breaks. */
|
|
148
185
|
declare function measureText(text: string, fontSize: number): number;
|
|
149
186
|
declare function wrapText(text: string, width: number, fontSize: number, measure?: MeasureTextWidth): string[];
|
|
150
187
|
declare function fitText(text: string, box: LayoutBox, requestedSize?: number, minFontSize?: number, measure?: MeasureTextWidth): TextFit;
|
|
188
|
+
interface QuoteContent {
|
|
189
|
+
text: string;
|
|
190
|
+
attribution?: string;
|
|
191
|
+
source?: string;
|
|
192
|
+
}
|
|
193
|
+
/** Source and displayed-text ranges are half-open UTF-16 offsets. Added punctuation has no source range. */
|
|
194
|
+
interface QuoteTextSource {
|
|
195
|
+
path: string;
|
|
196
|
+
start: number;
|
|
197
|
+
end: number;
|
|
198
|
+
outputStart: number;
|
|
199
|
+
outputEnd: number;
|
|
200
|
+
}
|
|
201
|
+
interface QuoteTextPart {
|
|
202
|
+
role: 'body' | 'footer';
|
|
203
|
+
path: string;
|
|
204
|
+
text: string;
|
|
205
|
+
sources: QuoteTextSource[];
|
|
206
|
+
/** Available space, before fitting. Invalid dimensions remain visible in failed results. */
|
|
207
|
+
box: LayoutBox;
|
|
208
|
+
requestedFontSize: number;
|
|
209
|
+
minFontSize: number;
|
|
210
|
+
requestedStyle: TextStyle;
|
|
211
|
+
style: TextStyle;
|
|
212
|
+
/** Absent when the available box is invalid; never fit against an invented one-pixel box. */
|
|
213
|
+
fit?: TextFit;
|
|
214
|
+
}
|
|
215
|
+
interface QuoteLayoutDiagnostic extends LayoutDiagnostic {
|
|
216
|
+
reason: 'invalid-part-box' | 'part-outside-cell' | 'text-fit' | 'part-overlap';
|
|
217
|
+
parts: QuoteTextPart['role'][];
|
|
218
|
+
}
|
|
219
|
+
interface QuoteLayout {
|
|
220
|
+
algorithm: 'quote-flow-v1';
|
|
221
|
+
textMeasurement: 'estimated' | 'provided';
|
|
222
|
+
parts: QuoteTextPart[];
|
|
223
|
+
diagnostics: QuoteLayoutDiagnostic[];
|
|
224
|
+
overflow: boolean;
|
|
225
|
+
}
|
|
226
|
+
interface QuoteLayoutOptions {
|
|
227
|
+
/** Canvas short edge divided by 720. Insets retain the current 18 reference-pixel contract. */
|
|
228
|
+
scale?: number;
|
|
229
|
+
fonts?: Partial<FontFamilies>;
|
|
230
|
+
/** Readability floor in reference pixels, before canvas scaling. */
|
|
231
|
+
minFontSize?: number;
|
|
232
|
+
overflow?: Composition['overflow'];
|
|
233
|
+
path?: string;
|
|
234
|
+
textMeasurement?: TextMeasurement;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Allocate and measure quote body/footer space for composition, rendering and export. Callers must
|
|
238
|
+
* check overflow before accepting the parts. Line boxes are not shaped glyph or native raster bounds.
|
|
239
|
+
*/
|
|
240
|
+
declare function layoutQuote(value: string | QuoteContent, box: LayoutBox, options?: QuoteLayoutOptions): QuoteLayout;
|
|
151
241
|
interface RichTextRun {
|
|
152
242
|
text: string;
|
|
153
243
|
bold?: boolean;
|
|
@@ -272,4 +362,4 @@ declare function resolveCanvasDimensions(input: unknown): {
|
|
|
272
362
|
height: number;
|
|
273
363
|
};
|
|
274
364
|
|
|
275
|
-
export { type ComposeSlideOptions, type ComposedFlow, type ComposedGroup, type ComposedItem, type Composition, type CompositionTrack, type FontFamilies, type LayoutBox, type LayoutDiagnostic, type ListEntryLayout, type ListFit, type ListText, type ListValue, MAX_COMPOSITION_DEPTH, type MeasureTextWidth, OPFCompositionError, type RichTextFit, type RichTextFragment, type RichTextLine, type RichTextOptions, type RichTextRun, type SlideComposition, type TableBorder, type TableCellLayout, type TableCellStyle, type TableGrid, type TableGridCell, type TableGridIssue, type TableLayout, type TableLayoutOptions, type TableRowLayout, type TextFit, type TextMeasurement, type TextStyle, composeSlide, fitList, fitRichText, fitText, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textWidthMeasurer, wrapText };
|
|
365
|
+
export { type ComposeSlideOptions, type ComposedFlow, type ComposedGroup, type ComposedItem, type Composition, type CompositionCandidate, type CompositionDecision, type CompositionExplanation, type CompositionPenalties, type CompositionTrack, type FontFamilies, type LayoutBox, type LayoutDiagnostic, type ListEntryLayout, type ListFit, type ListText, type ListValue, MAX_COMPOSITION_DEPTH, type MeasureTextWidth, OPFCompositionError, type QuoteContent, type QuoteLayout, type QuoteLayoutDiagnostic, type QuoteLayoutOptions, type QuoteTextPart, type QuoteTextSource, type RichTextFit, type RichTextFragment, type RichTextLine, type RichTextOptions, type RichTextRun, type SlideComposition, type TableBorder, type TableCellLayout, type TableCellStyle, type TableGrid, type TableGridCell, type TableGridIssue, type TableLayout, type TableLayoutOptions, type TableRowLayout, type TextFit, type TextMeasurement, type TextStyle, composeSlide, fitList, fitRichText, fitText, layoutQuote, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textWidthMeasurer, wrapText };
|
package/dist/composition.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textWidthMeasurer, wrapText } from './chunk-
|
|
1
|
+
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutQuote, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textWidthMeasurer, wrapText } from './chunk-NEQAPMS2.js';
|