@segnavia/html 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/qr.js +39 -0
- package/dist/slot-policy.d.ts +27 -0
- package/dist/slot-policy.js +61 -0
- package/dist/slot-print.d.ts +40 -0
- package/dist/slot-print.js +142 -0
- package/dist/slots.d.ts +8 -1
- package/dist/slots.js +114 -22
- package/package.json +5 -4
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { REFUSED_ELEMENTS, SanitizeOptions, findActiveContent, sanitize } from "
|
|
|
2
2
|
import { BINDINGS, FillOptions, Filled, bindingNames, fill, hasBindings } from "./bindings.js";
|
|
3
3
|
import { BookDocument, BookDocumentOptions, BookHeading, BookPrintPart, BookSectionInfo, bookPrintPart } from "./book/types.js";
|
|
4
4
|
import { bookDocument } from "./book/book-document.js";
|
|
5
|
+
import { SlotFillEntry, SlotFillRefusal, SlotFillsConflictError, SlotFillsRefusedError, SlotPrintOptions, assertSingleSlotCopy, assertSlotPrintOptions, collectSlotPrintCopies, isSlotCopyRefusal, slotFillCopyId, slotFillsConflict } from "./slot-print.js";
|
|
6
|
+
import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
|
|
5
7
|
import { MAX_SLOT_DECLARATIONS, PreparedSlots, SlotFillRefusedError, hasSlotWork, prepareSlotCollection, prepareSlots, slotFillRefusal } from "./slots.js";
|
|
6
8
|
//#region src/index.d.ts
|
|
7
9
|
export type HtmlKind = 'plain' | 'live-data' | 'scripted' | 'refused';
|
|
8
10
|
export declare function classify(html: string, prefix: string): HtmlKind;
|
|
9
11
|
export declare function searchText(html: string): string;
|
|
10
12
|
//#endregion
|
|
11
|
-
export { BINDINGS, type BookDocument, type BookDocumentOptions, type BookHeading, type BookPrintPart, type BookSectionInfo, type FillOptions, type Filled, MAX_SLOT_DECLARATIONS, type PreparedSlots, REFUSED_ELEMENTS, type SanitizeOptions, SlotFillRefusedError, bindingNames, bookDocument, bookPrintPart, fill, findActiveContent, hasBindings, hasSlotWork, prepareSlotCollection, prepareSlots, sanitize, slotFillRefusal };
|
|
13
|
+
export { BINDINGS, type BookDocument, type BookDocumentOptions, type BookHeading, type BookPrintPart, type BookSectionInfo, type FillOptions, type Filled, MAX_SLOT_DECLARATIONS, type PreparedSlots, REFUSED_ELEMENTS, RequiredSlotsRefusedError, type SanitizeOptions, type SlotFillEntry, type SlotFillRefusal, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, type SlotPrintOptions, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, bindingNames, bookDocument, bookPrintPart, collectSlotPrintCopies, fill, findActiveContent, hasBindings, hasSlotWork, isSlotCopyRefusal, mergeSlotFitResults, prepareSlotCollection, prepareSlots, requiredSlotRefusal, sanitize, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal };
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,9 @@ import { REFUSED_ELEMENTS, elements, findActiveContent, parse, sanitize } from "
|
|
|
2
2
|
import { BINDINGS, bindingNames, fill, hasBindings } from "./bindings.js";
|
|
3
3
|
import { bookPrintPart } from "./book/types.js";
|
|
4
4
|
import { bookDocument } from "./book/book-document.js";
|
|
5
|
+
import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
|
|
5
6
|
import { MAX_SLOT_DECLARATIONS, SlotFillRefusedError, hasSlotWork, prepareSlotCollection, prepareSlots, slotFillRefusal } from "./slots.js";
|
|
7
|
+
import { SlotFillsConflictError, SlotFillsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, collectSlotPrintCopies, isSlotCopyRefusal, slotFillCopyId, slotFillsConflict } from "./slot-print.js";
|
|
6
8
|
//#region src/index.ts
|
|
7
9
|
function classify(html, prefix) {
|
|
8
10
|
const scripted = elements(parse(html).children).some((element) => element.name.toLowerCase() === "script");
|
|
@@ -15,4 +17,4 @@ function searchText(html) {
|
|
|
15
17
|
return html.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
16
18
|
}
|
|
17
19
|
//#endregion
|
|
18
|
-
export { BINDINGS, MAX_SLOT_DECLARATIONS, REFUSED_ELEMENTS, SlotFillRefusedError, bindingNames, bookDocument, bookPrintPart, classify, fill, findActiveContent, hasBindings, hasSlotWork, prepareSlotCollection, prepareSlots, sanitize, searchText, slotFillRefusal };
|
|
20
|
+
export { BINDINGS, MAX_SLOT_DECLARATIONS, REFUSED_ELEMENTS, RequiredSlotsRefusedError, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, bindingNames, bookDocument, bookPrintPart, classify, collectSlotPrintCopies, fill, findActiveContent, hasBindings, hasSlotWork, isSlotCopyRefusal, mergeSlotFitResults, prepareSlotCollection, prepareSlots, requiredSlotRefusal, sanitize, searchText, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal };
|
package/dist/qr.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Element } from "domhandler";
|
|
2
|
+
import qrcode from "qrcode-generator";
|
|
3
|
+
//#region src/qr.ts
|
|
4
|
+
const QR_PAYLOAD_LIMIT = 4096;
|
|
5
|
+
function qrPayload(value) {
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
if (url.protocol !== "https:" || url.href.length > QR_PAYLOAD_LIMIT) return void 0;
|
|
9
|
+
const code = qrcode(0, "M");
|
|
10
|
+
code.addData(url.href);
|
|
11
|
+
code.make();
|
|
12
|
+
return url.href;
|
|
13
|
+
} catch {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** A QR code of the link at level M, drawn in currentColor with a four-module quiet zone. */
|
|
18
|
+
function qrSvg(value) {
|
|
19
|
+
const code = qrcode(0, "M");
|
|
20
|
+
code.addData(qrPayload(value) ?? value);
|
|
21
|
+
code.make();
|
|
22
|
+
const modules = code.getModuleCount();
|
|
23
|
+
const children = [];
|
|
24
|
+
for (let row = 0; row < modules; row++) for (let column = 0; column < modules; column++) if (code.isDark(row, column)) children.push(new Element("rect", {
|
|
25
|
+
x: String(column + 4),
|
|
26
|
+
y: String(row + 4),
|
|
27
|
+
width: "1",
|
|
28
|
+
height: "1",
|
|
29
|
+
fill: "currentColor"
|
|
30
|
+
}));
|
|
31
|
+
return new Element("svg", {
|
|
32
|
+
viewBox: `0 0 ${modules + 8} ${modules + 8}`,
|
|
33
|
+
"shape-rendering": "crispEdges",
|
|
34
|
+
role: "img",
|
|
35
|
+
"aria-label": value
|
|
36
|
+
}, children);
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { qrPayload, qrSvg };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SlotDeclaration, SlotReport, SlotState } from "@segnavia/format";
|
|
2
|
+
//#region src/slot-policy.d.ts
|
|
3
|
+
export declare class RequiredSlotsRefusedError extends Error {
|
|
4
|
+
readonly slots: ReadonlyArray<{
|
|
5
|
+
key: string;
|
|
6
|
+
state: SlotState;
|
|
7
|
+
}>;
|
|
8
|
+
constructor(slots: ReadonlyArray<{
|
|
9
|
+
key: string;
|
|
10
|
+
state: SlotState;
|
|
11
|
+
}>);
|
|
12
|
+
}
|
|
13
|
+
export declare class UnmeasuredSlotsRefusedError extends Error {
|
|
14
|
+
readonly keys: readonly string[];
|
|
15
|
+
constructor(slots: readonly string[]);
|
|
16
|
+
}
|
|
17
|
+
/** Returns the print/render refusal for required or overflowing declarations. */
|
|
18
|
+
export declare function requiredSlotRefusal(declarations: readonly SlotDeclaration[], report: SlotReport): RequiredSlotsRefusedError | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Refuses filled refuse-overflow slots that some frame marked but did not report before output. Counts are per key:
|
|
21
|
+
* frames that marked it, against fit reports received, so one frame's report cannot stand in for another's.
|
|
22
|
+
* A slot no frame binds has nothing to measure.
|
|
23
|
+
*/
|
|
24
|
+
export declare function unmeasuredSlotRefusal(declarations: readonly SlotDeclaration[], report: SlotReport, marked: ReadonlyMap<string, number>, measured: ReadonlyMap<string, number>): UnmeasuredSlotsRefusedError | undefined;
|
|
25
|
+
/** Merges measured fit states into filled declared slots; any other state, and any undeclared key, is left as it was. */
|
|
26
|
+
export declare function mergeSlotFitResults(declarations: readonly SlotDeclaration[], report: SlotReport, slots: Record<string, 'fit' | 'shrunk' | 'overflowed'>): SlotReport;
|
|
27
|
+
//#endregion
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/slot-policy.ts
|
|
2
|
+
var RequiredSlotsRefusedError = class extends Error {
|
|
3
|
+
slots;
|
|
4
|
+
constructor(slots) {
|
|
5
|
+
super(`Output was refused because slots prevent printing: ${slots.map(({ key, state }) => `"${key}" (${state})`).join(", ")}. Fill required slots; for an overflowed refuse slot, shorten its value or change the slot's overflow.`);
|
|
6
|
+
this.name = "RequiredSlotsRefusedError";
|
|
7
|
+
this.slots = slots;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var UnmeasuredSlotsRefusedError = class extends Error {
|
|
11
|
+
keys;
|
|
12
|
+
constructor(slots) {
|
|
13
|
+
super(`Output was refused because overflow could not be measured for slots: ${slots.map((key) => `"${key}"`).join(", ")}. Ensure each filled slot frame becomes ready before the output deadline, or change the slot's overflow from 'refuse'.`);
|
|
14
|
+
this.name = "UnmeasuredSlotsRefusedError";
|
|
15
|
+
this.keys = slots;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/** Returns the print/render refusal for required or overflowing declarations. */
|
|
19
|
+
function requiredSlotRefusal(declarations, report) {
|
|
20
|
+
const refused = declarations.flatMap((slot) => {
|
|
21
|
+
const state = report.slots[slot.key];
|
|
22
|
+
const required = slot.required && state !== "filled" && state !== "fit" && state !== "shrunk" && state !== void 0;
|
|
23
|
+
const overflow = slot.overflow === "refuse" && state === "overflowed";
|
|
24
|
+
return required || overflow ? [{
|
|
25
|
+
key: slot.key,
|
|
26
|
+
state
|
|
27
|
+
}] : [];
|
|
28
|
+
});
|
|
29
|
+
return refused.length === 0 ? void 0 : new RequiredSlotsRefusedError(refused);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Refuses filled refuse-overflow slots that some frame marked but did not report before output. Counts are per key:
|
|
33
|
+
* frames that marked it, against fit reports received, so one frame's report cannot stand in for another's.
|
|
34
|
+
* A slot no frame binds has nothing to measure.
|
|
35
|
+
*/
|
|
36
|
+
function unmeasuredSlotRefusal(declarations, report, marked, measured) {
|
|
37
|
+
const unmeasured = declarations.flatMap((slot) => slot.overflow === "refuse" && report.slots[slot.key] !== "overflowed" && (measured.get(slot.key) ?? 0) < (marked.get(slot.key) ?? 0) ? [slot.key] : []);
|
|
38
|
+
return unmeasured.length === 0 ? void 0 : new UnmeasuredSlotsRefusedError(unmeasured);
|
|
39
|
+
}
|
|
40
|
+
/** Merges measured fit states into filled declared slots; any other state, and any undeclared key, is left as it was. */
|
|
41
|
+
function mergeSlotFitResults(declarations, report, slots) {
|
|
42
|
+
const declared = new Set(declarations.map((slot) => slot.key));
|
|
43
|
+
const merged = { ...report.slots };
|
|
44
|
+
const rank = {
|
|
45
|
+
fit: 0,
|
|
46
|
+
shrunk: 1,
|
|
47
|
+
overflowed: 2
|
|
48
|
+
};
|
|
49
|
+
for (const [key, state] of Object.entries(slots)) {
|
|
50
|
+
if (!declared.has(key)) continue;
|
|
51
|
+
const previous = merged[key];
|
|
52
|
+
if (previous === "filled") merged[key] = state;
|
|
53
|
+
else if ((previous === "fit" || previous === "shrunk" || previous === "overflowed") && rank[state] > rank[previous]) merged[key] = state;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
...report,
|
|
57
|
+
slots: merged
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Fill, SlotsOptions } from "@segnavia/format";
|
|
2
|
+
//#region src/slot-print.d.ts
|
|
3
|
+
/** One copy of a print/PDF item: that copy's hand layer over its source layer. */
|
|
4
|
+
export interface SlotFillEntry {
|
|
5
|
+
fill?: Fill;
|
|
6
|
+
source?: Fill;
|
|
7
|
+
}
|
|
8
|
+
export interface SlotPrintOptions extends SlotsOptions {
|
|
9
|
+
fills?: SlotFillEntry[];
|
|
10
|
+
/** When set, copies that would be refused are reported and omitted; the job still refuses if none remain. */
|
|
11
|
+
skipRefused?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class SlotFillsConflictError extends Error {
|
|
14
|
+
constructor();
|
|
15
|
+
}
|
|
16
|
+
export interface SlotFillRefusal {
|
|
17
|
+
index: number;
|
|
18
|
+
id: string;
|
|
19
|
+
slots: ReadonlyArray<{
|
|
20
|
+
key: string;
|
|
21
|
+
state: string;
|
|
22
|
+
}>;
|
|
23
|
+
error: Error;
|
|
24
|
+
}
|
|
25
|
+
export declare class SlotFillsRefusedError extends Error {
|
|
26
|
+
readonly entries: readonly SlotFillRefusal[];
|
|
27
|
+
constructor(entries: readonly SlotFillRefusal[]);
|
|
28
|
+
}
|
|
29
|
+
export declare function slotFillCopyId(entry: SlotFillEntry, index: number): string;
|
|
30
|
+
export declare function slotFillsConflict(options: SlotPrintOptions | undefined): SlotFillsConflictError | undefined;
|
|
31
|
+
export declare function assertSlotPrintOptions(options: SlotPrintOptions | undefined): void;
|
|
32
|
+
/** Single-copy fill paths refuse `fills`; print and PDF jobs loop it themselves. */
|
|
33
|
+
export declare function assertSingleSlotCopy(options: SlotsOptions | undefined): void;
|
|
34
|
+
export declare function isSlotCopyRefusal(error: unknown): error is Error;
|
|
35
|
+
/**
|
|
36
|
+
* Runs one print/PDF copy per `fills` entry, in order. Without `fills`, runs once with the given options.
|
|
37
|
+
* Host `report` is called once per entry, in entry order, including refused copies.
|
|
38
|
+
*/
|
|
39
|
+
export declare function collectSlotPrintCopies<T>(options: SlotPrintOptions | undefined, renderCopy: (copy: SlotsOptions) => Promise<T>): Promise<T[]>;
|
|
40
|
+
//#endregion
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError } from "./slot-policy.js";
|
|
2
|
+
import { SlotFillRefusedError } from "./slots.js";
|
|
3
|
+
//#region src/slot-print.ts
|
|
4
|
+
var SlotFillsConflictError = class extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("slots.fills cannot be given together with slots.fill or slots.source. Pass only fills, or only fill and source, to clear this refusal.");
|
|
7
|
+
this.name = "SlotFillsConflictError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var SlotFillsRefusedError = class extends Error {
|
|
11
|
+
entries;
|
|
12
|
+
constructor(entries) {
|
|
13
|
+
super(slotFillsRefusedMessage(entries));
|
|
14
|
+
this.name = "SlotFillsRefusedError";
|
|
15
|
+
this.entries = entries;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function slotFillCopyId(entry, index) {
|
|
19
|
+
return entry.fill?.id ?? String(index);
|
|
20
|
+
}
|
|
21
|
+
function slotFillsConflict(options) {
|
|
22
|
+
return options?.fills && (options.fill || options.source) ? new SlotFillsConflictError() : void 0;
|
|
23
|
+
}
|
|
24
|
+
function assertSlotPrintOptions(options) {
|
|
25
|
+
const conflict = slotFillsConflict(options);
|
|
26
|
+
if (conflict) throw conflict;
|
|
27
|
+
}
|
|
28
|
+
/** Single-copy fill paths refuse `fills`; print and PDF jobs loop it themselves. */
|
|
29
|
+
function assertSingleSlotCopy(options) {
|
|
30
|
+
const print = options;
|
|
31
|
+
assertSlotPrintOptions(print);
|
|
32
|
+
if (print?.fills) throw new RangeError("slots.fills prints one copy per entry in a PDF or print document. Pass fill or source for a single copy, to clear this refusal.");
|
|
33
|
+
}
|
|
34
|
+
function isSlotCopyRefusal(error) {
|
|
35
|
+
return error instanceof RequiredSlotsRefusedError || error instanceof UnmeasuredSlotsRefusedError || error instanceof SlotFillRefusedError;
|
|
36
|
+
}
|
|
37
|
+
function copySlots(options, entry, capture) {
|
|
38
|
+
return {
|
|
39
|
+
declarations: options.declarations,
|
|
40
|
+
resolveAsset: options.resolveAsset,
|
|
41
|
+
fill: entry.fill,
|
|
42
|
+
source: entry.source,
|
|
43
|
+
report: capture
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function emptyReport(entry) {
|
|
47
|
+
return {
|
|
48
|
+
fill: entry.fill?.id ?? entry.source?.id ?? null,
|
|
49
|
+
slots: {},
|
|
50
|
+
origins: {}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function refusalFrom(error, entry, index) {
|
|
54
|
+
const id = slotFillCopyId(entry, index);
|
|
55
|
+
if (error instanceof RequiredSlotsRefusedError) return {
|
|
56
|
+
index,
|
|
57
|
+
id,
|
|
58
|
+
slots: error.slots,
|
|
59
|
+
error
|
|
60
|
+
};
|
|
61
|
+
if (error instanceof UnmeasuredSlotsRefusedError) return {
|
|
62
|
+
index,
|
|
63
|
+
id,
|
|
64
|
+
slots: error.keys.map((key) => ({
|
|
65
|
+
key,
|
|
66
|
+
state: "unmeasured"
|
|
67
|
+
})),
|
|
68
|
+
error
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
index,
|
|
72
|
+
id,
|
|
73
|
+
slots: [],
|
|
74
|
+
error
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function entryLabel(entry) {
|
|
78
|
+
return entry.id === String(entry.index) ? entry.id : `"${entry.id}"`;
|
|
79
|
+
}
|
|
80
|
+
function describeRefusal(entry) {
|
|
81
|
+
if (entry.error instanceof SlotFillRefusedError) return `${entryLabel(entry)} (scripts: '${entry.error.scripts}')`;
|
|
82
|
+
if (entry.slots.length === 0) return entryLabel(entry);
|
|
83
|
+
return `${entryLabel(entry)} ${entry.slots.map(({ key, state }) => `"${key}" (${state})`).join(", ")}`;
|
|
84
|
+
}
|
|
85
|
+
function clearsFor(entries) {
|
|
86
|
+
let required = false;
|
|
87
|
+
let unmeasured = false;
|
|
88
|
+
let scripts = false;
|
|
89
|
+
for (const entry of entries) if (entry.error instanceof RequiredSlotsRefusedError) required = true;
|
|
90
|
+
else if (entry.error instanceof UnmeasuredSlotsRefusedError) unmeasured = true;
|
|
91
|
+
else if (entry.error instanceof SlotFillRefusedError) scripts = true;
|
|
92
|
+
return [
|
|
93
|
+
required ? "Fill required slots; for an overflowed refuse slot, shorten its value or change the slot's overflow." : "",
|
|
94
|
+
unmeasured ? "Ensure each filled slot frame becomes ready before the output deadline, or change the slot's overflow from 'refuse'." : "",
|
|
95
|
+
scripts ? "Set the item's scripts to 'refuse', or show it without a fill." : ""
|
|
96
|
+
].filter(Boolean).join(" ");
|
|
97
|
+
}
|
|
98
|
+
function slotFillsRefusedMessage(entries) {
|
|
99
|
+
if (entries.length === 0) return "Output was refused because slots.fills has no entries. Pass at least one fill, or omit fills, to clear this refusal.";
|
|
100
|
+
return `Output was refused because fills prevent printing: ${entries.map(describeRefusal).join("; ")}. ${clearsFor(entries)}`;
|
|
101
|
+
}
|
|
102
|
+
async function renderOneCopy(options, entry, index, renderCopy) {
|
|
103
|
+
let lastReport;
|
|
104
|
+
const capture = options.report ? (report) => {
|
|
105
|
+
lastReport = report;
|
|
106
|
+
} : void 0;
|
|
107
|
+
try {
|
|
108
|
+
const value = await renderCopy(copySlots(options, entry, capture));
|
|
109
|
+
options.report?.(lastReport ?? emptyReport(entry));
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
value
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (!isSlotCopyRefusal(error)) throw error;
|
|
116
|
+
options.report?.(lastReport ?? emptyReport(entry));
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
refusal: refusalFrom(error, entry, index)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Runs one print/PDF copy per `fills` entry, in order. Without `fills`, runs once with the given options.
|
|
125
|
+
* Host `report` is called once per entry, in entry order, including refused copies.
|
|
126
|
+
*/
|
|
127
|
+
async function collectSlotPrintCopies(options, renderCopy) {
|
|
128
|
+
assertSlotPrintOptions(options);
|
|
129
|
+
if (!options?.fills) return [await renderCopy(options ?? {})];
|
|
130
|
+
if (options.fills.length === 0) throw new SlotFillsRefusedError([]);
|
|
131
|
+
const kept = [];
|
|
132
|
+
const refused = [];
|
|
133
|
+
for (const [index, entry] of options.fills.entries()) {
|
|
134
|
+
const outcome = await renderOneCopy(options, entry, index, renderCopy);
|
|
135
|
+
if (outcome.ok) kept.push(outcome.value);
|
|
136
|
+
else refused.push(outcome.refusal);
|
|
137
|
+
}
|
|
138
|
+
if (refused.length > 0 && (!options.skipRefused || kept.length === 0)) throw new SlotFillsRefusedError(refused);
|
|
139
|
+
return kept;
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
export { SlotFillsConflictError, SlotFillsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, collectSlotPrintCopies, isSlotCopyRefusal, slotFillCopyId, slotFillsConflict };
|
package/dist/slots.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
|
|
1
2
|
import { Scripts, SlotDeclaration, SlotReport, SlotsOptions } from "@segnavia/format";
|
|
2
3
|
//#region src/slots.d.ts
|
|
3
4
|
export declare const MAX_SLOT_DECLARATIONS = 200;
|
|
4
5
|
export interface PreparedSlots {
|
|
6
|
+
declarations: SlotDeclaration[];
|
|
5
7
|
report: SlotReport;
|
|
8
|
+
/** For each slot key, how many applications (one per rendered frame) marked it for measurement. */
|
|
9
|
+
marked: ReadonlyMap<string, number>;
|
|
6
10
|
apply(html: string): string;
|
|
11
|
+
mergeFit(slots: Record<string, 'fit' | 'shrunk' | 'overflowed'>): SlotReport;
|
|
7
12
|
}
|
|
8
13
|
export declare class SlotFillRefusedError extends Error {
|
|
14
|
+
readonly scripts: Exclude<Scripts, 'refuse'>;
|
|
9
15
|
constructor(scripts: Exclude<Scripts, 'refuse'>);
|
|
10
16
|
}
|
|
11
17
|
/** Whether an item needs slot preparation for declarations, filling, asset resolution or reporting. */
|
|
@@ -16,4 +22,5 @@ export declare function slotFillRefusal(scripts: Scripts, declarations: readonly
|
|
|
16
22
|
export declare function prepareSlots(declarations: readonly SlotDeclaration[] | undefined, options?: SlotsOptions, signal?: AbortSignal): Promise<PreparedSlots>;
|
|
17
23
|
/** Prepares one shared fill for a collection, merging each item's declarations exactly once. */
|
|
18
24
|
export declare function prepareSlotCollection(groups: ReadonlyArray<readonly SlotDeclaration[] | undefined>, options?: SlotsOptions, signal?: AbortSignal): Promise<PreparedSlots>;
|
|
19
|
-
//#endregion
|
|
25
|
+
//#endregion
|
|
26
|
+
export { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal };
|
package/dist/slots.js
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { elements, parse, serialize } from "./sanitize.js";
|
|
2
2
|
import { bindingNames, hasBindings } from "./bindings.js";
|
|
3
|
+
import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
|
|
4
|
+
import { qrPayload, qrSvg } from "./qr.js";
|
|
3
5
|
import { Text, isTag } from "domhandler";
|
|
4
6
|
import { SlotsInput, mergeSlotDeclarations } from "@segnavia/format";
|
|
5
7
|
//#region src/slots.ts
|
|
6
8
|
const MAX_SLOT_DECLARATIONS = 200;
|
|
7
9
|
const ASSET_RESOLUTION_CONCURRENCY = 4;
|
|
8
10
|
var SlotFillRefusedError = class extends Error {
|
|
11
|
+
scripts;
|
|
9
12
|
constructor(scripts) {
|
|
10
13
|
super(`A fill was not applied because this item runs its own scripts (scripts: '${scripts}'), which could read the filled values. Set the item's scripts to 'refuse', or show it without a fill.`);
|
|
11
14
|
this.name = "SlotFillRefusedError";
|
|
15
|
+
this.scripts = scripts;
|
|
12
16
|
}
|
|
13
17
|
};
|
|
14
18
|
/** Whether an item needs slot preparation for declarations, filling, asset resolution or reporting. */
|
|
15
19
|
function hasSlotWork(declarations, options) {
|
|
16
|
-
return Boolean(declarations?.length || options?.declarations?.length || options?.fill || options?.resolveAsset || options?.report);
|
|
20
|
+
return Boolean(declarations?.length || options?.declarations?.length || options?.fill || options?.source || options?.resolveAsset || options?.report);
|
|
17
21
|
}
|
|
18
22
|
/** Refuses exposing filled values to an item's own scripts. */
|
|
19
23
|
function slotFillRefusal(scripts, declarations, options) {
|
|
20
|
-
const fillsValues = options?.fill && (declarations?.length || options.declarations?.length);
|
|
24
|
+
const fillsValues = (options?.fill || options?.source) && (declarations?.length || options.declarations?.length);
|
|
21
25
|
return scripts !== "refuse" && fillsValues ? new SlotFillRefusedError(scripts) : void 0;
|
|
22
26
|
}
|
|
23
27
|
const UNENCODED_TEXT_ELEMENTS = /* @__PURE__ */ new Set([
|
|
@@ -32,8 +36,23 @@ const UNENCODED_TEXT_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
32
36
|
"textarea",
|
|
33
37
|
"title"
|
|
34
38
|
]);
|
|
39
|
+
const VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
40
|
+
"area",
|
|
41
|
+
"base",
|
|
42
|
+
"br",
|
|
43
|
+
"col",
|
|
44
|
+
"embed",
|
|
45
|
+
"hr",
|
|
46
|
+
"img",
|
|
47
|
+
"input",
|
|
48
|
+
"link",
|
|
49
|
+
"meta",
|
|
50
|
+
"source",
|
|
51
|
+
"track",
|
|
52
|
+
"wbr"
|
|
53
|
+
]);
|
|
35
54
|
function refusesValueBinding(tag) {
|
|
36
|
-
return UNENCODED_TEXT_ELEMENTS.has(tag);
|
|
55
|
+
return UNENCODED_TEXT_ELEMENTS.has(tag) || VOID_ELEMENTS.has(tag);
|
|
37
56
|
}
|
|
38
57
|
function assertDeclarationLimit(declarations, subject) {
|
|
39
58
|
if (declarations.length > 200) throw new RangeError(`${subject} declares ${declarations.length} slots; the limit is 200.`);
|
|
@@ -49,12 +68,18 @@ function mergedDeclarations(groups, options) {
|
|
|
49
68
|
});
|
|
50
69
|
return mergeSlotDeclarations(parsedHostDeclarations(options), ...groups);
|
|
51
70
|
}
|
|
52
|
-
function
|
|
71
|
+
function applyValue(element, name, resolved) {
|
|
53
72
|
const path = element.attribs[name];
|
|
54
73
|
if (path === void 0) return;
|
|
55
74
|
delete element.attribs[name];
|
|
56
|
-
const value = data[path];
|
|
75
|
+
const value = resolved.data[path];
|
|
57
76
|
if (value === void 0) return;
|
|
77
|
+
if (resolved.types[path] === "qr") {
|
|
78
|
+
const svg = qrSvg(value);
|
|
79
|
+
svg.parent = element;
|
|
80
|
+
element.children = [svg];
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
58
83
|
const text = new Text(value);
|
|
59
84
|
text.parent = element;
|
|
60
85
|
element.children = [text];
|
|
@@ -65,7 +90,7 @@ function invalidBinding(element, names, report) {
|
|
|
65
90
|
const value = element.attribs[names.value];
|
|
66
91
|
if (value !== void 0 && report.slots[value] !== void 0 && refusesValueBinding(tag)) {
|
|
67
92
|
report.slots[value] = "invalid";
|
|
68
|
-
return `Slot "${value}" uses a value binding on <${tag}>, whose
|
|
93
|
+
return `Slot "${value}" uses a value binding on <${tag}>, whose content cannot be filled safely.`;
|
|
69
94
|
}
|
|
70
95
|
const src = element.attribs[names.src];
|
|
71
96
|
if (src !== void 0 && report.slots[src] !== void 0 && tag !== "img") {
|
|
@@ -92,11 +117,42 @@ function applyUrl(element, attribute, name, resolved) {
|
|
|
92
117
|
if (resolved.linkOrigins.has(new URL(value).origin)) element.attribs.href = value;
|
|
93
118
|
} catch {}
|
|
94
119
|
}
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
function applyPresent(element, name, report) {
|
|
121
|
+
const path = element.attribs[name];
|
|
122
|
+
if (path === void 0) return true;
|
|
123
|
+
delete element.attribs[name];
|
|
124
|
+
return report.fill === null || report.slots[path] === "filled";
|
|
125
|
+
}
|
|
126
|
+
function markOverflow(element, names, resolved) {
|
|
127
|
+
if (resolved.report.fill === null) return;
|
|
128
|
+
for (const binding of Object.values(names)) {
|
|
129
|
+
const key = element.attribs[binding];
|
|
130
|
+
if (key === void 0) continue;
|
|
131
|
+
const declaration = resolved.declarations.get(key);
|
|
132
|
+
const overflow = declaration?.overflow ?? "wrap";
|
|
133
|
+
if (!declaration || overflow === "wrap" || resolved.report.slots[key] !== "filled") continue;
|
|
134
|
+
element.attribs["data-slot-overflow"] = `${encodeURIComponent(key)}:${overflow}${overflow === "shrink" ? `:${declaration.minSize ?? 6}` : ""}`;
|
|
135
|
+
resolved.markedHere.add(key);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function applyElements(elements, names, resolved) {
|
|
140
|
+
return elements.flatMap((element) => {
|
|
141
|
+
if (!applyPresent(element, names.present, resolved.report)) return [];
|
|
142
|
+
delete element.attribs["data-slot-overflow"];
|
|
143
|
+
markOverflow(element, names, resolved);
|
|
144
|
+
applyValue(element, names.value, resolved);
|
|
145
|
+
applyUrl(element, "href", names.href, resolved);
|
|
146
|
+
applyUrl(element, "src", names.src, resolved);
|
|
147
|
+
element.children = element.children.flatMap((child) => {
|
|
148
|
+
if (!isTag(child)) return [child];
|
|
149
|
+
return applyElements([child], names, resolved).map((kept) => {
|
|
150
|
+
kept.parent = element;
|
|
151
|
+
return kept;
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
return [element];
|
|
155
|
+
});
|
|
100
156
|
}
|
|
101
157
|
function bindingRefusal(roots, names, resolved) {
|
|
102
158
|
const reasons = [];
|
|
@@ -112,6 +168,7 @@ function bindingRefusal(roots, names, resolved) {
|
|
|
112
168
|
}
|
|
113
169
|
function validString(type, value) {
|
|
114
170
|
if (type === "email") return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
171
|
+
if (type === "qr") return qrPayload(value) !== void 0;
|
|
115
172
|
if (type === "link") try {
|
|
116
173
|
return new URL(value).protocol === "https:";
|
|
117
174
|
} catch {
|
|
@@ -174,14 +231,32 @@ async function valueFor(slot, value, resolveAsset) {
|
|
|
174
231
|
state: "filled"
|
|
175
232
|
} : { state: "invalid" };
|
|
176
233
|
}
|
|
234
|
+
function layeredValue(key, options) {
|
|
235
|
+
const hand = options.fill?.values[key];
|
|
236
|
+
if (hand !== void 0 && hand !== "") return {
|
|
237
|
+
value: hand,
|
|
238
|
+
origin: "hand"
|
|
239
|
+
};
|
|
240
|
+
const source = options.source?.values[key];
|
|
241
|
+
if (source !== void 0 && source !== "") return {
|
|
242
|
+
value: source,
|
|
243
|
+
origin: "source"
|
|
244
|
+
};
|
|
245
|
+
return {};
|
|
246
|
+
}
|
|
177
247
|
async function resolveSlots(declarations, options, signal) {
|
|
178
248
|
const data = {};
|
|
249
|
+
const types = {};
|
|
179
250
|
const states = {};
|
|
251
|
+
const origins = {};
|
|
180
252
|
const linkOrigins = /* @__PURE__ */ new Set();
|
|
181
253
|
const resolveAsset = assetResolver(options, signal);
|
|
182
254
|
await Promise.all(declarations.map(async (slot) => {
|
|
183
|
-
|
|
255
|
+
types[slot.key] = slot.type;
|
|
256
|
+
const selected = layeredValue(slot.key, options);
|
|
257
|
+
const resolved = await valueFor(slot, selected.value, resolveAsset);
|
|
184
258
|
states[slot.key] = resolved.state;
|
|
259
|
+
if (selected.origin) origins[slot.key] = selected.origin;
|
|
185
260
|
if (resolved.value !== void 0) {
|
|
186
261
|
data[slot.key] = resolved.value;
|
|
187
262
|
if (slot.type === "link" && resolved.state === "filled") linkOrigins.add(new URL(resolved.value).origin);
|
|
@@ -189,41 +264,58 @@ async function resolveSlots(declarations, options, signal) {
|
|
|
189
264
|
}));
|
|
190
265
|
signal.throwIfAborted();
|
|
191
266
|
const report = {
|
|
192
|
-
fill: options.fill?.id ?? null,
|
|
193
|
-
slots: states
|
|
267
|
+
fill: options.fill?.id ?? options.source?.id ?? null,
|
|
268
|
+
slots: states,
|
|
269
|
+
origins
|
|
194
270
|
};
|
|
195
271
|
options.report?.(report);
|
|
196
272
|
return {
|
|
197
273
|
data,
|
|
274
|
+
types,
|
|
198
275
|
report,
|
|
199
276
|
linkOrigins,
|
|
200
|
-
reportState: options.report
|
|
277
|
+
reportState: options.report,
|
|
278
|
+
declarations: new Map(declarations.map((slot) => [slot.key, slot])),
|
|
279
|
+
marked: /* @__PURE__ */ new Map(),
|
|
280
|
+
markedHere: /* @__PURE__ */ new Set()
|
|
201
281
|
};
|
|
202
282
|
}
|
|
203
283
|
function applySlots(html, resolved) {
|
|
204
|
-
if (
|
|
284
|
+
if (!hasBindings(html, "slot") && !html.includes("data-slot-overflow")) return html;
|
|
205
285
|
const names = bindingNames("slot");
|
|
206
286
|
const document = parse(html);
|
|
207
|
-
const bound = elements(document.children).filter((element) => Object.values(names).some((name) => element.attribs[name] !== void 0));
|
|
287
|
+
const bound = elements(document.children).filter((element) => element.attribs["data-slot-overflow"] !== void 0 || Object.values(names).some((name) => element.attribs[name] !== void 0));
|
|
208
288
|
const roots = bound.filter((element) => {
|
|
209
289
|
for (let parent = element.parent; parent; parent = parent.parent) if (isTag(parent) && bound.includes(parent)) return false;
|
|
210
290
|
return true;
|
|
211
291
|
});
|
|
212
292
|
const refusal = bindingRefusal(roots, names, resolved);
|
|
213
293
|
if (refusal) throw refusal;
|
|
214
|
-
|
|
294
|
+
resolved.markedHere.clear();
|
|
295
|
+
const kept = applyElements(roots, names, resolved);
|
|
296
|
+
for (const key of resolved.markedHere) resolved.marked.set(key, (resolved.marked.get(key) ?? 0) + 1);
|
|
215
297
|
return roots.map((root) => ({
|
|
216
298
|
start: root.startIndex,
|
|
217
299
|
end: root.endIndex,
|
|
218
|
-
html: serialize(root)
|
|
300
|
+
html: kept.includes(root) ? serialize(root) : ""
|
|
219
301
|
})).filter((part) => part.start !== null && part.end !== null).sort((a, b) => b.start - a.start).reduce((output, part) => `${output.slice(0, part.start)}${part.html}${output.slice(part.end + 1)}`, html);
|
|
220
302
|
}
|
|
221
303
|
async function prepared(declarations, options, signal) {
|
|
222
304
|
const resolved = await resolveSlots(declarations, options, signal);
|
|
223
|
-
|
|
305
|
+
const result = {
|
|
306
|
+
declarations,
|
|
224
307
|
report: resolved.report,
|
|
225
|
-
|
|
308
|
+
marked: resolved.marked,
|
|
309
|
+
apply: (html) => applySlots(html, resolved),
|
|
310
|
+
mergeFit: (slots) => {
|
|
311
|
+
const report = mergeSlotFitResults(declarations, result.report, slots);
|
|
312
|
+
result.report = report;
|
|
313
|
+
resolved.report = report;
|
|
314
|
+
options.report?.(report);
|
|
315
|
+
return report;
|
|
316
|
+
}
|
|
226
317
|
};
|
|
318
|
+
return result;
|
|
227
319
|
}
|
|
228
320
|
/** Prepares and applies the slots declared by one item. */
|
|
229
321
|
async function prepareSlots(declarations, options = {}, signal = new AbortController().signal) {
|
|
@@ -234,4 +326,4 @@ async function prepareSlotCollection(groups, options = {}, signal = new AbortCon
|
|
|
234
326
|
return prepared(mergedDeclarations(groups.map((group) => group ?? []), options), options, signal);
|
|
235
327
|
}
|
|
236
328
|
//#endregion
|
|
237
|
-
export { MAX_SLOT_DECLARATIONS, SlotFillRefusedError, hasSlotWork, prepareSlotCollection, prepareSlots, slotFillRefusal };
|
|
329
|
+
export { MAX_SLOT_DECLARATIONS, RequiredSlotsRefusedError, SlotFillRefusedError, UnmeasuredSlotsRefusedError, hasSlotWork, mergeSlotFitResults, prepareSlotCollection, prepareSlots, requiredSlotRefusal, slotFillRefusal, unmeasuredSlotRefusal };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segnavia/html",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@segnavia/format": "0.
|
|
30
|
-
"
|
|
29
|
+
"@segnavia/format": "0.24.0",
|
|
30
|
+
"dom-serializer": "^2.0.0",
|
|
31
31
|
"domhandler": "^5.0.3",
|
|
32
|
-
"
|
|
32
|
+
"htmlparser2": "^10.0.0",
|
|
33
|
+
"qrcode-generator": "2.0.4"
|
|
33
34
|
}
|
|
34
35
|
}
|