@openpresentation/opf 0.5.0 → 0.7.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-PFJE2IW4.js → chunk-H7UXKXEJ.js} +10 -2
- package/dist/{chunk-YLORKB63.js → chunk-IQ727X3E.js} +3 -3
- package/dist/{chunk-WG5A4KZG.js → chunk-OVPWNQIJ.js} +113 -27
- package/dist/composition.d.ts +86 -1
- package/dist/composition.js +1 -1
- package/dist/docs.js +3 -3
- package/dist/examples.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -4
- package/dist/pagination.js +4 -4
- package/dist/{presentation-UpSHz0aK.d.ts → presentation-B19O_Uzz.d.ts} +100 -6
- 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 +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as SchemaName } from './schemas
|
|
1
|
+
import { a as SchemaName } from './schemas--QRVBhU1.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Schema for audience records in the pptx.gallery library. Each record names an audience archetype (e.g. 'executives', 'engineering-team', 'investors') and carries seniority, technical-fluency, decision-power, and attention-budget hints used by AI-driven generation. Audiences are referenced from OPF documents via audience; the engine resolves the reference against catalogs.audiences (inline) → catalogs.audiences.source → the default catalog at https://www.pptx.gallery/audiences. The audience field also accepts free-form strings; this catalog is for engine-aware audience archetypes that carry generation hints.
|
package/dist/catalogs.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { C as CatalogEntry, a as CatalogIndex, b as CatalogIndexRecord, c as CatalogKind, d as CatalogRecord, i as audiences, j as catalogEntries, k as catalogIndexes, l as catalogKinds, m as catalogSchemaNames, n as catalogs, o as chartTypes, p as colorSchemes, q as fontSchemes, r as languages, s as layouts, t as narratives, u as purposes, v as socialPlatforms, w as themes, x as tones } from './catalogs-
|
|
2
|
-
import './schemas
|
|
1
|
+
export { C as CatalogEntry, a as CatalogIndex, b as CatalogIndexRecord, c as CatalogKind, d as CatalogRecord, i as audiences, j as catalogEntries, k as catalogIndexes, l as catalogKinds, m as catalogSchemaNames, n as catalogs, o as chartTypes, p as colorSchemes, q as fontSchemes, r as languages, s as layouts, t as narratives, u as purposes, v as socialPlatforms, w as themes, x as tones } from './catalogs-BnnkUX9O.js';
|
|
2
|
+
import './schemas--QRVBhU1.js';
|
|
@@ -3688,10 +3688,16 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
|
|
|
3688
3688
|
"items": {
|
|
3689
3689
|
"$ref": "#/$defs/TextRun"
|
|
3690
3690
|
}
|
|
3691
|
+
},
|
|
3692
|
+
{
|
|
3693
|
+
"$ref": "#/$defs/StyledTableCell"
|
|
3694
|
+
},
|
|
3695
|
+
{
|
|
3696
|
+
"type": "null"
|
|
3691
3697
|
}
|
|
3692
3698
|
]
|
|
3693
3699
|
},
|
|
3694
|
-
"description": "Optional column labels
|
|
3700
|
+
"description": "Optional column labels. Labels may be strings, rich runs or styled cell objects. Null is an empty label or a placeholder covered by a preceding column span.",
|
|
3695
3701
|
"examples": [
|
|
3696
3702
|
[
|
|
3697
3703
|
"Region",
|
|
@@ -3862,6 +3868,17 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
|
|
|
3862
3868
|
"description": "A cell in inline chart data."
|
|
3863
3869
|
},
|
|
3864
3870
|
"TableCell": {
|
|
3871
|
+
"oneOf": [
|
|
3872
|
+
{
|
|
3873
|
+
"$ref": "#/$defs/TableCellValue"
|
|
3874
|
+
},
|
|
3875
|
+
{
|
|
3876
|
+
"$ref": "#/$defs/StyledTableCell"
|
|
3877
|
+
}
|
|
3878
|
+
],
|
|
3879
|
+
"description": "A scalar, rich-run array, or styled/spanning cell object. Existing scalar and rich forms remain valid."
|
|
3880
|
+
},
|
|
3881
|
+
"TableCellValue": {
|
|
3865
3882
|
"oneOf": [
|
|
3866
3883
|
{
|
|
3867
3884
|
"type": "string"
|
|
@@ -3882,7 +3899,149 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
|
|
|
3882
3899
|
}
|
|
3883
3900
|
}
|
|
3884
3901
|
],
|
|
3885
|
-
"description": "A
|
|
3902
|
+
"description": "A scalar table value or canonical rich text runs, without cell decoration or geometry."
|
|
3903
|
+
},
|
|
3904
|
+
"StyledTableCell": {
|
|
3905
|
+
"type": "object",
|
|
3906
|
+
"additionalProperties": false,
|
|
3907
|
+
"required": [
|
|
3908
|
+
"value"
|
|
3909
|
+
],
|
|
3910
|
+
"properties": {
|
|
3911
|
+
"value": {
|
|
3912
|
+
"$ref": "#/$defs/TableCellValue",
|
|
3913
|
+
"description": "Editable cell content; styling and spans do not change its scalar type or rich runs."
|
|
3914
|
+
},
|
|
3915
|
+
"style": {
|
|
3916
|
+
"$ref": "#/$defs/TableCellStyle"
|
|
3917
|
+
},
|
|
3918
|
+
"colSpan": {
|
|
3919
|
+
"type": "integer",
|
|
3920
|
+
"minimum": 1,
|
|
3921
|
+
"description": "Number of grid columns covered, starting at this cell. Covered positions must contain null. Default 1."
|
|
3922
|
+
},
|
|
3923
|
+
"rowSpan": {
|
|
3924
|
+
"type": "integer",
|
|
3925
|
+
"minimum": 1,
|
|
3926
|
+
"description": "Number of grid rows covered, starting at this cell. Covered positions must contain null. Header cells cannot span into body rows. Default 1."
|
|
3927
|
+
}
|
|
3928
|
+
},
|
|
3929
|
+
"description": "A cell with explicit visual style or merged geometry. Its position remains its array column index; use null placeholders for every covered grid position."
|
|
3930
|
+
},
|
|
3931
|
+
"TableCellStyle": {
|
|
3932
|
+
"type": "object",
|
|
3933
|
+
"additionalProperties": false,
|
|
3934
|
+
"properties": {
|
|
3935
|
+
"fill": {
|
|
3936
|
+
"type": "string",
|
|
3937
|
+
"pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$",
|
|
3938
|
+
"description": "Explicit RGB or RGBA color. Eight-digit colors include alpha; #00000000 is transparent."
|
|
3939
|
+
},
|
|
3940
|
+
"color": {
|
|
3941
|
+
"type": "string",
|
|
3942
|
+
"pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$",
|
|
3943
|
+
"description": "Default text color, overridden by individual rich run colors."
|
|
3944
|
+
},
|
|
3945
|
+
"align": {
|
|
3946
|
+
"type": "string",
|
|
3947
|
+
"enum": [
|
|
3948
|
+
"left",
|
|
3949
|
+
"center",
|
|
3950
|
+
"right"
|
|
3951
|
+
],
|
|
3952
|
+
"description": "Horizontal text alignment inside the cell."
|
|
3953
|
+
},
|
|
3954
|
+
"verticalAlign": {
|
|
3955
|
+
"type": "string",
|
|
3956
|
+
"enum": [
|
|
3957
|
+
"top",
|
|
3958
|
+
"middle",
|
|
3959
|
+
"bottom"
|
|
3960
|
+
],
|
|
3961
|
+
"description": "Vertical alignment inside the padded cell box."
|
|
3962
|
+
},
|
|
3963
|
+
"padding": {
|
|
3964
|
+
"$ref": "#/$defs/TableCellPadding"
|
|
3965
|
+
},
|
|
3966
|
+
"borders": {
|
|
3967
|
+
"type": "object",
|
|
3968
|
+
"additionalProperties": false,
|
|
3969
|
+
"properties": {
|
|
3970
|
+
"top": {
|
|
3971
|
+
"$ref": "#/$defs/TableCellBorder"
|
|
3972
|
+
},
|
|
3973
|
+
"right": {
|
|
3974
|
+
"$ref": "#/$defs/TableCellBorder"
|
|
3975
|
+
},
|
|
3976
|
+
"bottom": {
|
|
3977
|
+
"$ref": "#/$defs/TableCellBorder"
|
|
3978
|
+
},
|
|
3979
|
+
"left": {
|
|
3980
|
+
"$ref": "#/$defs/TableCellBorder"
|
|
3981
|
+
}
|
|
3982
|
+
},
|
|
3983
|
+
"description": "Independent cell edges. Omitted edges retain the table theme border; width 0 removes an edge."
|
|
3984
|
+
}
|
|
3985
|
+
},
|
|
3986
|
+
"description": "Cell appearance. Sizes use reference pixels at a 720-pixel canvas short edge and scale with the slide."
|
|
3987
|
+
},
|
|
3988
|
+
"TableCellPadding": {
|
|
3989
|
+
"type": "object",
|
|
3990
|
+
"additionalProperties": false,
|
|
3991
|
+
"properties": {
|
|
3992
|
+
"top": {
|
|
3993
|
+
"type": "number",
|
|
3994
|
+
"minimum": 0,
|
|
3995
|
+
"maximum": 200
|
|
3996
|
+
},
|
|
3997
|
+
"right": {
|
|
3998
|
+
"type": "number",
|
|
3999
|
+
"minimum": 0,
|
|
4000
|
+
"maximum": 200
|
|
4001
|
+
},
|
|
4002
|
+
"bottom": {
|
|
4003
|
+
"type": "number",
|
|
4004
|
+
"minimum": 0,
|
|
4005
|
+
"maximum": 200
|
|
4006
|
+
},
|
|
4007
|
+
"left": {
|
|
4008
|
+
"type": "number",
|
|
4009
|
+
"minimum": 0,
|
|
4010
|
+
"maximum": 200
|
|
4011
|
+
}
|
|
4012
|
+
},
|
|
4013
|
+
"description": "Text insets in reference pixels. Defaults: top 8, right 10, bottom 4, left 10."
|
|
4014
|
+
},
|
|
4015
|
+
"TableCellBorder": {
|
|
4016
|
+
"type": "object",
|
|
4017
|
+
"additionalProperties": false,
|
|
4018
|
+
"required": [
|
|
4019
|
+
"color",
|
|
4020
|
+
"width"
|
|
4021
|
+
],
|
|
4022
|
+
"properties": {
|
|
4023
|
+
"color": {
|
|
4024
|
+
"type": "string",
|
|
4025
|
+
"pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$",
|
|
4026
|
+
"description": "Explicit RGB or RGBA color. Eight-digit colors include alpha; #00000000 is transparent."
|
|
4027
|
+
},
|
|
4028
|
+
"width": {
|
|
4029
|
+
"type": "number",
|
|
4030
|
+
"minimum": 0,
|
|
4031
|
+
"maximum": 32,
|
|
4032
|
+
"description": "Border width in reference pixels; 0 removes this edge."
|
|
4033
|
+
},
|
|
4034
|
+
"dash": {
|
|
4035
|
+
"type": "string",
|
|
4036
|
+
"enum": [
|
|
4037
|
+
"solid",
|
|
4038
|
+
"dash",
|
|
4039
|
+
"dot"
|
|
4040
|
+
],
|
|
4041
|
+
"description": "Default solid."
|
|
4042
|
+
}
|
|
4043
|
+
},
|
|
4044
|
+
"description": "One explicit cell border."
|
|
3886
4045
|
},
|
|
3887
4046
|
"Catalogs": {
|
|
3888
4047
|
"type": "object",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MAX_COMPOSITION_DEPTH } from './chunk-
|
|
2
|
-
import { schemas } from './chunk-
|
|
1
|
+
import { MAX_COMPOSITION_DEPTH, tableGrid } from './chunk-OVPWNQIJ.js';
|
|
2
|
+
import { schemas } from './chunk-GQSK3T7X.js';
|
|
3
3
|
import { catalogSchemaNames } from './chunk-TWMRZ43O.js';
|
|
4
4
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
5
5
|
import addFormats from 'ajv-formats';
|
|
@@ -802,6 +802,14 @@ function validatePresentationSemantics(value) {
|
|
|
802
802
|
slideIds.add(slide.id);
|
|
803
803
|
}
|
|
804
804
|
issues.push(...validateSlideRegions(slide, `/slides/${index}`));
|
|
805
|
+
const visitTables = (node, path) => {
|
|
806
|
+
if (isRecord(node.table)) for (const issue of tableGrid(node.table).issues) issues.push(semanticIssue(pathFor(path, "table") + issue.path.slice(5).replaceAll(".", "/"), issue.message));
|
|
807
|
+
if (Array.isArray(node.blocks)) node.blocks.forEach((block, i) => {
|
|
808
|
+
if (isRecord(block)) visitTables(block, `${path}/blocks/${i}`);
|
|
809
|
+
});
|
|
810
|
+
for (const key of promotedRegionKeys) if (isRecord(node[key])) visitTables(node[key], pathFor(path, key));
|
|
811
|
+
};
|
|
812
|
+
visitTables(slide, `/slides/${index}`);
|
|
805
813
|
}
|
|
806
814
|
});
|
|
807
815
|
return issues;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { assertValidPresentation } from './chunk-
|
|
2
|
-
import { resolveFontFamilies, resolveCanvasDimensions, composeSlide } from './chunk-
|
|
1
|
+
import { assertValidPresentation } from './chunk-H7UXKXEJ.js';
|
|
2
|
+
import { resolveFontFamilies, resolveCanvasDimensions, composeSlide, tableRowBoundaries } from './chunk-OVPWNQIJ.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
|
}
|
|
@@ -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",
|
|
@@ -33,7 +70,7 @@ var fields = ["text", "items", "bullets", "image", "video", "chart", "table", "c
|
|
|
33
70
|
var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
|
|
34
71
|
var rows = ["top", "middle", "bottom"];
|
|
35
72
|
var columns = ["left", "center", "right"];
|
|
36
|
-
var
|
|
73
|
+
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
37
74
|
var kind = (field) => field === "items" || field === "bullets" ? "list" : field;
|
|
38
75
|
var round = (value) => Math.round(value * 1e6) / 1e6 || value;
|
|
39
76
|
var textSegments = new Intl.Segmenter("und", { granularity: "grapheme" });
|
|
@@ -160,6 +197,16 @@ function richTextLayouter(input, box, requestedSize, options) {
|
|
|
160
197
|
y += height;
|
|
161
198
|
return line;
|
|
162
199
|
});
|
|
200
|
+
if (options.uniformLineHeight) {
|
|
201
|
+
const height = Math.max(...richLines.map((line) => line.height));
|
|
202
|
+
y = 0;
|
|
203
|
+
for (const line of richLines) {
|
|
204
|
+
line.baseline += y - line.y;
|
|
205
|
+
line.y = y;
|
|
206
|
+
line.height = height;
|
|
207
|
+
y += height;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
163
210
|
return { lines: ranges.map((range) => whole.slice(range.start, range.end)), fontSize, lineHeight: Math.max(fontSize * 1.22, ...richLines.map((line) => line.height)), richLines, height: y, overflow: y > box.height + 0.01 || richLines.some((line) => line.width > box.width + 0.01) };
|
|
164
211
|
};
|
|
165
212
|
return layout;
|
|
@@ -226,31 +273,70 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
|
|
|
226
273
|
function contentText(field, value) {
|
|
227
274
|
if (field === "text" || headings.has(field)) return flatten(value);
|
|
228
275
|
if (field === "items" || field === "bullets") return (Array.isArray(value) ? value : [value]).map((item) => {
|
|
229
|
-
const entry =
|
|
276
|
+
const entry = record2(item);
|
|
230
277
|
return `\u2022 ${flatten(item)}${entry.description ? `
|
|
231
278
|
${flatten(entry.description)}` : ""}`;
|
|
232
279
|
}).join("\n");
|
|
233
|
-
if (field === "code") return flatten(
|
|
234
|
-
if (field === "quote") return flatten(
|
|
280
|
+
if (field === "code") return flatten(record2(value).source ?? value);
|
|
281
|
+
if (field === "quote") return flatten(record2(value).text ?? value);
|
|
235
282
|
return void 0;
|
|
236
283
|
}
|
|
237
|
-
function
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
284
|
+
function layoutTable(value, box, options = {}) {
|
|
285
|
+
const scale = options.scale ?? 1, requested = 15 * scale, minimum = (options.minFontSize ?? 16) * scale;
|
|
286
|
+
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.");
|
|
287
|
+
const grid = tableGrid(value, options.path ?? "table");
|
|
288
|
+
if (grid.issues.length) throw new RangeError(`${grid.issues[0].path}: ${grid.issues[0].message}`);
|
|
289
|
+
const columnCount = grid.columnCount, cellWidth = box.width / columnCount;
|
|
290
|
+
const cells = grid.rows.flat().map((cell) => {
|
|
291
|
+
const padding = { top: 8, right: 10, bottom: 4, left: 10, ...cell.style.padding };
|
|
292
|
+
const width = cellWidth * cell.colSpan - (padding.left + padding.right) * scale;
|
|
293
|
+
const style = { fontFamily: options.fontFamily ?? "sans-serif", fontWeight: cell.header ? 700 : 400, italic: false, path: cell.valuePath };
|
|
294
|
+
return { ...cell, padding, width, textStyle: style };
|
|
295
|
+
});
|
|
296
|
+
const fitCell = (cell, height, min) => {
|
|
297
|
+
const textBox = { width: Math.max(scale, cell.width), height: Math.max(scale, height) };
|
|
298
|
+
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));
|
|
299
|
+
};
|
|
300
|
+
const textHeight = (fit) => "height" in fit ? fit.height : fit.lines.length * fit.lineHeight;
|
|
301
|
+
const required = (cell, size) => textHeight(fitCell(cell, scale, size)) + (cell.padding.top + cell.padding.bottom) * scale;
|
|
302
|
+
const natural = Array(grid.rowCount).fill(0), needed = Array(grid.rowCount).fill(0);
|
|
303
|
+
for (const cell of cells) if (cell.rowSpan === 1) {
|
|
304
|
+
natural[cell.row] = Math.max(natural[cell.row], required(cell, requested));
|
|
305
|
+
needed[cell.row] = Math.max(needed[cell.row], required(cell, minimum));
|
|
306
|
+
}
|
|
307
|
+
for (const cell of cells) if (cell.rowSpan > 1) for (const [heights2, size] of [[natural, requested], [needed, minimum]]) {
|
|
308
|
+
const current = heights2.slice(cell.row, cell.row + cell.rowSpan).reduce((a, b) => a + b, 0);
|
|
309
|
+
const extra = Math.max(0, required(cell, size) - current) / cell.rowSpan;
|
|
310
|
+
for (let r = cell.row; r < cell.row + cell.rowSpan; r++) heights2[r] += extra;
|
|
311
|
+
}
|
|
312
|
+
const preferred = natural.map((height) => Math.max(54 * scale, height));
|
|
313
|
+
for (let r = 0; r < needed.length; r++) needed[r] = Math.min(preferred[r], needed[r]);
|
|
314
|
+
const sum = (values) => values.reduce((a, b) => a + b, 0);
|
|
315
|
+
const preferredTotal = sum(preferred), naturalTotal = sum(natural), neededTotal = sum(needed);
|
|
316
|
+
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);
|
|
317
|
+
const ys = [box.y];
|
|
318
|
+
for (const height of heights) ys.push(ys.at(-1) + height);
|
|
319
|
+
let overflow = false;
|
|
320
|
+
const rows2 = heights.map((height, r) => ({ box: { x: box.x, y: ys[r], width: box.width, height }, cells: [] }));
|
|
321
|
+
for (const cell of cells) {
|
|
322
|
+
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] };
|
|
323
|
+
const available = cellBox.height - (cell.padding.top + cell.padding.bottom) * scale;
|
|
324
|
+
const fit = fitCell(cell, available, minimum), height = textHeight(fit);
|
|
325
|
+
const offset = cell.style.verticalAlign === "bottom" ? Math.max(0, available - height) : cell.style.verticalAlign === "middle" ? Math.max(0, (available - height) / 2) : 0;
|
|
326
|
+
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) };
|
|
327
|
+
overflow ||= cell.width <= 0 || available <= 0 || fit.overflow;
|
|
328
|
+
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 });
|
|
329
|
+
}
|
|
330
|
+
return { rows: rows2, columnCount, height: sum(heights), overflow };
|
|
331
|
+
}
|
|
332
|
+
function tableOverflows(value, box, scale, settings, options, path) {
|
|
333
|
+
return box.width <= 0 || box.height <= 0 || layoutTable(value, box, { scale, minFontSize: settings.minFontSize, fontFamily: options.fonts?.body, textMeasurement: options.textMeasurement, path }).overflow;
|
|
248
334
|
}
|
|
249
335
|
function flatten(value) {
|
|
250
336
|
if (value == null) return "";
|
|
251
337
|
if (typeof value !== "object") return String(value);
|
|
252
338
|
if (Array.isArray(value)) return value.map(flatten).join("");
|
|
253
|
-
const item =
|
|
339
|
+
const item = record2(value);
|
|
254
340
|
return flatten(item.text ?? item.value ?? item.source ?? JSON.stringify(value));
|
|
255
341
|
}
|
|
256
342
|
function regionParts(key) {
|
|
@@ -306,11 +392,11 @@ function assertComposition(value) {
|
|
|
306
392
|
if (value.overflow !== void 0 && !["warn", "error"].includes(value.overflow)) throw new RangeError("Invalid composition.overflow.");
|
|
307
393
|
}
|
|
308
394
|
function composeSlide(input, options = {}) {
|
|
309
|
-
const slide =
|
|
395
|
+
const slide = record2(input), layout = record2(options.layout);
|
|
310
396
|
const width = options.width ?? 1280, height = options.height ?? 720;
|
|
311
397
|
if (![width, height].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
|
|
312
398
|
const scale = Math.min(width, height) / 720;
|
|
313
|
-
const composition = { ...
|
|
399
|
+
const composition = { ...record2(layout.composition), ...record2(slide.composition) };
|
|
314
400
|
assertComposition(composition);
|
|
315
401
|
const padding = (composition.padding ?? 0.08) * Math.min(width, height);
|
|
316
402
|
const gap = (composition.gap ?? 1 / 30) * Math.min(width, height);
|
|
@@ -340,7 +426,7 @@ function composeSlide(input, options = {}) {
|
|
|
340
426
|
const collect = (host, basePath, depth = 0, ancestors = []) => {
|
|
341
427
|
if (Array.isArray(host.blocks)) {
|
|
342
428
|
if (depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(host)) throw new RangeError(`Content groups must be acyclic and nest at most ${MAX_COMPOSITION_DEPTH} levels.`);
|
|
343
|
-
const settings =
|
|
429
|
+
const settings = record2(host.composition);
|
|
344
430
|
assertComposition(settings);
|
|
345
431
|
return [{
|
|
346
432
|
field: "blocks",
|
|
@@ -349,13 +435,13 @@ function composeSlide(input, options = {}) {
|
|
|
349
435
|
path: basePath,
|
|
350
436
|
payload: host,
|
|
351
437
|
composition: settings,
|
|
352
|
-
children: host.blocks.flatMap((block, index) => collect(
|
|
438
|
+
children: host.blocks.flatMap((block, index) => collect(record2(block), `${basePath}.blocks.${index}`, depth + 1, [...ancestors, host]))
|
|
353
439
|
}];
|
|
354
440
|
}
|
|
355
441
|
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] } }));
|
|
356
442
|
};
|
|
357
443
|
const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
|
|
358
|
-
const pending = regions.length ? regions.flatMap((key) => collect(
|
|
444
|
+
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);
|
|
359
445
|
const groups = [], flows = [];
|
|
360
446
|
const inheritedSettings = (parent, own = {}) => ({
|
|
361
447
|
minFontSize: parent.minFontSize,
|
|
@@ -373,7 +459,7 @@ function composeSlide(input, options = {}) {
|
|
|
373
459
|
const fit = fitContent(node.field, node.value, text, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path);
|
|
374
460
|
score += (25 * scale - fit.fontSize) / scale + (fit.overflow ? 1e3 : 0);
|
|
375
461
|
}
|
|
376
|
-
if (node.field === "table" && tableOverflows(node.value, box, scale, settings, options)) score += 1e3;
|
|
462
|
+
if (node.field === "table" && tableOverflows(node.value, box, scale, settings, options, node.path)) score += 1e3;
|
|
377
463
|
if (box.width < 100 * scale || box.height < 60 * scale) score += 100;
|
|
378
464
|
return score;
|
|
379
465
|
};
|
|
@@ -428,7 +514,7 @@ function composeSlide(input, options = {}) {
|
|
|
428
514
|
arrange(pending, contentBox, rootSettings, composition.mode ? 0 : placeholders.length);
|
|
429
515
|
for (const item of items) {
|
|
430
516
|
for (const key of ["x", "y", "width", "height"]) item.box[key] = round(item.box[key]);
|
|
431
|
-
if (item.field === "table" && tableOverflows(item.value, item.box, scale, item.composition, options)) 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." });
|
|
517
|
+
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." });
|
|
432
518
|
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." });
|
|
433
519
|
}
|
|
434
520
|
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]);
|
|
@@ -447,7 +533,7 @@ function resolveCanvasDimensions(input) {
|
|
|
447
533
|
letter: [11, 8.5],
|
|
448
534
|
a4: [11.69, 8.27]
|
|
449
535
|
};
|
|
450
|
-
const value =
|
|
536
|
+
const value = record2(input);
|
|
451
537
|
const preset = presets[typeof input === "string" ? input : value.preset] ?? presets.widescreen;
|
|
452
538
|
const width = (value.widthInches ?? preset[0]) * 96;
|
|
453
539
|
const height = (value.heightInches ?? preset[1]) * 96;
|
|
@@ -455,4 +541,4 @@ function resolveCanvasDimensions(input) {
|
|
|
455
541
|
return { width, height };
|
|
456
542
|
}
|
|
457
543
|
|
|
458
|
-
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText };
|
|
544
|
+
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textWidthMeasurer, wrapText };
|
package/dist/composition.d.ts
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
+
/** Canonical table grid. No renderer, fonts, DOM or network dependencies. */
|
|
2
|
+
interface TableBorder {
|
|
3
|
+
color: string;
|
|
4
|
+
width: number;
|
|
5
|
+
dash?: 'solid' | 'dash' | 'dot';
|
|
6
|
+
}
|
|
7
|
+
interface TableCellStyle {
|
|
8
|
+
fill?: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
align?: 'left' | 'center' | 'right';
|
|
11
|
+
verticalAlign?: 'top' | 'middle' | 'bottom';
|
|
12
|
+
padding?: Partial<Record<'top' | 'right' | 'bottom' | 'left', number>>;
|
|
13
|
+
borders?: Partial<Record<'top' | 'right' | 'bottom' | 'left', TableBorder>>;
|
|
14
|
+
}
|
|
15
|
+
interface TableGridCell {
|
|
16
|
+
input: unknown;
|
|
17
|
+
value: unknown;
|
|
18
|
+
style: TableCellStyle;
|
|
19
|
+
row: number;
|
|
20
|
+
column: number;
|
|
21
|
+
rowSpan: number;
|
|
22
|
+
colSpan: number;
|
|
23
|
+
header: boolean;
|
|
24
|
+
path: string;
|
|
25
|
+
valuePath: string;
|
|
26
|
+
}
|
|
27
|
+
interface TableGridIssue {
|
|
28
|
+
path: string;
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
interface TableGrid {
|
|
32
|
+
rows: TableGridCell[][];
|
|
33
|
+
columnCount: number;
|
|
34
|
+
rowCount: number;
|
|
35
|
+
hasHeaders: boolean;
|
|
36
|
+
/** Anchor at each covered position; covered positions are not additional cells. */
|
|
37
|
+
owners: (TableGridCell | undefined)[][];
|
|
38
|
+
issues: TableGridIssue[];
|
|
39
|
+
}
|
|
40
|
+
declare function tableGrid(value: unknown, path?: string): TableGrid;
|
|
41
|
+
/** Row boundaries that preserve every vertical merge, used by pagination. */
|
|
42
|
+
declare function tableRowBoundaries(value: unknown): number[];
|
|
43
|
+
|
|
1
44
|
/** Portable layout geometry. No fonts, DOM, renderer, or network dependencies. */
|
|
2
45
|
interface Composition {
|
|
3
46
|
mode?: "auto" | "grid" | "row" | "column";
|
|
@@ -144,6 +187,8 @@ interface RichTextFit extends TextFit {
|
|
|
144
187
|
interface RichTextOptions {
|
|
145
188
|
style: TextStyle;
|
|
146
189
|
textMeasurement?: TextMeasurement;
|
|
190
|
+
/** Use one measured line advance for every line, as native table cells do. */
|
|
191
|
+
uniformLineHeight?: boolean;
|
|
147
192
|
}
|
|
148
193
|
/** Fit mixed styles without flattening font metrics. Run fontSize is in points. */
|
|
149
194
|
declare function fitRichText(input: readonly (string | RichTextRun)[], box: LayoutBox, requestedSize?: number, minFontSize?: number, options?: RichTextOptions): RichTextFit;
|
|
@@ -179,6 +224,46 @@ interface ListFit extends TextFit {
|
|
|
179
224
|
}
|
|
180
225
|
/** Shared hanging indents, mixed-run fitting and description spacing for list payloads. */
|
|
181
226
|
declare function fitList(input: readonly ListValue[], box: LayoutBox, requestedSize?: number, minFontSize?: number, options?: RichTextOptions): ListFit;
|
|
227
|
+
interface TableLayoutOptions {
|
|
228
|
+
scale?: number;
|
|
229
|
+
minFontSize?: number;
|
|
230
|
+
fontFamily?: string;
|
|
231
|
+
textMeasurement?: TextMeasurement;
|
|
232
|
+
path?: string;
|
|
233
|
+
}
|
|
234
|
+
interface TableCellLayout {
|
|
235
|
+
value: unknown;
|
|
236
|
+
input: unknown;
|
|
237
|
+
sourcePath: string;
|
|
238
|
+
style: TableCellStyle;
|
|
239
|
+
row: number;
|
|
240
|
+
column: number;
|
|
241
|
+
rowSpan: number;
|
|
242
|
+
colSpan: number;
|
|
243
|
+
path: string;
|
|
244
|
+
header: boolean;
|
|
245
|
+
rich: boolean;
|
|
246
|
+
box: LayoutBox;
|
|
247
|
+
textBox: LayoutBox;
|
|
248
|
+
textStyle: TextStyle;
|
|
249
|
+
fit: TextFit | RichTextFit;
|
|
250
|
+
}
|
|
251
|
+
interface TableRowLayout {
|
|
252
|
+
box: LayoutBox;
|
|
253
|
+
cells: TableCellLayout[];
|
|
254
|
+
}
|
|
255
|
+
interface TableLayout {
|
|
256
|
+
rows: TableRowLayout[];
|
|
257
|
+
columnCount: number;
|
|
258
|
+
height: number;
|
|
259
|
+
overflow: boolean;
|
|
260
|
+
}
|
|
261
|
+
/** Shared cell geometry and font fitting for SVG, native PPTX and pagination.
|
|
262
|
+
* Short rows retain their 54px preferred height. Multiline rows use the space
|
|
263
|
+
* their text needs; constrained tables consume row padding before readable text.
|
|
264
|
+
* All dimensions are canvas pixels; minFontSize is an unscaled canvas size.
|
|
265
|
+
*/
|
|
266
|
+
declare function layoutTable(value: unknown, box: LayoutBox, options?: TableLayoutOptions): TableLayout;
|
|
182
267
|
/** Compose a validated Slide. Layout resolution remains the caller's responsibility. */
|
|
183
268
|
declare function composeSlide(input: unknown, options?: ComposeSlideOptions): SlideComposition;
|
|
184
269
|
/** Canonical physical slide size, converted to reference pixels at 96 pixels/inch. */
|
|
@@ -187,4 +272,4 @@ declare function resolveCanvasDimensions(input: unknown): {
|
|
|
187
272
|
height: number;
|
|
188
273
|
};
|
|
189
274
|
|
|
190
|
-
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 TextFit, type TextMeasurement, type TextStyle, composeSlide, fitList, fitRichText, fitText, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText };
|
|
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 };
|
package/dist/composition.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText } from './chunk-
|
|
1
|
+
export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textWidthMeasurer, wrapText } from './chunk-OVPWNQIJ.js';
|