@openpresentation/opf 0.11.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/audit.d.ts +156 -0
- package/dist/audit.js +7 -0
- package/dist/{catalogs-CUClB3nl.d.ts → catalogs-u6TEeMcz.d.ts} +1 -1
- package/dist/catalogs.d.ts +2 -2
- package/dist/catalogs.js +2 -1
- package/dist/{chunk-UDPSGTZI.js → chunk-32DIB5BN.js} +12 -9
- package/dist/chunk-4FWCY4ZV.js +198 -0
- package/dist/chunk-6TSDIPSH.js +3624 -0
- package/dist/chunk-FYLJVF7X.js +18448 -0
- package/dist/{chunk-HJL64ETN.js → chunk-GJKJL4YY.js} +14 -0
- package/dist/chunk-JENJZR3F.js +1690 -0
- package/dist/{chunk-57NWIYX3.js → chunk-JVTN3HPP.js} +18 -4
- package/dist/{chunk-JTXCMVVP.js → chunk-RVN5C2FX.js} +754 -39
- package/dist/chunk-S5W34SIJ.js +1 -0
- package/dist/chunk-S667QI3M.js +261 -0
- package/dist/chunk-UQKJWHSS.js +1332 -0
- package/dist/{chunk-RXNFDGPC.js → chunk-WXSPETQ6.js} +311 -40
- package/dist/composition-DWbCiMwF.d.ts +1523 -0
- package/dist/composition.d.ts +1 -1
- package/dist/composition.js +2 -1
- package/dist/convert.d.ts +261 -0
- package/dist/convert.js +1107 -0
- package/dist/diff.d.ts +145 -0
- package/dist/diff.js +604 -0
- package/dist/docs.js +69 -15
- package/dist/examples.d.ts +1 -1
- package/dist/font-policy.d.ts +15 -0
- package/dist/font-policy.js +1 -1
- package/dist/format.d.ts +20 -0
- package/dist/format.js +83 -0
- package/dist/index.d.ts +173 -24
- package/dist/index.js +926 -262
- package/dist/lint.d.ts +4 -4
- package/dist/lint.js +6 -5
- package/dist/markdown.d.ts +74 -0
- package/dist/markdown.js +1941 -0
- package/dist/pagination.d.ts +1 -1
- package/dist/pagination.js +6 -5
- package/dist/patch.d.ts +99 -0
- package/dist/patch.js +6 -0
- package/dist/{presentation-Cn6vTOCP.d.ts → presentation-DRx4hpNZ.d.ts} +479 -32
- package/dist/repo-readme.js +1 -1
- package/dist/{schemas-X_NniU4A.d.ts → schemas-BSjlG8-R.d.ts} +28 -0
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +1 -1
- package/dist/spec/reference/font-policy.json +311 -40
- package/dist/spec/reference/font-policy.schema.json +40 -0
- package/dist/spec/reference/symbol-font-encodings.json +18354 -0
- package/dist/spec/reference/symbol-font-encodings.schema.json +156 -0
- package/dist/spec/schemas/opf.schema.json +608 -35
- package/dist/spec-files.d.ts +1 -1
- package/dist/spec-files.js +1 -1
- package/dist/symbol-font-encodings.d.ts +128 -0
- package/dist/symbol-font-encodings.js +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/validator-bu396ffT.d.ts +172 -0
- package/dist/validator.d.ts +4 -36
- package/dist/validator.js +5 -4
- package/package.json +31 -2
- package/dist/chunk-BVLWFMDX.js +0 -1812
- package/dist/chunk-JCXHSTSM.js +0 -1366
- package/dist/composition-DzLwY-Mi.d.ts +0 -887
- /package/dist/{chunk-AEUSVEUY.js → chunk-3ZGSWEHK.js} +0 -0
|
@@ -0,0 +1,1332 @@
|
|
|
1
|
+
import { promotedRegionKeys, isRecord, collectCitations, parseIsoDate, pathFor, MAX_COMPOSITION_DEPTH, visitContentPayloads, tableGrid, numberingFindings, walkCitationRuns, bareIdPattern, catalogIds, deprecatedCatalogIds, resolveChartOptions, chartOptionTarget, formatFurnitureDate } from './chunk-6TSDIPSH.js';
|
|
2
|
+
import { catalogSchemaNames } from './chunk-3ZGSWEHK.js';
|
|
3
|
+
import { schemas, schemaEntries } from './chunk-RVN5C2FX.js';
|
|
4
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
5
|
+
import addFormats from 'ajv-formats';
|
|
6
|
+
|
|
7
|
+
// src/annotation-validation.ts
|
|
8
|
+
var issue = (path, message, params) => ({ path, message, keyword: "opf", schemaPath: "#/x-opf-semantics", params });
|
|
9
|
+
var pointer = (dotted) => `/${dotted.split(".").map((part) => part.replaceAll("~", "~0").replaceAll("/", "~1")).join("/")}`;
|
|
10
|
+
function markedRuns(value) {
|
|
11
|
+
if (!Array.isArray(value)) return [];
|
|
12
|
+
return value.flatMap((run, index) => isRecord(run) && (run.cite !== void 0 || run.footnote !== void 0) ? [index] : []);
|
|
13
|
+
}
|
|
14
|
+
function annotationIssues(value) {
|
|
15
|
+
if (!isRecord(value) || !Array.isArray(value.slides)) return [];
|
|
16
|
+
const issues = [];
|
|
17
|
+
const ids = /* @__PURE__ */ new Set();
|
|
18
|
+
if (Array.isArray(value.references)) value.references.forEach((reference, index) => {
|
|
19
|
+
if (!isRecord(reference) || typeof reference.id !== "string") return;
|
|
20
|
+
if (ids.has(reference.id)) issues.push(issue(`/references/${index}/id`, `reference id '${reference.id}' is used by an earlier references entry; ids must be unique`, { code: "reference-id-duplicate", id: reference.id }));
|
|
21
|
+
ids.add(reference.id);
|
|
22
|
+
for (const runIndex of markedRuns(reference.text)) issues.push(issue(`/references/${index}/text/${runIndex}`, "reference text cannot cite or carry a footnote", { code: "cite-unsupported-location" }));
|
|
23
|
+
});
|
|
24
|
+
const unsupported = (runs, path, where) => {
|
|
25
|
+
for (const runIndex of markedRuns(runs)) issues.push(issue(`${path}/${runIndex}`, `cite and footnote are not supported in ${where}; markers are drawn only in text, bullets and list items`, { code: "cite-unsupported-location" }));
|
|
26
|
+
};
|
|
27
|
+
const cell = (entry, path) => {
|
|
28
|
+
if (Array.isArray(entry)) unsupported(entry, path, "table cells");
|
|
29
|
+
else if (isRecord(entry) && Array.isArray(entry.value)) unsupported(entry.value, pathFor(path, "value"), "table cells");
|
|
30
|
+
};
|
|
31
|
+
const payload = (node, path) => {
|
|
32
|
+
if (isRecord(node.table)) {
|
|
33
|
+
const table = node.table, tablePath = pathFor(path, "table");
|
|
34
|
+
if (Array.isArray(table.columns)) table.columns.forEach((column, index) => {
|
|
35
|
+
cell(column, `${pathFor(tablePath, "columns")}/${index}`);
|
|
36
|
+
});
|
|
37
|
+
if (Array.isArray(table.rows)) table.rows.forEach((row, rowIndex) => {
|
|
38
|
+
if (Array.isArray(row)) row.forEach((entry, cellIndex) => {
|
|
39
|
+
cell(entry, `${pathFor(tablePath, "rows")}/${rowIndex}/${cellIndex}`);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (node.caption !== void 0) {
|
|
44
|
+
const caption = node.caption, captionPath = pathFor(path, "caption");
|
|
45
|
+
if (Array.isArray(caption)) unsupported(caption, captionPath, "captions");
|
|
46
|
+
else if (isRecord(caption) && Array.isArray(caption.text)) unsupported(caption.text, pathFor(captionPath, "text"), "captions");
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
value.slides.forEach((slide, index) => {
|
|
50
|
+
if (!isRecord(slide)) return;
|
|
51
|
+
const slidePath = `/slides/${index}`;
|
|
52
|
+
payload(slide, slidePath);
|
|
53
|
+
visitContentPayloads(slide, slidePath, payload);
|
|
54
|
+
walkCitationRuns(slide, `slides.${index}`, (entry) => {
|
|
55
|
+
const runPath = pointer(entry.path);
|
|
56
|
+
const cite = entry.run.cite;
|
|
57
|
+
if (typeof cite === "string") {
|
|
58
|
+
if (!ids.has(cite)) issues.push(issue(`${runPath}/cite`, `unknown reference '${cite}'; add it to the top-level references list`, { code: "cite-unknown-reference", id: cite }));
|
|
59
|
+
} else if (Array.isArray(cite)) cite.forEach((id, position) => {
|
|
60
|
+
if (typeof id === "string" && !ids.has(id)) issues.push(issue(`${runPath}/cite/${position}`, `unknown reference '${id}'; add it to the top-level references list`, { code: "cite-unknown-reference", id }));
|
|
61
|
+
});
|
|
62
|
+
if (Array.isArray(entry.footnote)) unsupported(entry.footnote, `${runPath}/footnote`, "footnote text");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
return issues;
|
|
66
|
+
}
|
|
67
|
+
function unusedReferenceWarnings(value) {
|
|
68
|
+
if (!isRecord(value) || !Array.isArray(value.references) || !Array.isArray(value.slides)) return [];
|
|
69
|
+
const { unused } = collectCitations(value);
|
|
70
|
+
return value.references.flatMap((reference, index) => isRecord(reference) && typeof reference.id === "string" && unused.includes(reference.id) ? [issue(`/references/${index}`, `reference '${reference.id}' is never cited; cite it with a run's cite field or remove it`, { code: "unused-reference", id: reference.id })] : []);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/validation-definitions.ts
|
|
74
|
+
var schemaLocations;
|
|
75
|
+
var issueLocations = /* @__PURE__ */ new WeakMap();
|
|
76
|
+
function rememberValidationDefinition(issue2, parentSchema, keyword) {
|
|
77
|
+
if (!parentSchema || typeof parentSchema !== "object") return;
|
|
78
|
+
if (!schemaLocations) {
|
|
79
|
+
schemaLocations = /* @__PURE__ */ new WeakMap();
|
|
80
|
+
for (const entry of schemaEntries) {
|
|
81
|
+
const stack = [
|
|
82
|
+
{ value: entry.schema, pointer: "" }
|
|
83
|
+
];
|
|
84
|
+
while (stack.length) {
|
|
85
|
+
const current = stack.pop();
|
|
86
|
+
if (!current?.value || typeof current.value !== "object")
|
|
87
|
+
continue;
|
|
88
|
+
schemaLocations.set(current.value, {
|
|
89
|
+
schemaName: entry.name,
|
|
90
|
+
pointer: current.pointer,
|
|
91
|
+
uri: `${entry.schema.$id}#${current.pointer}`
|
|
92
|
+
});
|
|
93
|
+
for (const [key, value] of Object.entries(current.value)) {
|
|
94
|
+
const segment = key.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
95
|
+
stack.push({ value, pointer: `${current.pointer}/${segment}` });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const parent = schemaLocations.get(parentSchema);
|
|
101
|
+
if (!parent) return;
|
|
102
|
+
const suffix = `/${keyword.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
103
|
+
issueLocations.set(issue2, {
|
|
104
|
+
...parent,
|
|
105
|
+
pointer: parent.pointer + suffix,
|
|
106
|
+
uri: parent.uri + suffix
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function validationDefinition(issue2) {
|
|
110
|
+
return issueLocations.get(issue2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/variables.ts
|
|
114
|
+
var VARIABLE_KINDS = ["color", "text", "number", "date", "image", "url", "list"];
|
|
115
|
+
var DEFAULT_VARIABLE_DATE_FORMAT = "MMMM d, yyyy";
|
|
116
|
+
var OPFVariableError = class extends Error {
|
|
117
|
+
diagnostics;
|
|
118
|
+
constructor(diagnostics) {
|
|
119
|
+
const first = diagnostics.find((entry) => entry.severity === "error") ?? diagnostics[0];
|
|
120
|
+
super(first ? `OPF variables: ${first.message}` : "OPF variables failed");
|
|
121
|
+
this.name = "OPFVariableError";
|
|
122
|
+
this.diagnostics = diagnostics;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var idPattern = /^[a-z][a-z0-9-]*$/;
|
|
126
|
+
var referencePattern = /^var:([a-z][a-z0-9-]*)$/;
|
|
127
|
+
var hexPattern = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
128
|
+
var urlPattern = /^(?:https?:\/\/|mailto:|tel:)\S+$/;
|
|
129
|
+
var datePrefixPattern = /^(\d{4}-\d{2}-\d{2})(?:[T ].*)?$/;
|
|
130
|
+
var decimalPattern = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
131
|
+
var tokenPattern = /\\\{\{|\{\{\s*([a-z][a-z0-9-]*)\s*(?:\|([^{}]*))?\}\}/g;
|
|
132
|
+
var SAMPLES = {
|
|
133
|
+
color: "#000000",
|
|
134
|
+
text: "Sample text",
|
|
135
|
+
number: 0,
|
|
136
|
+
date: "2000-01-01",
|
|
137
|
+
image: "https://example.invalid/sample.png",
|
|
138
|
+
url: "https://example.invalid/",
|
|
139
|
+
list: ["Sample item"]
|
|
140
|
+
};
|
|
141
|
+
var hasOwn = (value, key) => Object.hasOwn(value, key);
|
|
142
|
+
var clone = (value) => value === void 0 ? value : JSON.parse(JSON.stringify(value));
|
|
143
|
+
function isTemplate(presentation) {
|
|
144
|
+
return isRecord(presentation) && presentation.template === true;
|
|
145
|
+
}
|
|
146
|
+
function isKind(value) {
|
|
147
|
+
return typeof value === "string" && VARIABLE_KINDS.includes(value);
|
|
148
|
+
}
|
|
149
|
+
function coerceVariableValue(kind, raw) {
|
|
150
|
+
const fail = (message) => ({ ok: false, message });
|
|
151
|
+
switch (kind) {
|
|
152
|
+
case "color":
|
|
153
|
+
return typeof raw === "string" && hexPattern.test(raw.trim()) ? { ok: true, value: raw.trim() } : fail("expected a hex color such as #0F4C81");
|
|
154
|
+
case "text": {
|
|
155
|
+
if (typeof raw === "string") return { ok: true, value: raw };
|
|
156
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return { ok: true, value: String(raw) };
|
|
157
|
+
if (typeof raw === "boolean") return { ok: true, value: String(raw) };
|
|
158
|
+
if (Array.isArray(raw) && raw.every((run) => typeof run === "string" || isRecord(run) && typeof run.text === "string")) return { ok: true, value: clone(raw) };
|
|
159
|
+
return fail("expected text: a string or an array of text runs");
|
|
160
|
+
}
|
|
161
|
+
case "number": {
|
|
162
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return { ok: true, value: raw };
|
|
163
|
+
if (typeof raw === "string" && decimalPattern.test(raw.trim())) {
|
|
164
|
+
const value = Number(raw.trim());
|
|
165
|
+
if (Number.isFinite(value)) return { ok: true, value };
|
|
166
|
+
}
|
|
167
|
+
return fail("expected a finite number");
|
|
168
|
+
}
|
|
169
|
+
case "date": {
|
|
170
|
+
if (typeof raw === "string") {
|
|
171
|
+
const match = datePrefixPattern.exec(raw.trim());
|
|
172
|
+
if (match?.[1] && parseIsoDate(match[1])) return { ok: true, value: match[1] };
|
|
173
|
+
}
|
|
174
|
+
return fail("expected an ISO calendar date (YYYY-MM-DD)");
|
|
175
|
+
}
|
|
176
|
+
case "image": {
|
|
177
|
+
if (typeof raw === "string" && raw.trim()) return { ok: true, value: raw.trim() };
|
|
178
|
+
if (isRecord(raw) && typeof raw.src === "string" && raw.src.trim()) return { ok: true, value: clone(raw) };
|
|
179
|
+
return fail("expected an image source: a string or an object with src");
|
|
180
|
+
}
|
|
181
|
+
case "url":
|
|
182
|
+
return typeof raw === "string" && urlPattern.test(raw.trim()) ? { ok: true, value: raw.trim() } : fail("expected an http, https, mailto or tel link");
|
|
183
|
+
case "list": {
|
|
184
|
+
if (typeof raw === "string") {
|
|
185
|
+
const entries = raw.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
|
|
186
|
+
return { ok: true, value: entries };
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string" || typeof entry === "number" && Number.isFinite(entry))) {
|
|
189
|
+
return { ok: true, value: raw.map(String) };
|
|
190
|
+
}
|
|
191
|
+
return fail("expected a list of strings");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function isProvided(kind, raw) {
|
|
196
|
+
if (raw === void 0 || raw === null) return false;
|
|
197
|
+
if (typeof raw === "string" && raw.trim() === "" && kind !== "text") return false;
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
function variableDeclarations(presentation) {
|
|
201
|
+
if (!isRecord(presentation) || !isRecord(presentation.variables)) return [];
|
|
202
|
+
const out = [];
|
|
203
|
+
for (const [id, entry] of Object.entries(presentation.variables)) {
|
|
204
|
+
if (!idPattern.test(id)) continue;
|
|
205
|
+
const path = pathFor("/variables", id);
|
|
206
|
+
if (typeof entry === "string") {
|
|
207
|
+
if (hexPattern.test(entry)) out.push({ id, kind: "color", path, required: true, value: entry });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (!isRecord(entry) || !isKind(entry.type)) continue;
|
|
211
|
+
const kind = entry.type;
|
|
212
|
+
const declaration = { id, kind, path, required: entry.required !== false };
|
|
213
|
+
const value = entry.value !== void 0 ? coerceVariableValue(kind, entry.value) : void 0;
|
|
214
|
+
if (value?.ok) declaration.value = value.value;
|
|
215
|
+
const example = entry.example !== void 0 ? coerceVariableValue(kind, entry.example) : void 0;
|
|
216
|
+
if (example?.ok) declaration.example = example.value;
|
|
217
|
+
if (typeof entry.format === "string") declaration.format = entry.format;
|
|
218
|
+
if (typeof entry.label === "string") declaration.label = entry.label;
|
|
219
|
+
if (typeof entry.description === "string") declaration.description = entry.description;
|
|
220
|
+
out.push(declaration);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
function hasContentVariables(presentation) {
|
|
225
|
+
return isTemplate(presentation) || variableDeclarations(presentation).some((declaration) => declaration.kind !== "color");
|
|
226
|
+
}
|
|
227
|
+
var POSITIVE_ZERO = "0";
|
|
228
|
+
function formatVariableNumber(value, pattern) {
|
|
229
|
+
if (!Number.isFinite(value)) return { error: `'${String(value)}' is not a finite number.` };
|
|
230
|
+
if (pattern === void 0 || pattern === "") return { text: String(value) };
|
|
231
|
+
const match = /^([^#0,.]*)((?:[#0,]*)(?:\.[#0]+)?)([\s\S]*)$/.exec(pattern);
|
|
232
|
+
const numeric = match?.[2] ?? "";
|
|
233
|
+
if (!match || !/[#0]/.test(numeric)) return { error: `Number format '${pattern}' needs a numeric part of '#' and '0' characters.` };
|
|
234
|
+
const prefix = match[1] ?? "";
|
|
235
|
+
const suffix = match[3] ?? "";
|
|
236
|
+
const [integerPattern = "", decimalPattern2 = ""] = numeric.split(".");
|
|
237
|
+
const grouping = integerPattern.includes(",");
|
|
238
|
+
const minInteger = [...integerPattern].filter((char) => char === "0").length;
|
|
239
|
+
const minDecimals = [...decimalPattern2].filter((char) => char === "0").length;
|
|
240
|
+
const maxDecimals = decimalPattern2.length;
|
|
241
|
+
const scaled = prefix.includes("%") || suffix.includes("%") ? value * 100 : value;
|
|
242
|
+
const absolute = Math.abs(scaled);
|
|
243
|
+
let fixed;
|
|
244
|
+
const shifted = Number(`${absolute}e${maxDecimals}`);
|
|
245
|
+
if (absolute >= 1e21) fixed = `${BigInt(absolute).toString()}${maxDecimals ? `.${"0".repeat(maxDecimals)}` : ""}`;
|
|
246
|
+
else if (/e/i.test(String(absolute)) || !Number.isSafeInteger(Math.round(shifted))) fixed = absolute.toFixed(maxDecimals);
|
|
247
|
+
else {
|
|
248
|
+
const digits = String(Math.round(shifted)).padStart(maxDecimals + 1, "0");
|
|
249
|
+
fixed = maxDecimals ? `${digits.slice(0, -maxDecimals)}.${digits.slice(-maxDecimals)}` : digits;
|
|
250
|
+
}
|
|
251
|
+
let [integer = POSITIVE_ZERO, decimals = ""] = fixed.split(".");
|
|
252
|
+
while (decimals.length > minDecimals && decimals.endsWith("0")) decimals = decimals.slice(0, -1);
|
|
253
|
+
integer = integer.replace(/^0+(?=\d)/, "");
|
|
254
|
+
if (integer === POSITIVE_ZERO && minInteger === 0 && decimals) integer = "";
|
|
255
|
+
integer = integer.padStart(minInteger, "0");
|
|
256
|
+
if (grouping) integer = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
257
|
+
const body = `${integer}${decimals ? `.${decimals}` : ""}` || POSITIVE_ZERO;
|
|
258
|
+
const negative = scaled < 0 && /[1-9]/.test(body);
|
|
259
|
+
return { text: `${negative ? "-" : ""}${prefix}${body}${suffix}` };
|
|
260
|
+
}
|
|
261
|
+
function diag(context, entry, dedupeKey) {
|
|
262
|
+
if (dedupeKey) {
|
|
263
|
+
if (context.seen.has(dedupeKey)) return;
|
|
264
|
+
context.seen.add(dedupeKey);
|
|
265
|
+
}
|
|
266
|
+
context.diagnostics.push(entry);
|
|
267
|
+
}
|
|
268
|
+
function sampleValue(declaration) {
|
|
269
|
+
return declaration.example !== void 0 ? declaration.example : declaration.value !== void 0 ? declaration.value : SAMPLES[declaration.kind];
|
|
270
|
+
}
|
|
271
|
+
function plainText(value) {
|
|
272
|
+
if (typeof value === "string") return value;
|
|
273
|
+
if (Array.isArray(value)) return value.map((run) => typeof run === "string" ? run : isRecord(run) && typeof run.text === "string" ? run.text : "").join("");
|
|
274
|
+
return "";
|
|
275
|
+
}
|
|
276
|
+
function inlineText(effective, format, path, context) {
|
|
277
|
+
const { declaration } = effective;
|
|
278
|
+
const value = effective.status === "unfilled" ? sampleValue(declaration) : effective.value;
|
|
279
|
+
switch (declaration.kind) {
|
|
280
|
+
case "color":
|
|
281
|
+
case "url":
|
|
282
|
+
return String(value);
|
|
283
|
+
case "text":
|
|
284
|
+
if (Array.isArray(value)) {
|
|
285
|
+
diag(context, { code: "variable-rich-flattened", severity: "info", path, id: declaration.id, message: `Rich text variable '${declaration.id}' is inserted as plain text inside a string; reference it whole as 'var:${declaration.id}' to keep its formatting.` }, `flat:${declaration.id}:${path}`);
|
|
286
|
+
}
|
|
287
|
+
return plainText(value);
|
|
288
|
+
case "number": {
|
|
289
|
+
const pattern = (format ?? declaration.format)?.trim();
|
|
290
|
+
const formatted = formatVariableNumber(value, pattern === "" ? void 0 : pattern);
|
|
291
|
+
if ("error" in formatted) {
|
|
292
|
+
diag(context, { code: "variable-format", severity: "error", path, id: declaration.id, message: formatted.error }, `fmt:${declaration.id}:${format ?? ""}`);
|
|
293
|
+
return String(value);
|
|
294
|
+
}
|
|
295
|
+
return formatted.text;
|
|
296
|
+
}
|
|
297
|
+
case "date": {
|
|
298
|
+
const pattern = (format ?? declaration.format)?.trim() || DEFAULT_VARIABLE_DATE_FORMAT;
|
|
299
|
+
const formatted = formatFurnitureDate(String(value), pattern);
|
|
300
|
+
if ("error" in formatted) {
|
|
301
|
+
diag(context, { code: "variable-format", severity: "error", path, id: declaration.id, message: formatted.error }, `fmt:${declaration.id}:${format ?? ""}`);
|
|
302
|
+
return String(value);
|
|
303
|
+
}
|
|
304
|
+
return formatted.text;
|
|
305
|
+
}
|
|
306
|
+
case "image":
|
|
307
|
+
return isRecord(value) ? String(value.src) : String(value);
|
|
308
|
+
case "list":
|
|
309
|
+
return value.join(format ?? ", ");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function interpolate(text, path, context) {
|
|
313
|
+
let changed = false;
|
|
314
|
+
const value = text.replace(tokenPattern, (match, id, format) => {
|
|
315
|
+
if (id === void 0) {
|
|
316
|
+
if (context.unescape) {
|
|
317
|
+
changed = true;
|
|
318
|
+
return "{{";
|
|
319
|
+
}
|
|
320
|
+
return match;
|
|
321
|
+
}
|
|
322
|
+
context.uses.push({ id, path, form: "token" });
|
|
323
|
+
const effective = context.effective.get(id);
|
|
324
|
+
if (!effective) {
|
|
325
|
+
diag(context, { code: "variable-unknown", severity: "warning", path, id, message: `'{{${id}}}' names no declared variable; declare '${id}' in the top-level variables map, or write '\\{{' for literal braces.` }, `unk:${id}:${path}`);
|
|
326
|
+
return match;
|
|
327
|
+
}
|
|
328
|
+
if (effective.status === "unfilled" && !context.shape) {
|
|
329
|
+
if (!effective.declaration.required) {
|
|
330
|
+
changed = true;
|
|
331
|
+
return "";
|
|
332
|
+
}
|
|
333
|
+
return match;
|
|
334
|
+
}
|
|
335
|
+
const result = inlineText(effective, effective.declaration.kind === "list" ? format : format?.trim(), path, context);
|
|
336
|
+
if (result === void 0) return match;
|
|
337
|
+
changed = true;
|
|
338
|
+
return result;
|
|
339
|
+
});
|
|
340
|
+
return { value, changed };
|
|
341
|
+
}
|
|
342
|
+
var OMIT = /* @__PURE__ */ Symbol("omit");
|
|
343
|
+
var Splice = class {
|
|
344
|
+
constructor(values) {
|
|
345
|
+
this.values = values;
|
|
346
|
+
}
|
|
347
|
+
values;
|
|
348
|
+
};
|
|
349
|
+
function reduceString(text, path, parentIsArray, context) {
|
|
350
|
+
const reference = referencePattern.exec(text);
|
|
351
|
+
if (reference) {
|
|
352
|
+
const id = reference[1];
|
|
353
|
+
const effective = context.effective.get(id);
|
|
354
|
+
if (effective && effective.declaration.kind !== "color") {
|
|
355
|
+
context.uses.push({ id, path, form: "reference" });
|
|
356
|
+
if (effective.status === "unfilled" && !context.shape) {
|
|
357
|
+
return effective.declaration.required ? { kind: "keep" } : { kind: "omit" };
|
|
358
|
+
}
|
|
359
|
+
const value = effective.status === "unfilled" ? sampleValue(effective.declaration) : effective.value;
|
|
360
|
+
if (effective.declaration.kind === "list" && parentIsArray) {
|
|
361
|
+
return context.shape ? { kind: "replace", value: value[0] ?? "" } : { kind: "splice", values: clone(value) };
|
|
362
|
+
}
|
|
363
|
+
return { kind: "replace", value: clone(value) };
|
|
364
|
+
}
|
|
365
|
+
if (effective) {
|
|
366
|
+
context.uses.push({ id, path, form: "reference" });
|
|
367
|
+
return { kind: "keep" };
|
|
368
|
+
}
|
|
369
|
+
return { kind: "keep" };
|
|
370
|
+
}
|
|
371
|
+
if (!text.includes("{{")) return { kind: "keep" };
|
|
372
|
+
const result = interpolate(text, path, context);
|
|
373
|
+
return result.changed ? { kind: "replace", value: result.value } : { kind: "keep" };
|
|
374
|
+
}
|
|
375
|
+
var skippedKeys = /* @__PURE__ */ new Set(["extensions"]);
|
|
376
|
+
var skippedRootKeys = /* @__PURE__ */ new Set(["variables", "catalogs", "$schema", "template"]);
|
|
377
|
+
function walk(value, path, parentIsArray, context) {
|
|
378
|
+
if (typeof value === "string") {
|
|
379
|
+
const reduced = reduceString(value, path, parentIsArray, context);
|
|
380
|
+
if (reduced.kind === "keep") return value;
|
|
381
|
+
if (reduced.kind === "omit") return OMIT;
|
|
382
|
+
if (reduced.kind === "splice") return new Splice(reduced.values);
|
|
383
|
+
return reduced.value;
|
|
384
|
+
}
|
|
385
|
+
if (Array.isArray(value)) {
|
|
386
|
+
let changed = false;
|
|
387
|
+
const out = [];
|
|
388
|
+
value.forEach((entry, index) => {
|
|
389
|
+
const next = walk(entry, `${path}/${index}`, true, context);
|
|
390
|
+
if (next === OMIT) {
|
|
391
|
+
changed = true;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (next instanceof Splice) {
|
|
395
|
+
changed = true;
|
|
396
|
+
out.push(...next.values);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (next !== entry) changed = true;
|
|
400
|
+
out.push(next);
|
|
401
|
+
});
|
|
402
|
+
return changed ? out : value;
|
|
403
|
+
}
|
|
404
|
+
if (isRecord(value)) {
|
|
405
|
+
let changed = false;
|
|
406
|
+
const entries = [];
|
|
407
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
408
|
+
if (skippedKeys.has(key) || path === "" && skippedRootKeys.has(key)) {
|
|
409
|
+
entries.push([key, entry]);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const next = walk(entry, pathFor(path || "/", key), false, context);
|
|
413
|
+
if (next === OMIT) {
|
|
414
|
+
changed = true;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (next !== entry) changed = true;
|
|
418
|
+
entries.push([key, next]);
|
|
419
|
+
}
|
|
420
|
+
return changed ? Object.fromEntries(entries) : value;
|
|
421
|
+
}
|
|
422
|
+
return value;
|
|
423
|
+
}
|
|
424
|
+
function plan(presentation, values, options) {
|
|
425
|
+
const declarations = variableDeclarations(presentation);
|
|
426
|
+
const template = options.template ?? isTemplate(presentation);
|
|
427
|
+
const lenient = template || options.partial === true;
|
|
428
|
+
const context = {
|
|
429
|
+
effective: /* @__PURE__ */ new Map(),
|
|
430
|
+
diagnostics: [],
|
|
431
|
+
uses: [],
|
|
432
|
+
unescape: false,
|
|
433
|
+
shape: options.shape === true,
|
|
434
|
+
content: hasContentVariables(presentation) || template,
|
|
435
|
+
seen: /* @__PURE__ */ new Set()
|
|
436
|
+
};
|
|
437
|
+
const unfilled = [];
|
|
438
|
+
const examplesUsed = [];
|
|
439
|
+
const providedColors = /* @__PURE__ */ new Map();
|
|
440
|
+
const declared = new Set(declarations.map((declaration) => declaration.id));
|
|
441
|
+
for (const key of Object.keys(values)) {
|
|
442
|
+
if (!declared.has(key)) {
|
|
443
|
+
context.diagnostics.push({ code: "variable-unknown-value", severity: "warning", path: "/variables", id: key, message: `A value was supplied for '${key}', which no variable declares.` });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
for (const declaration of declarations) {
|
|
447
|
+
const raw = hasOwn(values, declaration.id) ? values[declaration.id] : void 0;
|
|
448
|
+
let effective;
|
|
449
|
+
if (isProvided(declaration.kind, raw)) {
|
|
450
|
+
const coerced = coerceVariableValue(declaration.kind, raw);
|
|
451
|
+
if (coerced.ok) {
|
|
452
|
+
effective = { declaration, status: "filled", value: coerced.value, provided: true };
|
|
453
|
+
if (declaration.kind === "color") providedColors.set(declaration.id, coerced.value);
|
|
454
|
+
} else {
|
|
455
|
+
context.diagnostics.push({ code: "variable-invalid-value", severity: "error", path: declaration.path, id: declaration.id, message: `Value for '${declaration.id}' (${declaration.kind}): ${coerced.message}.` });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (!effective && declaration.value !== void 0) effective = { declaration, status: "filled", value: declaration.value, provided: false };
|
|
459
|
+
if (!effective && options.examples && declaration.example !== void 0) {
|
|
460
|
+
effective = { declaration, status: "example", value: declaration.example, provided: false };
|
|
461
|
+
examplesUsed.push(declaration.id);
|
|
462
|
+
context.diagnostics.push({ code: "variable-example-used", severity: "info", path: declaration.path, id: declaration.id, message: `'${declaration.id}' is unfilled; its example is used.` });
|
|
463
|
+
if (declaration.kind === "color") providedColors.set(declaration.id, declaration.example);
|
|
464
|
+
}
|
|
465
|
+
if (!effective) {
|
|
466
|
+
effective = { declaration, status: "unfilled", provided: false };
|
|
467
|
+
if (declaration.required) {
|
|
468
|
+
unfilled.push(declaration.id);
|
|
469
|
+
context.diagnostics.push({
|
|
470
|
+
code: "variable-unfilled",
|
|
471
|
+
severity: lenient ? "info" : "error",
|
|
472
|
+
path: declaration.path,
|
|
473
|
+
id: declaration.id,
|
|
474
|
+
message: lenient ? `'${declaration.id}' (${declaration.kind}) is not filled yet.` : `Required variable '${declaration.id}' (${declaration.kind}) has no value. Provide one, or mark the document as a template ("template": true).`
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
context.effective.set(declaration.id, effective);
|
|
479
|
+
}
|
|
480
|
+
return { context, declarations, unfilled, examplesUsed, providedColors };
|
|
481
|
+
}
|
|
482
|
+
function rebuildVariables(presentation, built, keepAll) {
|
|
483
|
+
const source = presentation.variables;
|
|
484
|
+
if (!isRecord(source)) return void 0;
|
|
485
|
+
const out = [];
|
|
486
|
+
for (const [id, entry] of Object.entries(source)) {
|
|
487
|
+
const effective = built.context.effective.get(id);
|
|
488
|
+
if (!effective) {
|
|
489
|
+
out.push([id, entry]);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const { declaration } = effective;
|
|
493
|
+
if (declaration.kind !== "color") {
|
|
494
|
+
if (keepAll || effective.status === "unfilled" && declaration.required) out.push([id, entry]);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
const color = built.providedColors.get(id);
|
|
498
|
+
if (color === void 0) {
|
|
499
|
+
out.push([id, entry]);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const base = isRecord(entry) ? entry : {};
|
|
503
|
+
const { example: _example, required: _required, label: _label, ...rest } = base;
|
|
504
|
+
out.push([id, { ...rest, type: "color", value: color }]);
|
|
505
|
+
}
|
|
506
|
+
return out.length ? Object.fromEntries(out) : void 0;
|
|
507
|
+
}
|
|
508
|
+
function finish(presentation, built, options) {
|
|
509
|
+
const { context } = built;
|
|
510
|
+
const complete = built.unfilled.length === 0;
|
|
511
|
+
context.unescape = complete && context.content && !context.shape;
|
|
512
|
+
const walked = context.content ? walk(presentation, "", false, context) : presentation;
|
|
513
|
+
const result = { ...walked };
|
|
514
|
+
const variables = rebuildVariables(presentation, built, context.shape);
|
|
515
|
+
if (variables) result.variables = variables;
|
|
516
|
+
else delete result.variables;
|
|
517
|
+
if (!context.shape && complete && hasOwn(result, "template")) delete result.template;
|
|
518
|
+
if (context.content) {
|
|
519
|
+
const used = new Set(context.uses.map((use) => use.id));
|
|
520
|
+
for (const declaration of built.declarations) {
|
|
521
|
+
if (declaration.kind !== "color" && !used.has(declaration.id)) {
|
|
522
|
+
context.diagnostics.push({ code: "variable-unused", severity: "warning", path: declaration.path, id: declaration.id, message: `Variable '${declaration.id}' is declared but never used; reference it as '{{${declaration.id}}}' or 'var:${declaration.id}'.` });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const diagnostics = context.diagnostics;
|
|
527
|
+
if (options.strict && diagnostics.some((entry) => entry.severity === "error")) throw new OPFVariableError(diagnostics);
|
|
528
|
+
return { presentation: result, diagnostics, unfilled: built.unfilled, examplesUsed: built.examplesUsed, complete };
|
|
529
|
+
}
|
|
530
|
+
function resolveVariables(presentation, values = {}, options = {}) {
|
|
531
|
+
if (!isRecord(presentation)) throw new TypeError("resolveVariables expects a presentation object.");
|
|
532
|
+
if (!isRecord(values)) throw new TypeError("resolveVariables values must be an object keyed by variable id.");
|
|
533
|
+
const built = plan(presentation, values, options);
|
|
534
|
+
if (!built.context.content && built.providedColors.size === 0 && built.context.diagnostics.length === 0) {
|
|
535
|
+
return { presentation, diagnostics: [], unfilled: built.unfilled, examplesUsed: [], complete: built.unfilled.length === 0 };
|
|
536
|
+
}
|
|
537
|
+
return finish(presentation, built, options);
|
|
538
|
+
}
|
|
539
|
+
function instantiateForValidation(presentation, values = {}, template) {
|
|
540
|
+
const options = { template, shape: true, partial: true };
|
|
541
|
+
return finish(presentation, plan(presentation, values, options), options);
|
|
542
|
+
}
|
|
543
|
+
function listVariables(presentation, values = {}) {
|
|
544
|
+
if (!isRecord(presentation)) return [];
|
|
545
|
+
const built = plan(presentation, isRecord(values) ? values : {}, { examples: false, partial: true });
|
|
546
|
+
built.context.shape = true;
|
|
547
|
+
built.context.content = true;
|
|
548
|
+
walk(presentation, "", false, built.context);
|
|
549
|
+
return built.declarations.map((declaration) => {
|
|
550
|
+
const effective = built.context.effective.get(declaration.id);
|
|
551
|
+
return {
|
|
552
|
+
...declaration,
|
|
553
|
+
...effective?.status === "filled" && effective.provided ? { value: effective.value } : {},
|
|
554
|
+
filled: effective?.status === "filled",
|
|
555
|
+
uses: built.context.uses.filter((use) => use.id === declaration.id)
|
|
556
|
+
};
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/validator.ts
|
|
561
|
+
var OPFValidationError = class extends Error {
|
|
562
|
+
issues;
|
|
563
|
+
result;
|
|
564
|
+
constructor(result) {
|
|
565
|
+
const first = result.errors[0];
|
|
566
|
+
super(first ? `OPF validation failed at ${first.path}: ${first.message}` : "OPF validation failed");
|
|
567
|
+
this.name = "OPFValidationError";
|
|
568
|
+
this.issues = result.errors;
|
|
569
|
+
this.result = result;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
var schemaNameByCatalogKind = catalogSchemaNames;
|
|
573
|
+
var ajv;
|
|
574
|
+
var dynamicSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
575
|
+
var promotedRegionKeySet = new Set(promotedRegionKeys);
|
|
576
|
+
var rootPayloadFields = [
|
|
577
|
+
"type",
|
|
578
|
+
"text",
|
|
579
|
+
"items",
|
|
580
|
+
"bullets",
|
|
581
|
+
"image",
|
|
582
|
+
"video",
|
|
583
|
+
"chart",
|
|
584
|
+
"table",
|
|
585
|
+
"code",
|
|
586
|
+
"metric",
|
|
587
|
+
"quote",
|
|
588
|
+
"timeline",
|
|
589
|
+
"blocks"
|
|
590
|
+
];
|
|
591
|
+
var contentKindSpecs = {
|
|
592
|
+
text: {
|
|
593
|
+
fields: ["text", "bullets"],
|
|
594
|
+
required: [],
|
|
595
|
+
requireAny: ["text", "bullets"]
|
|
596
|
+
},
|
|
597
|
+
list: {
|
|
598
|
+
fields: ["items"],
|
|
599
|
+
required: ["items"]
|
|
600
|
+
},
|
|
601
|
+
image: {
|
|
602
|
+
fields: ["image"],
|
|
603
|
+
required: ["image"]
|
|
604
|
+
},
|
|
605
|
+
chart: {
|
|
606
|
+
fields: ["chart"],
|
|
607
|
+
required: ["chart"]
|
|
608
|
+
},
|
|
609
|
+
table: {
|
|
610
|
+
fields: ["table"],
|
|
611
|
+
required: ["table"]
|
|
612
|
+
},
|
|
613
|
+
video: {
|
|
614
|
+
fields: ["video"],
|
|
615
|
+
required: ["video"]
|
|
616
|
+
},
|
|
617
|
+
code: {
|
|
618
|
+
fields: ["code"],
|
|
619
|
+
required: ["code"]
|
|
620
|
+
},
|
|
621
|
+
metric: {
|
|
622
|
+
fields: ["metric"],
|
|
623
|
+
required: ["metric"]
|
|
624
|
+
},
|
|
625
|
+
quote: {
|
|
626
|
+
fields: ["quote"],
|
|
627
|
+
required: ["quote"]
|
|
628
|
+
},
|
|
629
|
+
timeline: {
|
|
630
|
+
fields: ["timeline"],
|
|
631
|
+
required: ["timeline"]
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
var captionableKinds = /* @__PURE__ */ new Set(["image", "chart", "table", "video"]);
|
|
635
|
+
var columnSpans = {
|
|
636
|
+
left: [0],
|
|
637
|
+
center: [1],
|
|
638
|
+
right: [2],
|
|
639
|
+
"left+center": [0, 1],
|
|
640
|
+
"center+right": [1, 2],
|
|
641
|
+
"left+center+right": [0, 1, 2]
|
|
642
|
+
};
|
|
643
|
+
var rowSpans = {
|
|
644
|
+
top: [0],
|
|
645
|
+
middle: [1],
|
|
646
|
+
bottom: [2],
|
|
647
|
+
"top+middle": [0, 1],
|
|
648
|
+
"middle+bottom": [1, 2],
|
|
649
|
+
"top+middle+bottom": [0, 1, 2]
|
|
650
|
+
};
|
|
651
|
+
function getAjv() {
|
|
652
|
+
if (ajv) {
|
|
653
|
+
return ajv;
|
|
654
|
+
}
|
|
655
|
+
const instance = new Ajv2020({
|
|
656
|
+
allErrors: true,
|
|
657
|
+
verbose: true,
|
|
658
|
+
strict: false,
|
|
659
|
+
allowUnionTypes: true
|
|
660
|
+
});
|
|
661
|
+
addFormats(instance);
|
|
662
|
+
for (const schema of Object.values(schemas)) {
|
|
663
|
+
instance.addSchema(schema);
|
|
664
|
+
}
|
|
665
|
+
ajv = instance;
|
|
666
|
+
return instance;
|
|
667
|
+
}
|
|
668
|
+
function isSchemaName(value) {
|
|
669
|
+
return typeof value === "string" && value in schemas;
|
|
670
|
+
}
|
|
671
|
+
function isCatalogKind(value) {
|
|
672
|
+
return typeof value === "string" && value in schemaNameByCatalogKind;
|
|
673
|
+
}
|
|
674
|
+
function resolveValidator(schemaOrKind) {
|
|
675
|
+
const instance = getAjv();
|
|
676
|
+
if (isSchemaName(schemaOrKind)) {
|
|
677
|
+
const schema = schemas[schemaOrKind];
|
|
678
|
+
const validator2 = instance.getSchema(schema.$id) ?? instance.compile(schema);
|
|
679
|
+
return { validate: validator2, schemaName: schemaOrKind };
|
|
680
|
+
}
|
|
681
|
+
if (isCatalogKind(schemaOrKind)) {
|
|
682
|
+
const schemaName = schemaNameByCatalogKind[schemaOrKind];
|
|
683
|
+
const schema = schemas[schemaName];
|
|
684
|
+
const validator2 = instance.getSchema(schema.$id) ?? instance.compile(schema);
|
|
685
|
+
return { validate: validator2, schemaName, catalogKind: schemaOrKind };
|
|
686
|
+
}
|
|
687
|
+
const cached = dynamicSchemaCache.get(schemaOrKind);
|
|
688
|
+
if (cached) {
|
|
689
|
+
return { validate: cached };
|
|
690
|
+
}
|
|
691
|
+
const validator = instance.compile(schemaOrKind);
|
|
692
|
+
dynamicSchemaCache.set(schemaOrKind, validator);
|
|
693
|
+
return { validate: validator };
|
|
694
|
+
}
|
|
695
|
+
function toIssue(error) {
|
|
696
|
+
const issue2 = {
|
|
697
|
+
path: error.instancePath || "/",
|
|
698
|
+
message: error.message ?? "failed validation",
|
|
699
|
+
keyword: error.keyword,
|
|
700
|
+
schemaPath: error.schemaPath,
|
|
701
|
+
params: error.params
|
|
702
|
+
};
|
|
703
|
+
rememberValidationDefinition(issue2, error.parentSchema, error.keyword);
|
|
704
|
+
return issue2;
|
|
705
|
+
}
|
|
706
|
+
function semanticIssue(path, message, params = {}) {
|
|
707
|
+
return {
|
|
708
|
+
path,
|
|
709
|
+
message,
|
|
710
|
+
keyword: "opf",
|
|
711
|
+
schemaPath: "#/x-opf-semantics",
|
|
712
|
+
params
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function hasOwn2(value, key) {
|
|
716
|
+
return Object.hasOwn(value, key);
|
|
717
|
+
}
|
|
718
|
+
function isEnUkTag(value) {
|
|
719
|
+
return typeof value === "string" && value.toLowerCase() === "en-uk";
|
|
720
|
+
}
|
|
721
|
+
function presentFields(value, fields) {
|
|
722
|
+
return fields.filter((field) => hasOwn2(value, field));
|
|
723
|
+
}
|
|
724
|
+
function isContentKind(value) {
|
|
725
|
+
return typeof value === "string" && value in contentKindSpecs;
|
|
726
|
+
}
|
|
727
|
+
function inferredKinds(value) {
|
|
728
|
+
const kinds = [];
|
|
729
|
+
if (presentFields(value, contentKindSpecs.text.fields).length > 0) kinds.push("text");
|
|
730
|
+
if (hasOwn2(value, "items")) kinds.push("list");
|
|
731
|
+
if (hasOwn2(value, "image")) kinds.push("image");
|
|
732
|
+
if (hasOwn2(value, "video")) kinds.push("video");
|
|
733
|
+
if (hasOwn2(value, "chart")) kinds.push("chart");
|
|
734
|
+
if (hasOwn2(value, "table")) kinds.push("table");
|
|
735
|
+
if (hasOwn2(value, "code")) kinds.push("code");
|
|
736
|
+
if (hasOwn2(value, "metric")) kinds.push("metric");
|
|
737
|
+
if (hasOwn2(value, "quote")) kinds.push("quote");
|
|
738
|
+
if (hasOwn2(value, "timeline")) kinds.push("timeline");
|
|
739
|
+
return kinds;
|
|
740
|
+
}
|
|
741
|
+
function isImplicitBlocksComposition(value, inferred, options) {
|
|
742
|
+
if (options.slideRoot !== true || hasOwn2(value, "blocks")) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
return inferred.length > 1;
|
|
746
|
+
}
|
|
747
|
+
function validateContentPayload(value, path, options = {}) {
|
|
748
|
+
const issues = [];
|
|
749
|
+
const explicitType = value.type;
|
|
750
|
+
const payloadFields = presentFields(value, rootPayloadFields);
|
|
751
|
+
const hasBlocks = hasOwn2(value, "blocks");
|
|
752
|
+
const captionIssue = (reason, params = {}) => semanticIssue(pathFor(path, "caption"), `caption is only valid on an image, chart, table or video payload; ${reason}`, { code: "caption-unsupported-payload", ...params });
|
|
753
|
+
if (hasBlocks || explicitType === "group") {
|
|
754
|
+
if (hasOwn2(value, "caption")) issues.push(captionIssue("a group cannot carry one"));
|
|
755
|
+
if (explicitType !== void 0 && explicitType !== "group") issues.push(semanticIssue(path, "a group must use type 'group' or omit type"));
|
|
756
|
+
const incompatible2 = payloadFields.filter((field) => field !== "blocks" && field !== "type");
|
|
757
|
+
if (incompatible2.length) issues.push(semanticIssue(path, "blocks cannot be mixed with leaf payload fields", { fields: incompatible2 }));
|
|
758
|
+
if (!Array.isArray(value.blocks) || value.blocks.length === 0 && !options.slideRoot) issues.push(semanticIssue(pathFor(path, "blocks"), "a group requires at least one block"));
|
|
759
|
+
if (Array.isArray(value.blocks)) value.blocks.forEach((block, index) => {
|
|
760
|
+
if (isRecord(block)) issues.push(...validateContentPayload(block, `${pathFor(path, "blocks")}/${index}`));
|
|
761
|
+
});
|
|
762
|
+
return issues;
|
|
763
|
+
}
|
|
764
|
+
if (hasOwn2(value, "composition") && !options.slideRoot) issues.push(semanticIssue(pathFor(path, "composition"), "composition is only valid on a group containing blocks"));
|
|
765
|
+
if (explicitType !== void 0 && !isContentKind(explicitType)) {
|
|
766
|
+
return issues;
|
|
767
|
+
}
|
|
768
|
+
const kind = isContentKind(explicitType) ? explicitType : void 0;
|
|
769
|
+
const inferred = kind ? [kind] : inferredKinds(value);
|
|
770
|
+
if (!kind && inferred.length === 0) {
|
|
771
|
+
if (payloadFields.length > 0 || path !== "/") {
|
|
772
|
+
issues.push(semanticIssue(path, "content payload must include concrete content fields", {
|
|
773
|
+
fields: payloadFields
|
|
774
|
+
}));
|
|
775
|
+
}
|
|
776
|
+
return issues;
|
|
777
|
+
}
|
|
778
|
+
if (!kind && inferred.length > 1) {
|
|
779
|
+
if (isImplicitBlocksComposition(value, inferred, options)) {
|
|
780
|
+
if (hasOwn2(value, "caption")) issues.push(captionIssue("a slide root with several payloads cannot carry one; put the caption on a block", { inferredTypes: inferred }));
|
|
781
|
+
return issues;
|
|
782
|
+
}
|
|
783
|
+
issues.push(semanticIssue(path, "content payload mixes fields from incompatible content types", {
|
|
784
|
+
inferredTypes: inferred
|
|
785
|
+
}));
|
|
786
|
+
return issues;
|
|
787
|
+
}
|
|
788
|
+
const resolvedKind = inferred[0];
|
|
789
|
+
if (!resolvedKind) {
|
|
790
|
+
return issues;
|
|
791
|
+
}
|
|
792
|
+
const spec = contentKindSpecs[resolvedKind];
|
|
793
|
+
if (hasOwn2(value, "caption") && !captionableKinds.has(resolvedKind)) issues.push(captionIssue(`this payload is '${resolvedKind}'`, { type: resolvedKind }));
|
|
794
|
+
const allowedFields = /* @__PURE__ */ new Set([
|
|
795
|
+
"type",
|
|
796
|
+
...spec.fields
|
|
797
|
+
]);
|
|
798
|
+
for (const required of spec.required) {
|
|
799
|
+
if (!hasOwn2(value, required)) {
|
|
800
|
+
issues.push(semanticIssue(path, `content payload type '${resolvedKind}' requires '${required}'`, {
|
|
801
|
+
type: resolvedKind,
|
|
802
|
+
required
|
|
803
|
+
}));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (spec.requireAny && presentFields(value, spec.requireAny).length === 0) {
|
|
807
|
+
issues.push(semanticIssue(path, `content payload type '${resolvedKind}' requires one of: ${spec.requireAny.join(", ")}`, {
|
|
808
|
+
type: resolvedKind,
|
|
809
|
+
requiredAny: spec.requireAny
|
|
810
|
+
}));
|
|
811
|
+
}
|
|
812
|
+
const incompatible = payloadFields.filter((field) => !allowedFields.has(field));
|
|
813
|
+
if (incompatible.length > 0) {
|
|
814
|
+
issues.push(semanticIssue(path, `content payload type '${resolvedKind}' cannot include incompatible fields: ${incompatible.join(", ")}`, {
|
|
815
|
+
type: resolvedKind,
|
|
816
|
+
incompatible
|
|
817
|
+
}));
|
|
818
|
+
}
|
|
819
|
+
return issues;
|
|
820
|
+
}
|
|
821
|
+
function slideRegion(key) {
|
|
822
|
+
if (key.includes(":")) {
|
|
823
|
+
const [rowPart, columnPart] = key.split(":");
|
|
824
|
+
if (!rowPart || !columnPart) return void 0;
|
|
825
|
+
const rows = rowSpans[rowPart];
|
|
826
|
+
const columns = columnSpans[columnPart];
|
|
827
|
+
return rows && columns ? { rows, columns } : void 0;
|
|
828
|
+
}
|
|
829
|
+
if (key in columnSpans) {
|
|
830
|
+
const columns = columnSpans[key];
|
|
831
|
+
return columns ? { rows: [0, 1, 2], columns } : void 0;
|
|
832
|
+
}
|
|
833
|
+
if (key in rowSpans) {
|
|
834
|
+
const rows = rowSpans[key];
|
|
835
|
+
return rows ? { rows, columns: [0, 1, 2] } : void 0;
|
|
836
|
+
}
|
|
837
|
+
return void 0;
|
|
838
|
+
}
|
|
839
|
+
function intersects(left, right) {
|
|
840
|
+
return left.some((value) => right.includes(value));
|
|
841
|
+
}
|
|
842
|
+
function regionsOverlap(left, right) {
|
|
843
|
+
return intersects(left.rows, right.rows) && intersects(left.columns, right.columns);
|
|
844
|
+
}
|
|
845
|
+
function validateSlideRegions(slide, slidePath) {
|
|
846
|
+
const issues = [];
|
|
847
|
+
const regionKeys = Object.keys(slide).filter((key) => promotedRegionKeySet.has(key));
|
|
848
|
+
const rootFields = presentFields(slide, rootPayloadFields);
|
|
849
|
+
if (regionKeys.length > 0 && rootFields.length > 0) {
|
|
850
|
+
issues.push(semanticIssue(slidePath, "root content payload fields cannot be mixed with promoted region keys", {
|
|
851
|
+
rootFields,
|
|
852
|
+
regionKeys
|
|
853
|
+
}));
|
|
854
|
+
}
|
|
855
|
+
if (regionKeys.length === 0 && rootFields.length > 0) {
|
|
856
|
+
issues.push(...validateContentPayload(slide, slidePath, { slideRoot: true }));
|
|
857
|
+
}
|
|
858
|
+
for (const key of regionKeys) {
|
|
859
|
+
const value = slide[key];
|
|
860
|
+
if (isRecord(value)) {
|
|
861
|
+
issues.push(...validateContentPayload(value, pathFor(slidePath, key)));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
for (let leftIndex = 0; leftIndex < regionKeys.length; leftIndex += 1) {
|
|
865
|
+
const leftKey = regionKeys[leftIndex];
|
|
866
|
+
if (!leftKey) continue;
|
|
867
|
+
const leftRegion = slideRegion(leftKey);
|
|
868
|
+
if (!leftRegion) continue;
|
|
869
|
+
for (let rightIndex = leftIndex + 1; rightIndex < regionKeys.length; rightIndex += 1) {
|
|
870
|
+
const rightKey = regionKeys[rightIndex];
|
|
871
|
+
if (!rightKey) continue;
|
|
872
|
+
const rightRegion = slideRegion(rightKey);
|
|
873
|
+
if (!rightRegion) continue;
|
|
874
|
+
if (regionsOverlap(leftRegion, rightRegion)) {
|
|
875
|
+
issues.push(semanticIssue(slidePath, `promoted region keys '${leftKey}' and '${rightKey}' overlap`, {
|
|
876
|
+
regionKeys: [leftKey, rightKey]
|
|
877
|
+
}));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return issues;
|
|
882
|
+
}
|
|
883
|
+
function validatePresentationSemantics(value) {
|
|
884
|
+
if (!isRecord(value) || !Array.isArray(value.slides)) {
|
|
885
|
+
return [];
|
|
886
|
+
}
|
|
887
|
+
const issues = [];
|
|
888
|
+
if (isEnUkTag(value.language)) {
|
|
889
|
+
issues.push(semanticIssue("/language", "Use 'en-GB' for UK English; 'en-UK' is not a valid BCP-47 region tag", {
|
|
890
|
+
replacement: "en-GB"
|
|
891
|
+
}));
|
|
892
|
+
} else if (isRecord(value.language) && isEnUkTag(value.language.bcp47)) {
|
|
893
|
+
issues.push(semanticIssue("/language/bcp47", "Use 'en-GB' for UK English; 'en-UK' is not a valid BCP-47 region tag", {
|
|
894
|
+
replacement: "en-GB"
|
|
895
|
+
}));
|
|
896
|
+
}
|
|
897
|
+
const documentIds = /* @__PURE__ */ new Map();
|
|
898
|
+
value.slides.forEach((slide, index) => {
|
|
899
|
+
if (isRecord(slide)) {
|
|
900
|
+
const slidePath = `/slides/${index}`;
|
|
901
|
+
if (typeof slide.id === "string") {
|
|
902
|
+
const seen = documentIds.get(slide.id);
|
|
903
|
+
if (seen === "slide") issues.push(semanticIssue(`${slidePath}/id`, "slide ids must be unique within a presentation", { id: slide.id }));
|
|
904
|
+
else if (seen === "payload") issues.push(semanticIssue(`${slidePath}/id`, "slide id duplicates a content payload id; ids must be unique among slide and payload ids within a presentation", { id: slide.id }));
|
|
905
|
+
else documentIds.set(slide.id, "slide");
|
|
906
|
+
}
|
|
907
|
+
visitContentPayloads(slide, slidePath, (payload, path) => {
|
|
908
|
+
if (typeof payload.id === "string") {
|
|
909
|
+
if (documentIds.has(payload.id)) issues.push(semanticIssue(`${path}/id`, "content payload ids must be unique among slide and payload ids within a presentation", { id: payload.id }));
|
|
910
|
+
else documentIds.set(payload.id, "payload");
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
issues.push(...validateSlideRegions(slide, slidePath));
|
|
914
|
+
const payloadTableIssues = (payload, path) => {
|
|
915
|
+
if (isRecord(payload.table)) for (const issue2 of tableGrid(payload.table).issues) issues.push(semanticIssue(pathFor(path, "table") + issue2.path.slice(5).replaceAll(".", "/"), issue2.message));
|
|
916
|
+
};
|
|
917
|
+
payloadTableIssues(slide, slidePath);
|
|
918
|
+
visitContentPayloads(slide, slidePath, payloadTableIssues);
|
|
919
|
+
const payloadNumberingIssues = (payload, path) => {
|
|
920
|
+
for (const finding of numberingFindings(payload).errors) issues.push(semanticIssue(pathFor(path, finding.key), finding.message, finding.params));
|
|
921
|
+
};
|
|
922
|
+
payloadNumberingIssues(slide, slidePath);
|
|
923
|
+
visitContentPayloads(slide, slidePath, payloadNumberingIssues);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
issues.push(...annotationIssues(value));
|
|
927
|
+
return issues;
|
|
928
|
+
}
|
|
929
|
+
function inlineCatalogEntry(context, kind) {
|
|
930
|
+
const catalogsField = context.document.catalogs;
|
|
931
|
+
if (!isRecord(catalogsField)) {
|
|
932
|
+
return void 0;
|
|
933
|
+
}
|
|
934
|
+
const entry = catalogsField[kind];
|
|
935
|
+
return isRecord(entry) ? entry : void 0;
|
|
936
|
+
}
|
|
937
|
+
function unknownIdWarning(kind, value, path, context) {
|
|
938
|
+
if (typeof value !== "string" || !bareIdPattern.test(value)) {
|
|
939
|
+
return void 0;
|
|
940
|
+
}
|
|
941
|
+
if (context) {
|
|
942
|
+
const entry = inlineCatalogEntry(context, kind);
|
|
943
|
+
if (entry) {
|
|
944
|
+
const inline = Array.isArray(entry.records) ? entry.records.find((record) => isRecord(record) && record.id === value) : void 0;
|
|
945
|
+
if (isRecord(inline)) {
|
|
946
|
+
const replacedBy = isRecord(inline.deprecation) ? inline.deprecation.replacedBy : void 0;
|
|
947
|
+
return typeof replacedBy === "string" ? deprecatedIdWarning(kind, value, replacedBy, path) : void 0;
|
|
948
|
+
}
|
|
949
|
+
if (typeof entry.source === "string" || Array.isArray(entry.source) && entry.source.length > 0) {
|
|
950
|
+
return void 0;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (catalogIds[kind].includes(value)) {
|
|
955
|
+
const replacedBy = deprecatedCatalogIds[`${kind}/${value}`];
|
|
956
|
+
return replacedBy === void 0 ? void 0 : deprecatedIdWarning(kind, value, replacedBy, path);
|
|
957
|
+
}
|
|
958
|
+
return semanticIssue(path, `unknown ${kind} catalog id '${value}'`, { kind, id: value });
|
|
959
|
+
}
|
|
960
|
+
function deprecatedIdWarning(kind, id, replacedBy, path) {
|
|
961
|
+
return semanticIssue(path, `deprecated ${kind} catalog id '${id}'; use '${replacedBy}'`, { kind, id, replacedBy });
|
|
962
|
+
}
|
|
963
|
+
function referenceObjectWarning(kind, value, path, context) {
|
|
964
|
+
if (typeof value === "string") {
|
|
965
|
+
return unknownIdWarning(kind, value, path, context);
|
|
966
|
+
}
|
|
967
|
+
if (isRecord(value)) {
|
|
968
|
+
return unknownIdWarning(kind, value.id, pathFor(path, "id"), context);
|
|
969
|
+
}
|
|
970
|
+
return void 0;
|
|
971
|
+
}
|
|
972
|
+
function pushIfDefined(issues, issue2) {
|
|
973
|
+
if (issue2) {
|
|
974
|
+
issues.push(issue2);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function designReferenceWarnings(design, path, context) {
|
|
978
|
+
if (!isRecord(design)) {
|
|
979
|
+
return [];
|
|
980
|
+
}
|
|
981
|
+
const issues = [];
|
|
982
|
+
pushIfDefined(issues, referenceObjectWarning("themes", design.theme, pathFor(path, "theme"), context));
|
|
983
|
+
pushIfDefined(issues, referenceObjectWarning("colorSchemes", design.colorScheme, pathFor(path, "colorScheme"), context));
|
|
984
|
+
pushIfDefined(issues, referenceObjectWarning("fontSchemes", design.fontScheme, pathFor(path, "fontScheme"), context));
|
|
985
|
+
if (isRecord(design.theme)) {
|
|
986
|
+
const themePath = pathFor(path, "theme");
|
|
987
|
+
pushIfDefined(issues, referenceObjectWarning("colorSchemes", design.theme.colorScheme, pathFor(themePath, "colorScheme"), context));
|
|
988
|
+
pushIfDefined(issues, referenceObjectWarning("fontSchemes", design.theme.fontScheme, pathFor(themePath, "fontScheme"), context));
|
|
989
|
+
}
|
|
990
|
+
return issues;
|
|
991
|
+
}
|
|
992
|
+
function chartTypeWarnings(payload, path, context) {
|
|
993
|
+
if (!isRecord(payload)) {
|
|
994
|
+
return [];
|
|
995
|
+
}
|
|
996
|
+
const issues = [];
|
|
997
|
+
if (isRecord(payload.chart)) {
|
|
998
|
+
pushIfDefined(issues, unknownIdWarning("chartTypes", payload.chart.type, `${pathFor(path, "chart")}/type`, context));
|
|
999
|
+
for (const diagnostic of resolveChartOptions(payload.chart, chartOptionTarget(payload.chart.type)).diagnostics) {
|
|
1000
|
+
issues.push(semanticIssue(diagnostic.option.split(".").reduce(pathFor, pathFor(path, "chart")), diagnostic.message, { code: diagnostic.code, option: diagnostic.option, reason: diagnostic.reason }));
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (Array.isArray(payload.blocks)) {
|
|
1004
|
+
payload.blocks.forEach((block, index) => {
|
|
1005
|
+
issues.push(...chartTypeWarnings(block, `${pathFor(path, "blocks")}/${index}`, context));
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
return issues;
|
|
1009
|
+
}
|
|
1010
|
+
var cellBorderEdges = ["top", "right", "bottom", "left"];
|
|
1011
|
+
var variableIdPattern = /^[a-z][a-z0-9-]*$/;
|
|
1012
|
+
var hexColorPattern = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
1013
|
+
var schemeColorNames = /* @__PURE__ */ new Set([
|
|
1014
|
+
"accent1",
|
|
1015
|
+
"accent2",
|
|
1016
|
+
"accent3",
|
|
1017
|
+
"accent4",
|
|
1018
|
+
"accent5",
|
|
1019
|
+
"accent6",
|
|
1020
|
+
"dark1",
|
|
1021
|
+
"dark2",
|
|
1022
|
+
"light1",
|
|
1023
|
+
"light2",
|
|
1024
|
+
"hyperlink",
|
|
1025
|
+
"followedHyperlink",
|
|
1026
|
+
"primary",
|
|
1027
|
+
"secondary",
|
|
1028
|
+
"accent",
|
|
1029
|
+
"background",
|
|
1030
|
+
"surface",
|
|
1031
|
+
"text",
|
|
1032
|
+
"textSecondary"
|
|
1033
|
+
]);
|
|
1034
|
+
function variableReferenceWarnings(value) {
|
|
1035
|
+
if (!Array.isArray(value.slides)) {
|
|
1036
|
+
return [];
|
|
1037
|
+
}
|
|
1038
|
+
const variables = isRecord(value.variables) ? value.variables : {};
|
|
1039
|
+
const issues = [];
|
|
1040
|
+
const colorRef = (entry, path) => {
|
|
1041
|
+
if (typeof entry !== "string" || !entry.startsWith("var:")) return;
|
|
1042
|
+
const id = entry.slice("var:".length);
|
|
1043
|
+
if (!variableIdPattern.test(id) || hasOwn2(variables, id)) return;
|
|
1044
|
+
issues.push(semanticIssue(path, `unknown variable '${id}'; declare it in the top-level variables map`, { id }));
|
|
1045
|
+
};
|
|
1046
|
+
const runColor = (entry, path) => {
|
|
1047
|
+
if (typeof entry !== "string") return;
|
|
1048
|
+
if (hexColorPattern.test(entry) || schemeColorNames.has(entry)) return;
|
|
1049
|
+
if (entry.startsWith("var:") && variableIdPattern.test(entry.slice("var:".length))) {
|
|
1050
|
+
colorRef(entry, path);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
issues.push(semanticIssue(
|
|
1054
|
+
path,
|
|
1055
|
+
`run color '${entry}' is not a hex color, a color-scheme name, or a 'var:' reference; renderers fall back to the theme color`,
|
|
1056
|
+
{ color: entry }
|
|
1057
|
+
));
|
|
1058
|
+
};
|
|
1059
|
+
const richText = (entry, path) => {
|
|
1060
|
+
if (!Array.isArray(entry)) return;
|
|
1061
|
+
entry.forEach((run, index) => {
|
|
1062
|
+
if (isRecord(run)) runColor(run.color, `${path}/${index}/color`);
|
|
1063
|
+
});
|
|
1064
|
+
};
|
|
1065
|
+
const cellStyle = (style, path) => {
|
|
1066
|
+
if (!isRecord(style)) return;
|
|
1067
|
+
colorRef(style.fill, pathFor(path, "fill"));
|
|
1068
|
+
colorRef(style.color, pathFor(path, "color"));
|
|
1069
|
+
if (!isRecord(style.borders)) return;
|
|
1070
|
+
const bordersPath = pathFor(path, "borders");
|
|
1071
|
+
for (const edge of cellBorderEdges) {
|
|
1072
|
+
const border = style.borders[edge];
|
|
1073
|
+
if (isRecord(border)) colorRef(border.color, `${pathFor(bordersPath, edge)}/color`);
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
const tableCell = (cell, path) => {
|
|
1077
|
+
if (Array.isArray(cell)) {
|
|
1078
|
+
richText(cell, path);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (!isRecord(cell)) return;
|
|
1082
|
+
richText(cell.value, pathFor(path, "value"));
|
|
1083
|
+
cellStyle(cell.style, pathFor(path, "style"));
|
|
1084
|
+
};
|
|
1085
|
+
const table = (node, path) => {
|
|
1086
|
+
if (!isRecord(node)) return;
|
|
1087
|
+
if (Array.isArray(node.columns)) {
|
|
1088
|
+
const columnsPath = pathFor(path, "columns");
|
|
1089
|
+
node.columns.forEach((column, index) => {
|
|
1090
|
+
tableCell(column, `${columnsPath}/${index}`);
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
if (Array.isArray(node.rows)) {
|
|
1094
|
+
const rowsPath = pathFor(path, "rows");
|
|
1095
|
+
node.rows.forEach((row, rowIndex) => {
|
|
1096
|
+
if (!Array.isArray(row)) return;
|
|
1097
|
+
row.forEach((cell, cellIndex) => {
|
|
1098
|
+
tableCell(cell, `${rowsPath}/${rowIndex}/${cellIndex}`);
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
const textEntries = (entries, path) => {
|
|
1104
|
+
if (!Array.isArray(entries)) return;
|
|
1105
|
+
entries.forEach((entry, index) => {
|
|
1106
|
+
const entryPath = `${path}/${index}`;
|
|
1107
|
+
if (Array.isArray(entry)) {
|
|
1108
|
+
richText(entry, entryPath);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (!isRecord(entry)) return;
|
|
1112
|
+
richText(entry.text, pathFor(entryPath, "text"));
|
|
1113
|
+
richText(entry.description, pathFor(entryPath, "description"));
|
|
1114
|
+
});
|
|
1115
|
+
};
|
|
1116
|
+
const payload = (node, path) => {
|
|
1117
|
+
richText(node.text, pathFor(path, "text"));
|
|
1118
|
+
textEntries(node.items, pathFor(path, "items"));
|
|
1119
|
+
textEntries(node.bullets, pathFor(path, "bullets"));
|
|
1120
|
+
table(node.table, pathFor(path, "table"));
|
|
1121
|
+
};
|
|
1122
|
+
value.slides.forEach((slide, index) => {
|
|
1123
|
+
if (!isRecord(slide)) return;
|
|
1124
|
+
const slidePath = `/slides/${index}`;
|
|
1125
|
+
payload(slide, slidePath);
|
|
1126
|
+
visitContentPayloads(slide, slidePath, payload);
|
|
1127
|
+
});
|
|
1128
|
+
return issues;
|
|
1129
|
+
}
|
|
1130
|
+
function presentationReferenceWarnings(value) {
|
|
1131
|
+
if (!isRecord(value) || !Array.isArray(value.slides)) {
|
|
1132
|
+
return [];
|
|
1133
|
+
}
|
|
1134
|
+
const context = { document: value };
|
|
1135
|
+
const issues = [];
|
|
1136
|
+
if (typeof value.narrative === "string") {
|
|
1137
|
+
pushIfDefined(issues, unknownIdWarning("narratives", value.narrative, "/narrative", context));
|
|
1138
|
+
}
|
|
1139
|
+
if (Array.isArray(value.audience)) {
|
|
1140
|
+
value.audience.forEach((entry, index) => {
|
|
1141
|
+
pushIfDefined(issues, referenceObjectWarning("audiences", entry, `/audience/${index}`, context));
|
|
1142
|
+
});
|
|
1143
|
+
} else {
|
|
1144
|
+
pushIfDefined(issues, unknownIdWarning("audiences", value.audience, "/audience", context));
|
|
1145
|
+
}
|
|
1146
|
+
issues.push(...designReferenceWarnings(value.design, "/design", context));
|
|
1147
|
+
issues.push(...variableReferenceWarnings(value));
|
|
1148
|
+
value.slides.forEach((slide, index) => {
|
|
1149
|
+
if (!isRecord(slide)) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const slidePath = `/slides/${index}`;
|
|
1153
|
+
issues.push(...designReferenceWarnings(slide.design, pathFor(slidePath, "design"), context));
|
|
1154
|
+
issues.push(...chartTypeWarnings(slide, slidePath, context));
|
|
1155
|
+
const numberingWarnings = (payload, path) => {
|
|
1156
|
+
for (const finding of numberingFindings(payload).warnings) issues.push(semanticIssue(pathFor(path, finding.key), finding.message, finding.params));
|
|
1157
|
+
};
|
|
1158
|
+
numberingWarnings(slide, slidePath);
|
|
1159
|
+
visitContentPayloads(slide, slidePath, numberingWarnings);
|
|
1160
|
+
for (const key of Object.keys(slide)) {
|
|
1161
|
+
if (promotedRegionKeySet.has(key)) {
|
|
1162
|
+
issues.push(...chartTypeWarnings(slide[key], pathFor(slidePath, key), context));
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
return issues;
|
|
1167
|
+
}
|
|
1168
|
+
var catalogCrossLinkFields = {
|
|
1169
|
+
audience: { recommendedNarratives: "narratives", recommendedTones: "tones" },
|
|
1170
|
+
purpose: { recommendedNarratives: "narratives", recommendedTones: "tones" },
|
|
1171
|
+
tone: { recommendedNarratives: "narratives" }
|
|
1172
|
+
};
|
|
1173
|
+
function catalogCrossLinkWarnings(schemaName, value) {
|
|
1174
|
+
const fields = catalogCrossLinkFields[schemaName];
|
|
1175
|
+
if (!fields || !isRecord(value)) {
|
|
1176
|
+
return [];
|
|
1177
|
+
}
|
|
1178
|
+
const issues = [];
|
|
1179
|
+
for (const [field, kind] of Object.entries(fields)) {
|
|
1180
|
+
const links = value[field];
|
|
1181
|
+
if (!Array.isArray(links)) {
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
links.forEach((link, index) => {
|
|
1185
|
+
pushIfDefined(issues, unknownIdWarning(kind, link, `${pathFor("/", field)}/${index}`));
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
return issues;
|
|
1189
|
+
}
|
|
1190
|
+
function validateLanguageSemantics(value) {
|
|
1191
|
+
if (!isRecord(value)) {
|
|
1192
|
+
return [];
|
|
1193
|
+
}
|
|
1194
|
+
if (isEnUkTag(value.bcp47)) {
|
|
1195
|
+
return [semanticIssue("/bcp47", "Use 'en-GB' for UK English; 'en-UK' is not a valid BCP-47 region tag", {
|
|
1196
|
+
replacement: "en-GB"
|
|
1197
|
+
})];
|
|
1198
|
+
}
|
|
1199
|
+
return [];
|
|
1200
|
+
}
|
|
1201
|
+
function contentDepthIssues(value) {
|
|
1202
|
+
if (!isRecord(value) || !Array.isArray(value.slides)) return [];
|
|
1203
|
+
const stack = [];
|
|
1204
|
+
value.slides.forEach((slide, index) => {
|
|
1205
|
+
if (!isRecord(slide)) return;
|
|
1206
|
+
if (Array.isArray(slide.blocks)) slide.blocks.forEach((block, i) => {
|
|
1207
|
+
stack.push({ value: block, path: `/slides/${index}/blocks/${i}`, depth: 0, ancestors: [] });
|
|
1208
|
+
});
|
|
1209
|
+
for (const key of promotedRegionKeys) if (hasOwn2(slide, key)) stack.push({ value: slide[key], path: `/slides/${index}/${key}`, depth: 0, ancestors: [] });
|
|
1210
|
+
});
|
|
1211
|
+
while (stack.length) {
|
|
1212
|
+
const entry = stack.pop();
|
|
1213
|
+
if (!isRecord(entry.value) || !Array.isArray(entry.value.blocks)) continue;
|
|
1214
|
+
if (entry.ancestors.includes(entry.value) || entry.depth >= MAX_COMPOSITION_DEPTH) return [semanticIssue(entry.path, `content groups must be acyclic and nest at most ${MAX_COMPOSITION_DEPTH} levels`)];
|
|
1215
|
+
const ancestors = [...entry.ancestors, entry.value];
|
|
1216
|
+
entry.value.blocks.forEach((block, i) => {
|
|
1217
|
+
stack.push({ value: block, path: `${entry.path}/blocks/${i}`, depth: entry.depth + 1, ancestors });
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
return [];
|
|
1221
|
+
}
|
|
1222
|
+
var variableTypeList = VARIABLE_KINDS.join(", ");
|
|
1223
|
+
function variableIssue(id, suffix, keyword, message, params) {
|
|
1224
|
+
return {
|
|
1225
|
+
instancePath: `/variables/${id.replaceAll("~", "~0").replaceAll("/", "~1")}${suffix}`,
|
|
1226
|
+
schemaPath: "#/$defs/Variable/oneOf",
|
|
1227
|
+
keyword,
|
|
1228
|
+
params,
|
|
1229
|
+
message
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
function typedVariableErrors(errors, value) {
|
|
1233
|
+
if (!isRecord(value) || !isRecord(value.variables)) return errors;
|
|
1234
|
+
if (!errors.some((error) => error.instancePath.startsWith("/variables/"))) return errors;
|
|
1235
|
+
const variables = value.variables;
|
|
1236
|
+
const kept = errors.filter((error) => !error.instancePath.startsWith("/variables/"));
|
|
1237
|
+
const schemaId = schemas.presentation.$id;
|
|
1238
|
+
for (const [id, entry] of Object.entries(variables)) {
|
|
1239
|
+
if (typeof entry === "string") {
|
|
1240
|
+
if (!hexColorPattern.test(entry)) kept.push(variableIssue(id, "", "type", "must be a hex color string or a variable object", {}));
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
if (!isRecord(entry)) {
|
|
1244
|
+
kept.push(variableIssue(id, "", "type", "must be a hex color string or a variable object", {}));
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
const kind = entry.type;
|
|
1248
|
+
if (typeof kind !== "string" || !VARIABLE_KINDS.includes(kind)) {
|
|
1249
|
+
kept.push(variableIssue(id, "/type", "enum", `must be one of: ${variableTypeList} (a hex string is shorthand for a color variable)`, { allowedValues: [...VARIABLE_KINDS] }));
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
const definition = `${kind.charAt(0).toUpperCase()}${kind.slice(1)}Variable`;
|
|
1253
|
+
const check = getAjv().getSchema(`${schemaId}#/$defs/${definition}`);
|
|
1254
|
+
if (!check || check(entry) === true) continue;
|
|
1255
|
+
for (const error of check.errors ?? []) {
|
|
1256
|
+
kept.push({ ...error, instancePath: `/variables/${id.replaceAll("~", "~0").replaceAll("/", "~1")}${error.instancePath}`, schemaPath: `#/$defs/${definition}${error.schemaPath.replace(/^#/, "")}` });
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return kept;
|
|
1260
|
+
}
|
|
1261
|
+
function validate(value, schemaOrKind = "presentation", options = {}) {
|
|
1262
|
+
const resolved = resolveValidator(schemaOrKind);
|
|
1263
|
+
if (resolved.schemaName === "presentation") {
|
|
1264
|
+
const errors2 = contentDepthIssues(value);
|
|
1265
|
+
if (errors2.length) return { valid: false, errors: errors2, warnings: [], schemaName: resolved.schemaName };
|
|
1266
|
+
}
|
|
1267
|
+
const variableIssues = { errors: [], warnings: [] };
|
|
1268
|
+
let subject = value;
|
|
1269
|
+
let template;
|
|
1270
|
+
let unfilledVariables;
|
|
1271
|
+
if (resolved.schemaName === "presentation" && isRecord(value) && (hasContentVariables(value) || options.values !== void 0 || options.template !== void 0)) {
|
|
1272
|
+
template = options.template ?? isTemplate(value);
|
|
1273
|
+
const view = instantiateForValidation(value, options.values ?? {}, template);
|
|
1274
|
+
subject = view.presentation;
|
|
1275
|
+
unfilledVariables = view.unfilled;
|
|
1276
|
+
for (const entry of view.diagnostics) {
|
|
1277
|
+
if (entry.code === "variable-example-used" || entry.code === "variable-rich-flattened" || entry.code === "variable-unfilled") continue;
|
|
1278
|
+
const issue2 = semanticIssue(entry.path, entry.message, { id: entry.id, code: entry.code });
|
|
1279
|
+
(entry.severity === "error" ? variableIssues.errors : variableIssues.warnings).push(issue2);
|
|
1280
|
+
}
|
|
1281
|
+
if (!template) {
|
|
1282
|
+
const declared = isRecord(value.variables) ? value.variables : {};
|
|
1283
|
+
for (const id of view.unfilled) {
|
|
1284
|
+
if (isRecord(declared[id]) && declared[id].value !== void 0) continue;
|
|
1285
|
+
variableIssues.errors.push(semanticIssue(`/variables/${id}`, `required variable '${id}' has no value; give it a value, fill it before use, or mark the document as a template ("template": true)`, { id, code: "variable-unfilled" }));
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
const valid = resolved.validate(subject) === true;
|
|
1290
|
+
const errors = valid ? [] : typedVariableErrors(resolved.validate.errors ?? [], subject).map(toIssue);
|
|
1291
|
+
const warnings = [];
|
|
1292
|
+
if (resolved.schemaName === "presentation") {
|
|
1293
|
+
errors.push(...validatePresentationSemantics(subject));
|
|
1294
|
+
errors.push(...variableIssues.errors);
|
|
1295
|
+
warnings.push(...presentationReferenceWarnings(subject));
|
|
1296
|
+
warnings.push(...variableIssues.warnings);
|
|
1297
|
+
} else if (resolved.schemaName === "language") {
|
|
1298
|
+
errors.push(...validateLanguageSemantics(value));
|
|
1299
|
+
}
|
|
1300
|
+
if (resolved.schemaName) {
|
|
1301
|
+
warnings.push(...catalogCrossLinkWarnings(resolved.schemaName, value));
|
|
1302
|
+
}
|
|
1303
|
+
return {
|
|
1304
|
+
valid: errors.length === 0,
|
|
1305
|
+
errors,
|
|
1306
|
+
warnings,
|
|
1307
|
+
schemaName: resolved.schemaName,
|
|
1308
|
+
catalogKind: resolved.catalogKind,
|
|
1309
|
+
...template !== void 0 ? { template, unfilledVariables } : {}
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
function assertValid(value, schemaOrKind = "presentation", options = {}) {
|
|
1313
|
+
const result = validate(value, schemaOrKind, options);
|
|
1314
|
+
if (!result.valid) {
|
|
1315
|
+
throw new OPFValidationError(result);
|
|
1316
|
+
}
|
|
1317
|
+
return value;
|
|
1318
|
+
}
|
|
1319
|
+
function validatePresentation(value, options = {}) {
|
|
1320
|
+
return validate(value, "presentation", options);
|
|
1321
|
+
}
|
|
1322
|
+
function assertValidPresentation(value, options = {}) {
|
|
1323
|
+
assertValid(value, "presentation", options);
|
|
1324
|
+
}
|
|
1325
|
+
function validateCatalogRecord(kind, value) {
|
|
1326
|
+
return validate(value, kind);
|
|
1327
|
+
}
|
|
1328
|
+
function assertValidCatalogRecord(kind, value) {
|
|
1329
|
+
assertValid(value, kind);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
export { DEFAULT_VARIABLE_DATE_FORMAT, OPFValidationError, OPFVariableError, VARIABLE_KINDS, assertValid, assertValidCatalogRecord, assertValidPresentation, coerceVariableValue, formatVariableNumber, hasContentVariables, isTemplate, listVariables, resolveVariables, unusedReferenceWarnings, validate, validateCatalogRecord, validatePresentation, validationDefinition, variableDeclarations };
|