@broadpaper/core 0.2.0 → 0.3.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 +62 -0
- package/dist/index.d.ts +259 -1
- package/dist/index.js +760 -70
- package/dist/index.js.map +1 -1
- package/package.json +21 -1
package/dist/index.js
CHANGED
|
@@ -879,22 +879,22 @@ var Scope = class _Scope {
|
|
|
879
879
|
}
|
|
880
880
|
};
|
|
881
881
|
var FUNCTIONS = {
|
|
882
|
-
sum: ([
|
|
883
|
-
count: ([
|
|
884
|
-
avg: ([
|
|
885
|
-
const ns = numbers(
|
|
882
|
+
sum: ([list2]) => numbers(list2).reduce((a, b) => a + b, 0),
|
|
883
|
+
count: ([list2]) => Array.isArray(list2) ? list2.length : isEmptyValue(list2) ? 0 : 1,
|
|
884
|
+
avg: ([list2]) => {
|
|
885
|
+
const ns = numbers(list2);
|
|
886
886
|
return ns.length ? ns.reduce((a, b) => a + b, 0) / ns.length : null;
|
|
887
887
|
},
|
|
888
|
-
min: ([
|
|
889
|
-
const ns = rest.length ? numbers([
|
|
888
|
+
min: ([list2, ...rest]) => {
|
|
889
|
+
const ns = rest.length ? numbers([list2, ...rest]) : numbers(list2);
|
|
890
890
|
return ns.length ? Math.min(...ns) : null;
|
|
891
891
|
},
|
|
892
|
-
max: ([
|
|
893
|
-
const ns = rest.length ? numbers([
|
|
892
|
+
max: ([list2, ...rest]) => {
|
|
893
|
+
const ns = rest.length ? numbers([list2, ...rest]) : numbers(list2);
|
|
894
894
|
return ns.length ? Math.max(...ns) : null;
|
|
895
895
|
},
|
|
896
|
-
first: ([
|
|
897
|
-
last: ([
|
|
896
|
+
first: ([list2]) => Array.isArray(list2) ? list2[0] : list2,
|
|
897
|
+
last: ([list2]) => Array.isArray(list2) ? list2[list2.length - 1] : list2,
|
|
898
898
|
length: ([v]) => Array.isArray(v) ? v.length : toDisplayString(v).length,
|
|
899
899
|
coalesce: (args) => args.find((a) => !isEmptyValue(a)) ?? null,
|
|
900
900
|
isEmpty: ([v]) => isEmptyValue(v),
|
|
@@ -933,13 +933,13 @@ function dateOf(v) {
|
|
|
933
933
|
}
|
|
934
934
|
return null;
|
|
935
935
|
}
|
|
936
|
-
function numbers(
|
|
937
|
-
if (!Array.isArray(
|
|
938
|
-
const n = toNumber(
|
|
936
|
+
function numbers(list2) {
|
|
937
|
+
if (!Array.isArray(list2)) {
|
|
938
|
+
const n = toNumber(list2);
|
|
939
939
|
return n === null ? [] : [n];
|
|
940
940
|
}
|
|
941
941
|
const out = [];
|
|
942
|
-
for (const v of
|
|
942
|
+
for (const v of list2) {
|
|
943
943
|
const n = toNumber(v);
|
|
944
944
|
if (n !== null) out.push(n);
|
|
945
945
|
}
|
|
@@ -1442,8 +1442,8 @@ function defaultDocumentStyles() {
|
|
|
1442
1442
|
function isTokenRef(value) {
|
|
1443
1443
|
return typeof value === "string" && value.startsWith("$") && value.length > 1;
|
|
1444
1444
|
}
|
|
1445
|
-
function resolveToken(
|
|
1446
|
-
const path =
|
|
1445
|
+
function resolveToken(token2, theme) {
|
|
1446
|
+
const path = token2.slice(1).split(".");
|
|
1447
1447
|
const fromTheme = walk(theme, path);
|
|
1448
1448
|
if (typeof fromTheme === "string") return fromTheme;
|
|
1449
1449
|
const fromDefault = walk(DEFAULT_THEME, path);
|
|
@@ -2204,6 +2204,169 @@ function humanizeKey(key) {
|
|
|
2204
2204
|
return key.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
|
|
2205
2205
|
}
|
|
2206
2206
|
|
|
2207
|
+
// src/data/pipes.ts
|
|
2208
|
+
function defaultPipeFor(schema) {
|
|
2209
|
+
if (!schema) return null;
|
|
2210
|
+
switch (schema.type) {
|
|
2211
|
+
case "currency": {
|
|
2212
|
+
const code = schema.currency?.trim().toUpperCase();
|
|
2213
|
+
return code ? `currency:'${code}'` : "currency";
|
|
2214
|
+
}
|
|
2215
|
+
case "percent":
|
|
2216
|
+
return "percent";
|
|
2217
|
+
case "number":
|
|
2218
|
+
return "number";
|
|
2219
|
+
case "date":
|
|
2220
|
+
return schema.format ? `date:'${escapePattern(schema.format)}'` : "date";
|
|
2221
|
+
case "datetime":
|
|
2222
|
+
return schema.format ? `datetime:'${escapePattern(schema.format)}'` : "datetime";
|
|
2223
|
+
default:
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
function withDefaultPipe(expr, schema) {
|
|
2228
|
+
const trimmed = expr.trim();
|
|
2229
|
+
if (!trimmed || hasPipe(trimmed)) return trimmed;
|
|
2230
|
+
if (!isPlainPath(trimmed)) return trimmed;
|
|
2231
|
+
const pipe = defaultPipeFor(schema);
|
|
2232
|
+
return pipe ? `${trimmed} | ${pipe}` : trimmed;
|
|
2233
|
+
}
|
|
2234
|
+
function hasPipe(expr) {
|
|
2235
|
+
for (let i = 0; i < expr.length; i++) {
|
|
2236
|
+
if (expr[i] !== "|") continue;
|
|
2237
|
+
if (expr[i + 1] === "|") {
|
|
2238
|
+
i++;
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2241
|
+
if (expr[i - 1] === "|") continue;
|
|
2242
|
+
return true;
|
|
2243
|
+
}
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
function isPlainPath(expr) {
|
|
2247
|
+
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[\d+\])*$/.test(expr.trim());
|
|
2248
|
+
}
|
|
2249
|
+
function escapePattern(pattern) {
|
|
2250
|
+
return pattern.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// src/data/calculation.ts
|
|
2254
|
+
function calcPaths(calc) {
|
|
2255
|
+
const out = [];
|
|
2256
|
+
const add = (term) => {
|
|
2257
|
+
if (term.kind === "field" || term.kind === "aggregate") out.push(term.path);
|
|
2258
|
+
};
|
|
2259
|
+
add(calc.start);
|
|
2260
|
+
for (const step of calc.steps) add(step.term);
|
|
2261
|
+
return out;
|
|
2262
|
+
}
|
|
2263
|
+
function termAst(term) {
|
|
2264
|
+
if (term.kind === "number") return { kind: "literal", value: term.value };
|
|
2265
|
+
const path = { kind: "path", segments: term.path.split(".").map((name) => ({ kind: "prop", name })) };
|
|
2266
|
+
return term.kind === "aggregate" ? { kind: "call", name: term.fn, args: [path] } : path;
|
|
2267
|
+
}
|
|
2268
|
+
function safe(term) {
|
|
2269
|
+
if (term.kind === "number") return termAst(term);
|
|
2270
|
+
return { kind: "call", name: "coalesce", args: [termAst(term), { kind: "literal", value: 0 }] };
|
|
2271
|
+
}
|
|
2272
|
+
function applyStep(left, step) {
|
|
2273
|
+
switch (step.op) {
|
|
2274
|
+
case "add":
|
|
2275
|
+
return { kind: "binary", op: "+", left, right: termAst(step.term) };
|
|
2276
|
+
case "subtract":
|
|
2277
|
+
return { kind: "binary", op: "-", left, right: termAst(step.term) };
|
|
2278
|
+
case "multiply":
|
|
2279
|
+
return { kind: "binary", op: "*", left, right: termAst(step.term) };
|
|
2280
|
+
case "divide":
|
|
2281
|
+
return { kind: "binary", op: "/", left, right: termAst(step.term) };
|
|
2282
|
+
case "addPercent":
|
|
2283
|
+
return { kind: "binary", op: "*", left, right: { kind: "binary", op: "+", left: { kind: "literal", value: 1 }, right: safe(step.term) } };
|
|
2284
|
+
case "subtractPercent":
|
|
2285
|
+
return { kind: "binary", op: "*", left, right: { kind: "binary", op: "-", left: { kind: "literal", value: 1 }, right: safe(step.term) } };
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
function calcToAst(calc) {
|
|
2289
|
+
let ast = termAst(calc.start);
|
|
2290
|
+
for (const step of calc.steps) ast = applyStep(ast, step);
|
|
2291
|
+
if (!calc.format) return ast;
|
|
2292
|
+
const piped = parseExpression(`__x__ | ${calc.format}`);
|
|
2293
|
+
return graft(piped, ast);
|
|
2294
|
+
}
|
|
2295
|
+
function graft(piped, input) {
|
|
2296
|
+
if (piped.kind === "pipe") return { ...piped, input: graft(piped.input, input) };
|
|
2297
|
+
return input;
|
|
2298
|
+
}
|
|
2299
|
+
function toExpression(calc) {
|
|
2300
|
+
return exprToString(calcToAst(calc));
|
|
2301
|
+
}
|
|
2302
|
+
var AGGREGATES = ["sum", "avg", "count", "min", "max"];
|
|
2303
|
+
function asTerm(e) {
|
|
2304
|
+
if (e.kind === "literal" && typeof e.value === "number") return { kind: "number", value: e.value };
|
|
2305
|
+
if (e.kind === "path" && e.segments.every((s) => s.kind === "prop")) {
|
|
2306
|
+
return { kind: "field", path: e.segments.map((s) => s.kind === "prop" ? s.name : "").join(".") };
|
|
2307
|
+
}
|
|
2308
|
+
if (e.kind === "call" && AGGREGATES.includes(e.name) && e.args.length === 1) {
|
|
2309
|
+
const inner = asTerm(e.args[0]);
|
|
2310
|
+
if (inner?.kind === "field") return { kind: "aggregate", fn: e.name, path: inner.path };
|
|
2311
|
+
}
|
|
2312
|
+
return null;
|
|
2313
|
+
}
|
|
2314
|
+
function asSafeTerm(e) {
|
|
2315
|
+
if (e.kind === "call" && e.name === "coalesce" && e.args.length === 2) {
|
|
2316
|
+
const fallback = e.args[1];
|
|
2317
|
+
if (fallback.kind === "literal" && fallback.value === 0) return asTerm(e.args[0]);
|
|
2318
|
+
return null;
|
|
2319
|
+
}
|
|
2320
|
+
return asTerm(e);
|
|
2321
|
+
}
|
|
2322
|
+
function asPercent(e) {
|
|
2323
|
+
if (e.kind !== "binary" || e.op !== "+" && e.op !== "-") return null;
|
|
2324
|
+
if (e.left.kind !== "literal" || e.left.value !== 1) return null;
|
|
2325
|
+
const term = asSafeTerm(e.right);
|
|
2326
|
+
if (!term) return null;
|
|
2327
|
+
return { op: e.op === "+" ? "addPercent" : "subtractPercent", term };
|
|
2328
|
+
}
|
|
2329
|
+
var OPS = { "+": "add", "-": "subtract", "*": "multiply", "/": "divide" };
|
|
2330
|
+
function fromExpression(expr) {
|
|
2331
|
+
let ast;
|
|
2332
|
+
try {
|
|
2333
|
+
ast = parseExpression(expr);
|
|
2334
|
+
} catch {
|
|
2335
|
+
return null;
|
|
2336
|
+
}
|
|
2337
|
+
const formats = [];
|
|
2338
|
+
while (ast.kind === "pipe") {
|
|
2339
|
+
formats.unshift(`${ast.name}${ast.args.length ? ":" + ast.args.map(exprToString).join(":") : ""}`);
|
|
2340
|
+
ast = ast.input;
|
|
2341
|
+
}
|
|
2342
|
+
const format = formats.length ? formats.join(" | ") : void 0;
|
|
2343
|
+
const steps = [];
|
|
2344
|
+
let cursor = ast;
|
|
2345
|
+
while (cursor.kind === "binary") {
|
|
2346
|
+
const op = OPS[cursor.op];
|
|
2347
|
+
if (!op) return null;
|
|
2348
|
+
if (op === "multiply") {
|
|
2349
|
+
const percent = asPercent(cursor.right);
|
|
2350
|
+
if (percent) {
|
|
2351
|
+
steps.unshift(percent);
|
|
2352
|
+
cursor = cursor.left;
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
const term = asTerm(cursor.right);
|
|
2357
|
+
if (!term) return null;
|
|
2358
|
+
steps.unshift({ op, term });
|
|
2359
|
+
cursor = cursor.left;
|
|
2360
|
+
}
|
|
2361
|
+
const start = asTerm(cursor);
|
|
2362
|
+
if (!start) return null;
|
|
2363
|
+
const calc = { start, steps, ...format ? { format } : {} };
|
|
2364
|
+
return toExpression(calc) === expr.trim() ? calc : null;
|
|
2365
|
+
}
|
|
2366
|
+
function isCalculation(calc) {
|
|
2367
|
+
return calc.steps.length > 0 || calc.start.kind === "aggregate";
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2207
2370
|
// src/resolve/describe.ts
|
|
2208
2371
|
function describeExpression(expr, scope) {
|
|
2209
2372
|
try {
|
|
@@ -2232,6 +2395,522 @@ function humaniseLabel(text, scope) {
|
|
|
2232
2395
|
});
|
|
2233
2396
|
}
|
|
2234
2397
|
|
|
2398
|
+
// src/diff/template-diff.ts
|
|
2399
|
+
var DISPLAY_LIMIT = 80;
|
|
2400
|
+
var SUMMARY_LIMIT = 120;
|
|
2401
|
+
var AREA_RANK = { document: 0, page: 1, styles: 2, header: 3, body: 4, footer: 5 };
|
|
2402
|
+
function diffTemplates(before, after, options = {}) {
|
|
2403
|
+
const ctx = {
|
|
2404
|
+
registry: options.registry,
|
|
2405
|
+
scope: createSchemaScope(options.dataSources ?? [])
|
|
2406
|
+
};
|
|
2407
|
+
const placed = [
|
|
2408
|
+
...documentChanges(before, after, ctx),
|
|
2409
|
+
...pageChanges(before.page, after.page),
|
|
2410
|
+
...styleChanges(before, after, ctx),
|
|
2411
|
+
...nodeChanges(before, after, ctx)
|
|
2412
|
+
];
|
|
2413
|
+
const ordered = placed.map((c, i) => ({ c, i })).sort((a, b) => AREA_RANK[a.c.area] - AREA_RANK[b.c.area] || a.c.rank - b.c.rank || a.i - b.i).map(({ c }) => strip(c));
|
|
2414
|
+
const counts = { added: 0, removed: 0, changed: 0, moved: 0 };
|
|
2415
|
+
for (const change of ordered) counts[change.kind]++;
|
|
2416
|
+
const max = Math.max(0, options.maxChanges ?? 200);
|
|
2417
|
+
return {
|
|
2418
|
+
changes: ordered.slice(0, max),
|
|
2419
|
+
counts,
|
|
2420
|
+
truncated: Math.max(0, ordered.length - max),
|
|
2421
|
+
identical: ordered.length === 0
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
function summariseDiff(diff) {
|
|
2425
|
+
const total = diff.counts.added + diff.counts.removed + diff.counts.changed + diff.counts.moved;
|
|
2426
|
+
if (total === 0) return "No changes";
|
|
2427
|
+
if (total === 1) {
|
|
2428
|
+
const only = diff.changes[0];
|
|
2429
|
+
return clip(only ? `1 change: ${only.summary}` : "1 change", SUMMARY_LIMIT);
|
|
2430
|
+
}
|
|
2431
|
+
const parts = ["added", "changed", "moved", "removed"].filter((k) => diff.counts[k] > 0).map((k) => `${diff.counts[k]} ${k}`);
|
|
2432
|
+
return clip(`${total} changes: ${parts.join(", ")}`, SUMMARY_LIMIT);
|
|
2433
|
+
}
|
|
2434
|
+
function strip(change) {
|
|
2435
|
+
const { rank, ...rest } = change;
|
|
2436
|
+
void rank;
|
|
2437
|
+
const out = { kind: rest.kind, area: rest.area, subject: rest.subject, summary: rest.summary };
|
|
2438
|
+
if (rest.nodeId !== void 0) out.nodeId = rest.nodeId;
|
|
2439
|
+
if (rest.before !== void 0) out.before = clip(rest.before, DISPLAY_LIMIT);
|
|
2440
|
+
if (rest.after !== void 0) out.after = clip(rest.after, DISPLAY_LIMIT);
|
|
2441
|
+
return out;
|
|
2442
|
+
}
|
|
2443
|
+
function documentChanges(before, after, ctx) {
|
|
2444
|
+
void ctx;
|
|
2445
|
+
const out = [];
|
|
2446
|
+
if (before.name !== after.name) {
|
|
2447
|
+
out.push({ kind: "changed", area: "document", subject: "Design name", summary: "Renamed the design", before: before.name, after: after.name, rank: 0 });
|
|
2448
|
+
}
|
|
2449
|
+
const settings = [
|
|
2450
|
+
["locale", "Language and region", "Changed the language and region"],
|
|
2451
|
+
["currency", "Currency", "Changed the currency"],
|
|
2452
|
+
["timeZone", "Time zone", "Changed the time zone"]
|
|
2453
|
+
];
|
|
2454
|
+
settings.forEach(([key, subject, summary], i) => {
|
|
2455
|
+
const a = before.settings?.[key];
|
|
2456
|
+
const b = after.settings?.[key];
|
|
2457
|
+
if (a === b) return;
|
|
2458
|
+
out.push({ kind: "changed", area: "document", subject, summary, before: a === void 0 ? "Default" : String(a), after: b === void 0 ? "Default" : String(b), rank: i + 1 });
|
|
2459
|
+
});
|
|
2460
|
+
return out;
|
|
2461
|
+
}
|
|
2462
|
+
function pageChanges(before, after) {
|
|
2463
|
+
const out = [];
|
|
2464
|
+
let rank = 0;
|
|
2465
|
+
const push = (subject, summary, a, b) => {
|
|
2466
|
+
out.push({ kind: "changed", area: "page", subject, summary, ...a !== void 0 ? { before: a } : {}, ...b !== void 0 ? { after: b } : {}, rank: rank++ });
|
|
2467
|
+
};
|
|
2468
|
+
if (!same(before.size, after.size)) push("Page size", "Changed the page size", pageSize(before.size), pageSize(after.size));
|
|
2469
|
+
if (before.orientation !== after.orientation) push("Page orientation", "Changed the page orientation", humanize(before.orientation), humanize(after.orientation));
|
|
2470
|
+
if (!same(before.margins, after.margins)) push("Page margins", "Changed the page margins", margins(before.margins), margins(after.margins));
|
|
2471
|
+
for (const region of ["header", "footer"]) {
|
|
2472
|
+
const a = before[region];
|
|
2473
|
+
const b = after[region];
|
|
2474
|
+
const name = region === "header" ? "Header" : "Footer";
|
|
2475
|
+
if (a?.height !== b?.height) push(`${name} height`, `Changed the ${region} height`, regionHeight(a?.height), regionHeight(b?.height));
|
|
2476
|
+
if (Boolean(a?.differentFirstPage) !== Boolean(b?.differentFirstPage)) {
|
|
2477
|
+
push(
|
|
2478
|
+
`Different first-page ${region}`,
|
|
2479
|
+
b?.differentFirstPage ? `Turned on a different ${region} for the first page` : `Turned off the different ${region} for the first page`,
|
|
2480
|
+
onOff(a?.differentFirstPage),
|
|
2481
|
+
onOff(b?.differentFirstPage)
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
if (before.background !== after.background) push("Page background", "Changed the page background", colour(before.background), colour(after.background));
|
|
2486
|
+
if (before.minPages !== after.minPages) {
|
|
2487
|
+
push("Empty pages shown", "Changed how many pages an empty document shows", String(before.minPages ?? 1), String(after.minPages ?? 1));
|
|
2488
|
+
}
|
|
2489
|
+
const known = /* @__PURE__ */ new Set(["size", "orientation", "margins", "header", "footer", "background", "minPages"]);
|
|
2490
|
+
const extra = keysOf(before, after).filter((k) => !known.has(k) && !same(before[k], after[k]));
|
|
2491
|
+
if (extra.length) push("Page settings", "Changed the page settings");
|
|
2492
|
+
return out;
|
|
2493
|
+
}
|
|
2494
|
+
function pageSize(size) {
|
|
2495
|
+
return typeof size === "string" ? size : `${size.width} \xD7 ${size.height} mm`;
|
|
2496
|
+
}
|
|
2497
|
+
function margins(m) {
|
|
2498
|
+
return [m.top, m.right, m.bottom, m.left].map((v) => `${v} mm`).join(" \xB7 ");
|
|
2499
|
+
}
|
|
2500
|
+
function regionHeight(height) {
|
|
2501
|
+
return !height ? "None" : `${height} mm`;
|
|
2502
|
+
}
|
|
2503
|
+
var TEXT_STYLE_WORDS = {
|
|
2504
|
+
fontFamily: "font",
|
|
2505
|
+
fontSize: "size",
|
|
2506
|
+
fontWeight: "weight",
|
|
2507
|
+
fontStyle: "slant",
|
|
2508
|
+
lineHeight: "line height",
|
|
2509
|
+
letterSpacing: "letter spacing",
|
|
2510
|
+
textTransform: "capitalisation",
|
|
2511
|
+
color: "colour",
|
|
2512
|
+
marginTop: "space above",
|
|
2513
|
+
marginBottom: "space below",
|
|
2514
|
+
keepWithNext: "keep with next"
|
|
2515
|
+
};
|
|
2516
|
+
var TABLE_WORDS = {
|
|
2517
|
+
fontSize: "text size",
|
|
2518
|
+
headerBackground: "header background",
|
|
2519
|
+
headerColor: "header text colour",
|
|
2520
|
+
headerFontWeight: "header weight",
|
|
2521
|
+
borderColor: "border colour",
|
|
2522
|
+
borderWidth: "border width",
|
|
2523
|
+
borders: "lines",
|
|
2524
|
+
zebraBackground: "stripe colour",
|
|
2525
|
+
cellPaddingX: "cell padding",
|
|
2526
|
+
cellPaddingY: "cell padding"
|
|
2527
|
+
};
|
|
2528
|
+
var CARD_WORDS = {
|
|
2529
|
+
background: "background",
|
|
2530
|
+
borderColor: "border colour",
|
|
2531
|
+
borderWidth: "border width",
|
|
2532
|
+
borderRadius: "corner radius",
|
|
2533
|
+
padding: "padding",
|
|
2534
|
+
shadow: "shadow"
|
|
2535
|
+
};
|
|
2536
|
+
var LINK_WORDS = { color: "colour", underline: "underline" };
|
|
2537
|
+
var SPACING_WORDS = {
|
|
2538
|
+
blockGap: "space between blocks",
|
|
2539
|
+
sectionPadding: "section padding",
|
|
2540
|
+
columnGap: "space between columns"
|
|
2541
|
+
};
|
|
2542
|
+
function styleChanges(before, after, ctx) {
|
|
2543
|
+
void ctx;
|
|
2544
|
+
const out = [];
|
|
2545
|
+
let rank = 0;
|
|
2546
|
+
const a = before.styles ?? {};
|
|
2547
|
+
const b = after.styles ?? {};
|
|
2548
|
+
const beforeText = a.text ?? {};
|
|
2549
|
+
const afterText = b.text ?? {};
|
|
2550
|
+
const ids = [...Object.keys(afterText), ...Object.keys(beforeText).filter((id) => !(id in afterText))];
|
|
2551
|
+
for (const id of ids) {
|
|
2552
|
+
const label = textStyleLabel(id);
|
|
2553
|
+
const subject = `${label} style`;
|
|
2554
|
+
if (!(id in beforeText)) {
|
|
2555
|
+
out.push({ kind: "added", area: "styles", subject, summary: `Added the ${label} style`, rank: rank++ });
|
|
2556
|
+
} else if (!(id in afterText)) {
|
|
2557
|
+
out.push({ kind: "removed", area: "styles", subject, summary: `Removed the ${label} style`, rank: rank++ });
|
|
2558
|
+
} else {
|
|
2559
|
+
const change = groupChange(beforeText[id], afterText[id], TEXT_STYLE_WORDS, (words) => `Changed the ${words} of the ${label} style`, subject);
|
|
2560
|
+
if (change) out.push({ ...change, rank: rank++ });
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
const groups = [
|
|
2564
|
+
["table", TABLE_WORDS, "Table style", (w) => `Changed the ${w} of tables`],
|
|
2565
|
+
["card", CARD_WORDS, "Card style", (w) => `Changed the ${w} of cards`],
|
|
2566
|
+
["link", LINK_WORDS, "Link style", (w) => `Changed the ${w} of links`],
|
|
2567
|
+
["spacing", SPACING_WORDS, "Spacing", (w) => `Changed the ${w}`]
|
|
2568
|
+
];
|
|
2569
|
+
for (const [key, words, subject, sentence] of groups) {
|
|
2570
|
+
const change = groupChange(
|
|
2571
|
+
a[key] ?? {},
|
|
2572
|
+
b[key] ?? {},
|
|
2573
|
+
words,
|
|
2574
|
+
sentence,
|
|
2575
|
+
subject
|
|
2576
|
+
);
|
|
2577
|
+
if (change) out.push({ ...change, rank: rank++ });
|
|
2578
|
+
}
|
|
2579
|
+
const known = /* @__PURE__ */ new Set(["text", "table", "card", "link", "spacing"]);
|
|
2580
|
+
const extra = keysOf(a, b).filter((k) => !known.has(k) && !same(a[k], b[k]));
|
|
2581
|
+
if (extra.length) out.push({ kind: "changed", area: "styles", subject: "Document styles", summary: "Changed the document styles", rank: rank++ });
|
|
2582
|
+
return out;
|
|
2583
|
+
}
|
|
2584
|
+
function groupChange(before, after, words, sentence, subject) {
|
|
2585
|
+
const changed = keysOf(before, after).filter((k) => !same(before[k], after[k]));
|
|
2586
|
+
if (!changed.length) return null;
|
|
2587
|
+
const names = unique(changed.map((k) => words[k] ?? lowerFirst(humanize(k))));
|
|
2588
|
+
const change = { kind: "changed", area: "styles", subject, summary: sentence(list(names)) };
|
|
2589
|
+
if (changed.length === 1) {
|
|
2590
|
+
const key = changed[0];
|
|
2591
|
+
change.before = styleValue(key, before[key]);
|
|
2592
|
+
change.after = styleValue(key, after[key]);
|
|
2593
|
+
}
|
|
2594
|
+
return change;
|
|
2595
|
+
}
|
|
2596
|
+
function styleValue(key, value) {
|
|
2597
|
+
if (value === void 0 || value === null) return "Default";
|
|
2598
|
+
if (typeof value === "boolean") return onOff(value);
|
|
2599
|
+
if (typeof value === "number") return /line|weight/i.test(key) ? String(value) : `${value} px`;
|
|
2600
|
+
if (typeof value === "string") return /colou?r|background/i.test(key) ? colour(value) : value.startsWith("$") ? token(value) : humanize(value);
|
|
2601
|
+
return "Custom";
|
|
2602
|
+
}
|
|
2603
|
+
function indexTemplate(template) {
|
|
2604
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2605
|
+
const ordered = [];
|
|
2606
|
+
const children = /* @__PURE__ */ new Map();
|
|
2607
|
+
const lists = [
|
|
2608
|
+
["header", "header:default", template.header?.default],
|
|
2609
|
+
["header", "header:firstPage", template.header?.firstPage],
|
|
2610
|
+
["body", "body", template.body],
|
|
2611
|
+
["footer", "footer:default", template.footer?.default],
|
|
2612
|
+
["footer", "footer:firstPage", template.footer?.firstPage]
|
|
2613
|
+
];
|
|
2614
|
+
const walk2 = (nodes, area, parentKey, parentId) => {
|
|
2615
|
+
const ids = [];
|
|
2616
|
+
for (const node of nodes) {
|
|
2617
|
+
if (byId.has(node.id)) continue;
|
|
2618
|
+
const entry = { node, parentId, parentKey, area, order: ordered.length };
|
|
2619
|
+
byId.set(node.id, entry);
|
|
2620
|
+
ordered.push(entry);
|
|
2621
|
+
ids.push(node.id);
|
|
2622
|
+
if (node.children?.length) walk2(node.children, area, node.id, node.id);
|
|
2623
|
+
}
|
|
2624
|
+
children.set(parentKey, ids);
|
|
2625
|
+
};
|
|
2626
|
+
for (const [area, key, nodes] of lists) if (nodes) walk2(nodes, area, key, null);
|
|
2627
|
+
return { byId, ordered, children };
|
|
2628
|
+
}
|
|
2629
|
+
function nodeChanges(before, after, ctx) {
|
|
2630
|
+
const a = indexTemplate(before);
|
|
2631
|
+
const b = indexTemplate(after);
|
|
2632
|
+
const out = [];
|
|
2633
|
+
const removedRank = (entry) => {
|
|
2634
|
+
for (let i = entry.order - 1; i >= 0; i--) {
|
|
2635
|
+
const survivor = b.byId.get(a.ordered[i].node.id);
|
|
2636
|
+
if (survivor) return survivor.order + 0.5;
|
|
2637
|
+
}
|
|
2638
|
+
return -0.5;
|
|
2639
|
+
};
|
|
2640
|
+
for (const entry of a.ordered) {
|
|
2641
|
+
const id = entry.node.id;
|
|
2642
|
+
if (b.byId.has(id)) continue;
|
|
2643
|
+
if (entry.parentId && !b.byId.has(entry.parentId)) continue;
|
|
2644
|
+
out.push({
|
|
2645
|
+
kind: "removed",
|
|
2646
|
+
area: entry.area,
|
|
2647
|
+
nodeId: id,
|
|
2648
|
+
subject: subjectOf(entry, a, ctx),
|
|
2649
|
+
summary: `Removed ${refOf(entry, a, ctx, "definite")}${place(entry, a, "from", ctx)}`,
|
|
2650
|
+
rank: removedRank(entry)
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
const reordered = /* @__PURE__ */ new Set();
|
|
2654
|
+
for (const [key, afterIds] of b.children) {
|
|
2655
|
+
const beforeIds = a.children.get(key);
|
|
2656
|
+
if (!beforeIds) continue;
|
|
2657
|
+
const shared = beforeIds.filter((id) => afterIds.includes(id));
|
|
2658
|
+
if (shared.length < 2) continue;
|
|
2659
|
+
const positions = shared.map((id) => afterIds.indexOf(id));
|
|
2660
|
+
const keep = longestIncreasing(positions);
|
|
2661
|
+
shared.forEach((id, i) => {
|
|
2662
|
+
if (!keep.has(i)) reordered.add(id);
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
for (const entry of b.ordered) {
|
|
2666
|
+
const id = entry.node.id;
|
|
2667
|
+
const was = a.byId.get(id);
|
|
2668
|
+
if (!was) {
|
|
2669
|
+
if (entry.parentId && !a.byId.has(entry.parentId)) continue;
|
|
2670
|
+
out.push({
|
|
2671
|
+
kind: "added",
|
|
2672
|
+
area: entry.area,
|
|
2673
|
+
nodeId: id,
|
|
2674
|
+
subject: subjectOf(entry, b, ctx),
|
|
2675
|
+
summary: `Added ${refOf(entry, b, ctx, "indefinite")}${place(entry, b, "to", ctx)}`,
|
|
2676
|
+
rank: entry.order
|
|
2677
|
+
});
|
|
2678
|
+
continue;
|
|
2679
|
+
}
|
|
2680
|
+
if (was.parentKey !== entry.parentKey) {
|
|
2681
|
+
const from = place(was, a, "from", ctx);
|
|
2682
|
+
const to = place(entry, b, "to", ctx);
|
|
2683
|
+
const within = place(entry, b, "within", ctx);
|
|
2684
|
+
out.push({
|
|
2685
|
+
kind: "moved",
|
|
2686
|
+
area: entry.area,
|
|
2687
|
+
nodeId: id,
|
|
2688
|
+
subject: subjectOf(entry, b, ctx),
|
|
2689
|
+
// Between two columns of the same section is still "within" it: naming the
|
|
2690
|
+
// same section twice would read as a mistake.
|
|
2691
|
+
summary: `Moved ${refOf(entry, b, ctx, "definite")}${from.replace(/^ from /, "") === to.replace(/^ to /, "") ? within : `${from}${to}`}`,
|
|
2692
|
+
rank: entry.order
|
|
2693
|
+
});
|
|
2694
|
+
} else if (reordered.has(id)) {
|
|
2695
|
+
out.push({
|
|
2696
|
+
kind: "moved",
|
|
2697
|
+
area: entry.area,
|
|
2698
|
+
nodeId: id,
|
|
2699
|
+
subject: subjectOf(entry, b, ctx),
|
|
2700
|
+
summary: `Moved ${refOf(entry, b, ctx, "definite")}${place(entry, b, "within", ctx)}`,
|
|
2701
|
+
rank: entry.order
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
const changed = nodeEdit(was, a, entry, b, ctx);
|
|
2705
|
+
if (changed) out.push({ ...changed, rank: entry.order + 0.25 });
|
|
2706
|
+
}
|
|
2707
|
+
return out;
|
|
2708
|
+
}
|
|
2709
|
+
function nodeEdit(was, wasIndex, now, nowIndex, ctx) {
|
|
2710
|
+
const x = was.node;
|
|
2711
|
+
const y = now.node;
|
|
2712
|
+
const propKeys = keysOf(x.props ?? {}, y.props ?? {}).filter((k) => !same(x.props?.[k], y.props?.[k]));
|
|
2713
|
+
const typeChanged = x.type !== y.type;
|
|
2714
|
+
const styleChanged = !same(x.style, y.style);
|
|
2715
|
+
const visibilityChanged = !same(x.visibleWhen, y.visibleWhen);
|
|
2716
|
+
const flowChanged = !same(x.flow, y.flow);
|
|
2717
|
+
const nameChanged = (x.name ?? "") !== (y.name ?? "");
|
|
2718
|
+
if (!propKeys.length && !typeChanged && !styleChanged && !visibilityChanged && !flowChanged && !nameChanged) return null;
|
|
2719
|
+
const ref = refOf(now, nowIndex, ctx, "definite");
|
|
2720
|
+
const where = place(now, nowIndex, "in", ctx);
|
|
2721
|
+
const base = { kind: "changed", area: now.area, nodeId: y.id, subject: subjectOf(now, nowIndex, ctx), rank: 0 };
|
|
2722
|
+
const aspects = propKeys.length + Number(styleChanged) + Number(visibilityChanged) + Number(flowChanged) + Number(nameChanged) + Number(typeChanged);
|
|
2723
|
+
if (aspects === 1 && nameChanged) {
|
|
2724
|
+
return { ...base, summary: `Renamed ${refOf(was, wasIndex, ctx, "definite")}${place(was, wasIndex, "in", ctx)}`, before: x.name ?? "", after: y.name ?? "" };
|
|
2725
|
+
}
|
|
2726
|
+
if (aspects === 1 && styleChanged) return { ...base, summary: `Changed the look of ${ref}${where}` };
|
|
2727
|
+
if (aspects === 1 && visibilityChanged) return { ...base, summary: `Changed when ${ref}${where} appears` };
|
|
2728
|
+
if (aspects === 1 && flowChanged) return { ...base, summary: `Changed how ${ref}${where} breaks across pages` };
|
|
2729
|
+
const names = unique([
|
|
2730
|
+
...typeChanged ? ["type"] : [],
|
|
2731
|
+
...propKeys.map((k) => propLabel(y.type, k, ctx)),
|
|
2732
|
+
...styleChanged ? ["look"] : [],
|
|
2733
|
+
...visibilityChanged ? ["visibility"] : [],
|
|
2734
|
+
...flowChanged ? ["page breaks"] : [],
|
|
2735
|
+
...nameChanged ? ["name"] : []
|
|
2736
|
+
]);
|
|
2737
|
+
const summary = names.length > 3 ? `Changed ${names.length} settings of ${ref}${where}` : `Changed the ${list(names)} of ${ref}${where}`;
|
|
2738
|
+
const change = { ...base, summary };
|
|
2739
|
+
if (aspects === 1 && propKeys.length === 1) {
|
|
2740
|
+
const key = propKeys[0];
|
|
2741
|
+
const from = simpleValue(x.type, key, x.props?.[key], ctx);
|
|
2742
|
+
const to = simpleValue(y.type, key, y.props?.[key], ctx);
|
|
2743
|
+
if (from !== null && to !== null) {
|
|
2744
|
+
change.before = from;
|
|
2745
|
+
change.after = to;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return change;
|
|
2749
|
+
}
|
|
2750
|
+
var STRUCTURE_NOUNS = { section: "section", row: "row of columns", column: "column" };
|
|
2751
|
+
function blockName(type, ctx) {
|
|
2752
|
+
if (STRUCTURE_NOUNS[type]) return humanize(STRUCTURE_NOUNS[type]);
|
|
2753
|
+
const name = ctx.registry?.get(type)?.name;
|
|
2754
|
+
return name && name.trim() ? name.trim() : humanize(type.replace(/[-_.]+/g, " "));
|
|
2755
|
+
}
|
|
2756
|
+
function subjectOf(entry, index, ctx) {
|
|
2757
|
+
const node = entry.node;
|
|
2758
|
+
if (node.name?.trim()) return node.name.trim();
|
|
2759
|
+
if (node.type === "section") {
|
|
2760
|
+
const label = sectionLabel(entry, index, ctx);
|
|
2761
|
+
return label.quoted ? `Section \u201C${label.text}\u201D` : humanize(label.text);
|
|
2762
|
+
}
|
|
2763
|
+
return blockName(node.type, ctx);
|
|
2764
|
+
}
|
|
2765
|
+
function refOf(entry, index, ctx, form) {
|
|
2766
|
+
const node = entry.node;
|
|
2767
|
+
if (node.type === "section") {
|
|
2768
|
+
const label = sectionLabel(entry, index, ctx);
|
|
2769
|
+
return label.quoted ? `the section \u201C${label.text}\u201D` : label.text;
|
|
2770
|
+
}
|
|
2771
|
+
if (node.name?.trim()) return `\u201C${node.name.trim()}\u201D`;
|
|
2772
|
+
const noun = lowerFirst(blockName(node.type, ctx));
|
|
2773
|
+
if (form === "definite") return `the ${noun}`;
|
|
2774
|
+
return `${/^[aeiou]/i.test(noun) ? "an" : "a"} ${noun}`;
|
|
2775
|
+
}
|
|
2776
|
+
function sectionLabel(entry, index, ctx) {
|
|
2777
|
+
const node = entry.node;
|
|
2778
|
+
if (node.name?.trim()) return { text: node.name.trim(), quoted: true };
|
|
2779
|
+
const heading = firstHeading(node, ctx);
|
|
2780
|
+
if (heading) return { text: heading, quoted: true };
|
|
2781
|
+
const siblings = index.children.get(entry.parentKey) ?? [];
|
|
2782
|
+
const sections = siblings.filter((id) => index.byId.get(id)?.node.type === "section");
|
|
2783
|
+
return { text: `section ${sections.indexOf(node.id) + 1}`, quoted: false };
|
|
2784
|
+
}
|
|
2785
|
+
function firstHeading(node, ctx) {
|
|
2786
|
+
for (const child of node.children ?? []) {
|
|
2787
|
+
if (child.type === "heading") {
|
|
2788
|
+
const text = child.props?.text;
|
|
2789
|
+
if (typeof text === "string" && text.trim()) return clip(readableText(text, ctx).trim(), 60);
|
|
2790
|
+
if (isRichTextDoc(text)) return clip(richTextToPlain(text, (e) => expressionLabel(e, ctx)).trim(), 60);
|
|
2791
|
+
}
|
|
2792
|
+
const nested = firstHeading(child, ctx);
|
|
2793
|
+
if (nested) return nested;
|
|
2794
|
+
}
|
|
2795
|
+
return null;
|
|
2796
|
+
}
|
|
2797
|
+
function place(entry, index, preposition, ctx) {
|
|
2798
|
+
let cursor = entry.parentId ? index.byId.get(entry.parentId) : void 0;
|
|
2799
|
+
while (cursor) {
|
|
2800
|
+
if (cursor.node.type === "section") {
|
|
2801
|
+
const label = sectionLabel(cursor, index, ctx);
|
|
2802
|
+
return ` ${preposition} ${label.quoted ? `\u201C${label.text}\u201D` : label.text}`;
|
|
2803
|
+
}
|
|
2804
|
+
cursor = cursor.parentId ? index.byId.get(cursor.parentId) : void 0;
|
|
2805
|
+
}
|
|
2806
|
+
if (entry.area === "header") return ` ${preposition} the header`;
|
|
2807
|
+
if (entry.area === "footer") return ` ${preposition} the footer`;
|
|
2808
|
+
return preposition === "within" ? " within the document" : "";
|
|
2809
|
+
}
|
|
2810
|
+
function propLabel(type, prop, ctx) {
|
|
2811
|
+
const def = ctx.registry?.get(type);
|
|
2812
|
+
const find = (fields) => {
|
|
2813
|
+
for (const field of fields) if (field.prop === prop || field.prop.startsWith(`${prop}.`)) return field.label;
|
|
2814
|
+
return null;
|
|
2815
|
+
};
|
|
2816
|
+
for (const section of def?.inspector ?? []) {
|
|
2817
|
+
const label = find(section.fields);
|
|
2818
|
+
if (label) return lowerFirst(label);
|
|
2819
|
+
}
|
|
2820
|
+
return lowerFirst(humanize(prop));
|
|
2821
|
+
}
|
|
2822
|
+
function simpleValue(type, prop, value, ctx) {
|
|
2823
|
+
if (value === void 0 || value === null) return "None";
|
|
2824
|
+
if (typeof value === "boolean") return onOff(value);
|
|
2825
|
+
if (typeof value === "number") return String(value);
|
|
2826
|
+
if (isRichTextDoc(value)) return oneLine(richTextToPlain(value, (e) => expressionLabel(e, ctx)));
|
|
2827
|
+
if (typeof value !== "string") return null;
|
|
2828
|
+
const kind = ctx.registry?.bindingsOf(type).find((b) => b.prop === prop)?.kind;
|
|
2829
|
+
if (kind === "value" || kind === "collection") return expressionLabel(value, ctx);
|
|
2830
|
+
if (kind === "condition" || kind === "list") return null;
|
|
2831
|
+
if (!kind && looksLikeExpression(value)) return expressionLabel(value, ctx);
|
|
2832
|
+
return oneLine(readableText(value, ctx));
|
|
2833
|
+
}
|
|
2834
|
+
function readableText(text, ctx) {
|
|
2835
|
+
return text.replace(/\{\{([^}]*)\}\}/g, (_, expr) => expressionLabel(expr.trim(), ctx));
|
|
2836
|
+
}
|
|
2837
|
+
function looksLikeExpression(value) {
|
|
2838
|
+
return /\|\s*[A-Za-z_]\w*/.test(value) || /^\s*[A-Za-z_]\w*\s*\(/.test(value);
|
|
2839
|
+
}
|
|
2840
|
+
function expressionLabel(expr, ctx) {
|
|
2841
|
+
if (!expr.trim()) return "nothing";
|
|
2842
|
+
const { label } = describeExpression(expr, ctx.scope);
|
|
2843
|
+
if (label === expr && /[^\w\s'’-]/.test(expr)) return "a calculated value";
|
|
2844
|
+
return label;
|
|
2845
|
+
}
|
|
2846
|
+
function colour(value) {
|
|
2847
|
+
if (!value) return "None";
|
|
2848
|
+
return value.startsWith("$") ? token(value) : value;
|
|
2849
|
+
}
|
|
2850
|
+
function token(value) {
|
|
2851
|
+
const last = value.slice(1).split(".").pop() ?? value;
|
|
2852
|
+
return `Theme ${lowerFirst(humanize(last))}`;
|
|
2853
|
+
}
|
|
2854
|
+
function onOff(value) {
|
|
2855
|
+
return value ? "On" : "Off";
|
|
2856
|
+
}
|
|
2857
|
+
function list(words) {
|
|
2858
|
+
if (words.length <= 1) return words[0] ?? "";
|
|
2859
|
+
return `${words.slice(0, -1).join(", ")} and ${words[words.length - 1]}`;
|
|
2860
|
+
}
|
|
2861
|
+
function lowerFirst(text) {
|
|
2862
|
+
if (text.length > 1 && text[1] === text[1].toUpperCase() && /[A-Z]/.test(text[1])) return text;
|
|
2863
|
+
return text.charAt(0).toLowerCase() + text.slice(1);
|
|
2864
|
+
}
|
|
2865
|
+
function oneLine(text) {
|
|
2866
|
+
return text.replace(/\s+/g, " ").trim();
|
|
2867
|
+
}
|
|
2868
|
+
function clip(text, limit) {
|
|
2869
|
+
return text.length <= limit ? text : `${text.slice(0, limit - 1).trimEnd()}\u2026`;
|
|
2870
|
+
}
|
|
2871
|
+
function unique(items) {
|
|
2872
|
+
return [...new Set(items)];
|
|
2873
|
+
}
|
|
2874
|
+
function keysOf(a, b) {
|
|
2875
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2876
|
+
for (const o of [a, b]) if (o && typeof o === "object") for (const k of Object.keys(o)) keys.add(k);
|
|
2877
|
+
return [...keys];
|
|
2878
|
+
}
|
|
2879
|
+
function same(a, b) {
|
|
2880
|
+
if (a === b) return true;
|
|
2881
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
2882
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
2883
|
+
if (Array.isArray(a)) {
|
|
2884
|
+
const other = b;
|
|
2885
|
+
return a.length === other.length && a.every((v, i) => same(v, other[i]));
|
|
2886
|
+
}
|
|
2887
|
+
const x = a;
|
|
2888
|
+
const y = b;
|
|
2889
|
+
return keysOf(x, y).every((k) => same(x[k], y[k]));
|
|
2890
|
+
}
|
|
2891
|
+
function longestIncreasing(values) {
|
|
2892
|
+
const tails = [];
|
|
2893
|
+
const previous = new Array(values.length).fill(-1);
|
|
2894
|
+
for (let i = 0; i < values.length; i++) {
|
|
2895
|
+
let lo = 0;
|
|
2896
|
+
let hi = tails.length;
|
|
2897
|
+
while (lo < hi) {
|
|
2898
|
+
const mid = lo + hi >> 1;
|
|
2899
|
+
if (values[tails[mid]] < values[i]) lo = mid + 1;
|
|
2900
|
+
else hi = mid;
|
|
2901
|
+
}
|
|
2902
|
+
if (lo > 0) previous[i] = tails[lo - 1];
|
|
2903
|
+
tails[lo] = i;
|
|
2904
|
+
}
|
|
2905
|
+
const keep = /* @__PURE__ */ new Set();
|
|
2906
|
+
let cursor = tails.length ? tails[tails.length - 1] : -1;
|
|
2907
|
+
while (cursor >= 0) {
|
|
2908
|
+
keep.add(cursor);
|
|
2909
|
+
cursor = previous[cursor];
|
|
2910
|
+
}
|
|
2911
|
+
return keep;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2235
2914
|
// src/resolve/resolve.ts
|
|
2236
2915
|
var display = toDisplayString;
|
|
2237
2916
|
function resolveTemplate(opts) {
|
|
@@ -2495,8 +3174,8 @@ function resolveNode(ctx, node, scope, schemaScope, keyPrefix, _parentRole) {
|
|
|
2495
3174
|
for (const spec of bindingSpecsFor(def)) {
|
|
2496
3175
|
if (spec.perItem) continue;
|
|
2497
3176
|
if (spec.items) {
|
|
2498
|
-
const
|
|
2499
|
-
if (Array.isArray(
|
|
3177
|
+
const list2 = getPath(props, spec.prop);
|
|
3178
|
+
if (Array.isArray(list2)) list2.forEach((item, i) => item && typeof item === "object" && spec.items.forEach((s) => !s.perItem && applySpec(s, item, `${spec.prop}.${i}.`)));
|
|
2500
3179
|
continue;
|
|
2501
3180
|
}
|
|
2502
3181
|
applySpec(spec, props, "");
|
|
@@ -2684,7 +3363,7 @@ function computePageGeometry(page) {
|
|
|
2684
3363
|
const portrait = page.orientation !== "landscape";
|
|
2685
3364
|
const width = mmToPx(portrait ? size.width : size.height);
|
|
2686
3365
|
const height = mmToPx(portrait ? size.height : size.width);
|
|
2687
|
-
const
|
|
3366
|
+
const margins2 = {
|
|
2688
3367
|
top: mmToPx(page.margins.top),
|
|
2689
3368
|
right: mmToPx(page.margins.right),
|
|
2690
3369
|
bottom: mmToPx(page.margins.bottom),
|
|
@@ -2692,18 +3371,18 @@ function computePageGeometry(page) {
|
|
|
2692
3371
|
};
|
|
2693
3372
|
const headerHeight = mmToPx(Math.max(0, page.header.height));
|
|
2694
3373
|
const footerHeight = mmToPx(Math.max(0, page.footer.height));
|
|
2695
|
-
const contentTop =
|
|
2696
|
-
const footerTop = height -
|
|
3374
|
+
const contentTop = margins2.top + headerHeight;
|
|
3375
|
+
const footerTop = height - margins2.bottom - footerHeight;
|
|
2697
3376
|
return {
|
|
2698
3377
|
width,
|
|
2699
3378
|
height,
|
|
2700
|
-
margins,
|
|
3379
|
+
margins: margins2,
|
|
2701
3380
|
headerHeight,
|
|
2702
3381
|
footerHeight,
|
|
2703
|
-
contentWidth: Math.max(0, width -
|
|
3382
|
+
contentWidth: Math.max(0, width - margins2.left - margins2.right),
|
|
2704
3383
|
contentHeight: Math.max(0, footerTop - contentTop),
|
|
2705
3384
|
contentTop,
|
|
2706
|
-
headerTop:
|
|
3385
|
+
headerTop: margins2.top,
|
|
2707
3386
|
footerTop
|
|
2708
3387
|
};
|
|
2709
3388
|
}
|
|
@@ -2713,16 +3392,16 @@ function g(ctx, node) {
|
|
|
2713
3392
|
return ctx.geom.nodes[node.key] ?? { top: 0, height: 0, left: 0, width: 0 };
|
|
2714
3393
|
}
|
|
2715
3394
|
var EPS = 0.01;
|
|
2716
|
-
function bestBreak(ctx,
|
|
3395
|
+
function bestBreak(ctx, list2, limit, minY) {
|
|
2717
3396
|
const visible = (from, step) => {
|
|
2718
|
-
for (let j = from; j >= 0 && j <
|
|
2719
|
-
const s =
|
|
3397
|
+
for (let j = from; j >= 0 && j < list2.length; j += step) {
|
|
3398
|
+
const s = list2[j];
|
|
2720
3399
|
if (g(ctx, s).height > EPS) return s;
|
|
2721
3400
|
}
|
|
2722
3401
|
return void 0;
|
|
2723
3402
|
};
|
|
2724
|
-
for (let i =
|
|
2725
|
-
const child =
|
|
3403
|
+
for (let i = list2.length - 1; i >= 0; i--) {
|
|
3404
|
+
const child = list2[i];
|
|
2726
3405
|
const cg = g(ctx, child);
|
|
2727
3406
|
const top = cg.top;
|
|
2728
3407
|
const bottom = cg.top + cg.height;
|
|
@@ -3044,10 +3723,10 @@ function validateTemplate(opts) {
|
|
|
3044
3723
|
}
|
|
3045
3724
|
if (kind === "collection" && r.issues.length === 0 && r.type !== "array" && r.type !== "any" && r.type !== "unknown") out.push(issue("error", "missing-collection", `"${expr}" is a ${r.type}, not a list`, { ...where, prop }));
|
|
3046
3725
|
if (expected && r.issues.length === 0 && r.type !== "unknown" && r.type !== "any" && r.type !== "null") {
|
|
3047
|
-
const
|
|
3726
|
+
const list2 = Array.isArray(expected) ? expected : [expected];
|
|
3048
3727
|
const numeric = ["number", "currency", "percent"];
|
|
3049
|
-
const compatible =
|
|
3050
|
-
if (!compatible) out.push(issue("warning", "type-mismatch", `"${expr}" is a ${r.type}; expected ${
|
|
3728
|
+
const compatible = list2.includes(r.type) || list2.some((t) => numeric.includes(t)) && numeric.includes(r.type) || list2.includes("string") && r.type !== "array" && r.type !== "object";
|
|
3729
|
+
if (!compatible) out.push(issue("warning", "type-mismatch", `"${expr}" is a ${r.type}; expected ${list2.join(" or ")}`, { ...where, prop }));
|
|
3051
3730
|
}
|
|
3052
3731
|
issues.push(...out);
|
|
3053
3732
|
return out;
|
|
@@ -3106,8 +3785,8 @@ function validateTemplate(opts) {
|
|
|
3106
3785
|
continue;
|
|
3107
3786
|
}
|
|
3108
3787
|
if (spec.items) {
|
|
3109
|
-
const
|
|
3110
|
-
if (Array.isArray(
|
|
3788
|
+
const list2 = getPath2(props, spec.prop);
|
|
3789
|
+
if (Array.isArray(list2)) list2.forEach((item, i) => item && typeof item === "object" && spec.items.forEach((sub) => runSpec(sub, item, `${spec.prop}.${i}.`, sub.perItem ? itemScope : scope)));
|
|
3111
3790
|
continue;
|
|
3112
3791
|
}
|
|
3113
3792
|
runSpec(spec, props, "", scope);
|
|
@@ -3401,7 +4080,7 @@ function readRegion(template, region) {
|
|
|
3401
4080
|
return template.footer.firstPage ?? [];
|
|
3402
4081
|
}
|
|
3403
4082
|
}
|
|
3404
|
-
function
|
|
4083
|
+
function indexTemplate2(template) {
|
|
3405
4084
|
const index = /* @__PURE__ */ new Map();
|
|
3406
4085
|
const visit = (nodes, parent, region, path) => {
|
|
3407
4086
|
nodes.forEach((node, i) => {
|
|
@@ -3414,21 +4093,21 @@ function indexTemplate(template) {
|
|
|
3414
4093
|
return index;
|
|
3415
4094
|
}
|
|
3416
4095
|
function findNode(template, id) {
|
|
3417
|
-
return
|
|
4096
|
+
return indexTemplate2(template).get(id);
|
|
3418
4097
|
}
|
|
3419
4098
|
function listAt(template, location) {
|
|
3420
|
-
let
|
|
4099
|
+
let list2 = getRegion(template, location.region);
|
|
3421
4100
|
for (let i = 0; i < location.path.length - 1; i++) {
|
|
3422
|
-
const n =
|
|
4101
|
+
const n = list2[location.path[i]];
|
|
3423
4102
|
if (!n) throw new Error("Invalid node path");
|
|
3424
4103
|
n.children ??= [];
|
|
3425
|
-
|
|
4104
|
+
list2 = n.children;
|
|
3426
4105
|
}
|
|
3427
|
-
return { list, index: location.path[location.path.length - 1] ?? 0 };
|
|
4106
|
+
return { list: list2, index: location.path[location.path.length - 1] ?? 0 };
|
|
3428
4107
|
}
|
|
3429
4108
|
function nodeAt(template, location) {
|
|
3430
|
-
const { list, index } = listAt(template, location);
|
|
3431
|
-
return
|
|
4109
|
+
const { list: list2, index } = listAt(template, location);
|
|
4110
|
+
return list2[index];
|
|
3432
4111
|
}
|
|
3433
4112
|
function ancestors(index, id) {
|
|
3434
4113
|
const chain = [];
|
|
@@ -3496,28 +4175,28 @@ function applyCommand(draft, cmd) {
|
|
|
3496
4175
|
const t = draft;
|
|
3497
4176
|
switch (cmd.type) {
|
|
3498
4177
|
case "insertNode": {
|
|
3499
|
-
const { list, index } = listAt(t, cmd.at);
|
|
3500
|
-
|
|
4178
|
+
const { list: list2, index } = listAt(t, cmd.at);
|
|
4179
|
+
list2.splice(Math.min(index, list2.length), 0, cmd.node);
|
|
3501
4180
|
return;
|
|
3502
4181
|
}
|
|
3503
4182
|
case "insertNodes": {
|
|
3504
|
-
const { list, index } = listAt(t, cmd.at);
|
|
3505
|
-
|
|
4183
|
+
const { list: list2, index } = listAt(t, cmd.at);
|
|
4184
|
+
list2.splice(Math.min(index, list2.length), 0, ...cmd.nodes);
|
|
3506
4185
|
return;
|
|
3507
4186
|
}
|
|
3508
4187
|
case "removeNodes": {
|
|
3509
|
-
const index =
|
|
4188
|
+
const index = indexTemplate2(t);
|
|
3510
4189
|
const entries = cmd.ids.map((id) => index.get(id)).filter((e) => !!e);
|
|
3511
4190
|
entries.sort((a, b) => b.depth - a.depth || b.index - a.index);
|
|
3512
4191
|
for (const e of entries) {
|
|
3513
|
-
const
|
|
3514
|
-
const i =
|
|
3515
|
-
if (i >= 0)
|
|
4192
|
+
const list2 = e.parent ? e.parent.children ??= [] : getRegion(t, e.region);
|
|
4193
|
+
const i = list2.findIndex((n) => n.id === e.node.id);
|
|
4194
|
+
if (i >= 0) list2.splice(i, 1);
|
|
3516
4195
|
}
|
|
3517
4196
|
return;
|
|
3518
4197
|
}
|
|
3519
4198
|
case "moveNode": {
|
|
3520
|
-
const index =
|
|
4199
|
+
const index = indexTemplate2(t);
|
|
3521
4200
|
const entry = index.get(cmd.id);
|
|
3522
4201
|
if (!entry) throw new CommandError(`Node ${cmd.id} not found`);
|
|
3523
4202
|
const target = listAt(t, cmd.to);
|
|
@@ -3542,13 +4221,13 @@ function applyCommand(draft, cmd) {
|
|
|
3542
4221
|
return;
|
|
3543
4222
|
}
|
|
3544
4223
|
case "duplicateNode": {
|
|
3545
|
-
const index =
|
|
4224
|
+
const index = indexTemplate2(t);
|
|
3546
4225
|
const entry = index.get(cmd.id);
|
|
3547
4226
|
if (!entry) throw new CommandError(`Node ${cmd.id} not found`);
|
|
3548
|
-
const
|
|
4227
|
+
const list2 = entry.parent ? entry.parent.children ??= [] : getRegion(t, entry.region);
|
|
3549
4228
|
const copy = cloneWithNewIds(isDraft(entry.node) ? current(entry.node) : entry.node);
|
|
3550
4229
|
if (cmd.newId) copy.id = cmd.newId;
|
|
3551
|
-
|
|
4230
|
+
list2.splice(entry.index + 1, 0, copy);
|
|
3552
4231
|
return;
|
|
3553
4232
|
}
|
|
3554
4233
|
case "setProps": {
|
|
@@ -3621,27 +4300,27 @@ function applyCommand(draft, cmd) {
|
|
|
3621
4300
|
return;
|
|
3622
4301
|
}
|
|
3623
4302
|
case "wrapNodes": {
|
|
3624
|
-
const index =
|
|
4303
|
+
const index = indexTemplate2(t);
|
|
3625
4304
|
const entries = cmd.ids.map((id) => index.get(id)).filter((e) => !!e);
|
|
3626
4305
|
if (entries.length === 0) return;
|
|
3627
4306
|
const first = entries[0];
|
|
3628
|
-
const
|
|
4307
|
+
const list2 = first.parent ? first.parent.children ??= [] : getRegion(t, first.region);
|
|
3629
4308
|
const nodes = entries.map((e) => e.node);
|
|
3630
|
-
const startIndex =
|
|
4309
|
+
const startIndex = list2.indexOf(first.node);
|
|
3631
4310
|
for (const n of nodes) {
|
|
3632
|
-
const i =
|
|
3633
|
-
if (i >= 0)
|
|
4311
|
+
const i = list2.indexOf(n);
|
|
4312
|
+
if (i >= 0) list2.splice(i, 1);
|
|
3634
4313
|
}
|
|
3635
4314
|
const wrapper = { ...cmd.wrapper, children: [...cmd.wrapper.children ?? [], ...nodes.map((n) => isDraft(n) ? current(n) : n)] };
|
|
3636
|
-
|
|
4315
|
+
list2.splice(Math.min(startIndex, list2.length), 0, wrapper);
|
|
3637
4316
|
return;
|
|
3638
4317
|
}
|
|
3639
4318
|
case "replaceNode": {
|
|
3640
|
-
const index =
|
|
4319
|
+
const index = indexTemplate2(t);
|
|
3641
4320
|
const entry = index.get(cmd.id);
|
|
3642
4321
|
if (!entry) throw new CommandError(`Node ${cmd.id} not found`);
|
|
3643
|
-
const
|
|
3644
|
-
|
|
4322
|
+
const list2 = entry.parent ? entry.parent.children ??= [] : getRegion(t, entry.region);
|
|
4323
|
+
list2[entry.index] = cmd.node;
|
|
3645
4324
|
return;
|
|
3646
4325
|
}
|
|
3647
4326
|
case "setPage": {
|
|
@@ -3701,7 +4380,7 @@ function applyCommand(draft, cmd) {
|
|
|
3701
4380
|
}
|
|
3702
4381
|
}
|
|
3703
4382
|
function requireNode(t, id) {
|
|
3704
|
-
const e =
|
|
4383
|
+
const e = indexTemplate2(t).get(id);
|
|
3705
4384
|
if (!e) throw new CommandError(`Node ${id} not found`);
|
|
3706
4385
|
return e.node;
|
|
3707
4386
|
}
|
|
@@ -3774,7 +4453,7 @@ var EditorStore = class {
|
|
|
3774
4453
|
/** Cached id → node index for the current document version. */
|
|
3775
4454
|
index() {
|
|
3776
4455
|
if (!this.indexCache || this.indexCache.version !== this.state.version) {
|
|
3777
|
-
this.indexCache = { version: this.state.version, index:
|
|
4456
|
+
this.indexCache = { version: this.state.version, index: indexTemplate2(this.state.template) };
|
|
3778
4457
|
}
|
|
3779
4458
|
return this.indexCache.index;
|
|
3780
4459
|
}
|
|
@@ -3968,7 +4647,7 @@ var EditorStore = class {
|
|
|
3968
4647
|
}
|
|
3969
4648
|
};
|
|
3970
4649
|
function findIn(template, id) {
|
|
3971
|
-
return
|
|
4650
|
+
return indexTemplate2(template).get(id)?.node;
|
|
3972
4651
|
}
|
|
3973
4652
|
function sameIds(a, b) {
|
|
3974
4653
|
return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
@@ -4061,9 +4740,9 @@ function markWrap(children, marks, opts) {
|
|
|
4061
4740
|
if (marks.color) out = h("span", { style: { color: resolveValue(marks.color, opts.theme, "inherit") } }, out);
|
|
4062
4741
|
if (marks.link) {
|
|
4063
4742
|
const href = marks.link;
|
|
4064
|
-
const
|
|
4743
|
+
const safe2 = isSafeUrl(href);
|
|
4065
4744
|
const linkColor = resolveValue(opts.styles.link.color, opts.theme, opts.theme.colors.secondary);
|
|
4066
|
-
out = h("a", { class: "bp-link", href:
|
|
4745
|
+
out = h("a", { class: "bp-link", href: safe2 ? href : void 0, rel: "noopener", style: { color: linkColor, "text-decoration": opts.styles.link.underline === false ? "none" : "underline" } }, out);
|
|
4067
4746
|
}
|
|
4068
4747
|
return out;
|
|
4069
4748
|
}
|
|
@@ -4223,6 +4902,8 @@ export {
|
|
|
4223
4902
|
bestBreak,
|
|
4224
4903
|
bindingSpecsFor,
|
|
4225
4904
|
blockStyleSchema,
|
|
4905
|
+
calcPaths,
|
|
4906
|
+
calcToAst,
|
|
4226
4907
|
checkExpressionSyntax,
|
|
4227
4908
|
checkOutput,
|
|
4228
4909
|
classNames,
|
|
@@ -4246,8 +4927,10 @@ export {
|
|
|
4246
4927
|
dataSourceSchemas,
|
|
4247
4928
|
defaultDocumentStyles,
|
|
4248
4929
|
defaultPageSettings,
|
|
4930
|
+
defaultPipeFor,
|
|
4249
4931
|
defineBlock,
|
|
4250
4932
|
describeExpression,
|
|
4933
|
+
diffTemplates,
|
|
4251
4934
|
documentStylesSchema,
|
|
4252
4935
|
escapeHtml,
|
|
4253
4936
|
evaluate,
|
|
@@ -4263,19 +4946,23 @@ export {
|
|
|
4263
4946
|
formatDatePattern,
|
|
4264
4947
|
formatNumber,
|
|
4265
4948
|
formatPercent,
|
|
4949
|
+
fromExpression,
|
|
4266
4950
|
getField,
|
|
4267
4951
|
getRegion,
|
|
4268
4952
|
h,
|
|
4953
|
+
hasPipe,
|
|
4269
4954
|
humaniseLabel,
|
|
4270
4955
|
humanize,
|
|
4271
4956
|
humanizeKey,
|
|
4272
|
-
indexTemplate,
|
|
4957
|
+
indexTemplate2 as indexTemplate,
|
|
4273
4958
|
inlineFromTemplate,
|
|
4274
4959
|
inlineToPlain,
|
|
4275
4960
|
interpolate,
|
|
4961
|
+
isCalculation,
|
|
4276
4962
|
isDarkColor,
|
|
4277
4963
|
isDescendant,
|
|
4278
4964
|
isEmptyValue,
|
|
4965
|
+
isPlainPath,
|
|
4279
4966
|
isRichTextDoc,
|
|
4280
4967
|
isSafeUrl,
|
|
4281
4968
|
isTokenRef,
|
|
@@ -4322,12 +5009,14 @@ export {
|
|
|
4322
5009
|
splitTemplate,
|
|
4323
5010
|
styleString,
|
|
4324
5011
|
styleToCss,
|
|
5012
|
+
summariseDiff,
|
|
4325
5013
|
synthesizeSampleData,
|
|
4326
5014
|
synthesizeValue,
|
|
4327
5015
|
templateSchema,
|
|
4328
5016
|
textStyleLabel,
|
|
4329
5017
|
toDate,
|
|
4330
5018
|
toDisplayString,
|
|
5019
|
+
toExpression,
|
|
4331
5020
|
toHtml,
|
|
4332
5021
|
toNumber,
|
|
4333
5022
|
tokenChip,
|
|
@@ -4338,6 +5027,7 @@ export {
|
|
|
4338
5027
|
usedBlockTypes,
|
|
4339
5028
|
validateTemplate,
|
|
4340
5029
|
walkTemplate,
|
|
4341
|
-
walkVNodes
|
|
5030
|
+
walkVNodes,
|
|
5031
|
+
withDefaultPipe
|
|
4342
5032
|
};
|
|
4343
5033
|
//# sourceMappingURL=index.js.map
|