@segnavia/html 0.26.0 → 0.27.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/base64.d.ts +4 -0
- package/dist/base64.js +51 -0
- package/dist/bindings.d.ts +3 -0
- package/dist/bindings.js +15 -1
- package/dist/control-char.js +10 -0
- package/dist/font-css.d.ts +11 -0
- package/dist/font-css.js +75 -0
- package/dist/font-format.d.ts +4 -0
- package/dist/font-format.js +11 -0
- package/dist/font-references.d.ts +4 -0
- package/dist/font-references.js +192 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +9 -2
- package/dist/motifs.d.ts +31 -0
- package/dist/motifs.js +241 -0
- package/dist/printed-pages.d.ts +28 -0
- package/dist/printed-pages.js +75 -0
- package/dist/unicode-range.d.ts +11 -0
- package/dist/unicode-range.js +86 -0
- package/package.json +2 -2
package/dist/base64.d.ts
ADDED
package/dist/base64.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/base64.ts
|
|
2
|
+
const TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
3
|
+
function triplet(data, index, count) {
|
|
4
|
+
let value = 0;
|
|
5
|
+
for (let offset = 0; offset < 3; offset += 1) {
|
|
6
|
+
const byte = offset < count ? data[index + offset] : 0;
|
|
7
|
+
value = value << 8 | (byte ?? 0);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function encodeTriplet(value, chars) {
|
|
12
|
+
const shifts = [
|
|
13
|
+
18,
|
|
14
|
+
12,
|
|
15
|
+
6,
|
|
16
|
+
0
|
|
17
|
+
];
|
|
18
|
+
let out = "";
|
|
19
|
+
for (let i = 0; i < 4; i += 1) {
|
|
20
|
+
const shift = shifts[i] ?? 0;
|
|
21
|
+
out += i < chars ? TABLE[value >> shift & 63] ?? "" : "=";
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function bytesToBase64(data) {
|
|
26
|
+
let out = "";
|
|
27
|
+
const full = Math.floor(data.length / 3) * 3;
|
|
28
|
+
for (let i = 0; i < full; i += 3) out += encodeTriplet(triplet(data, i, 3), 4);
|
|
29
|
+
const rest = data.length - full;
|
|
30
|
+
if (rest > 0) out += encodeTriplet(triplet(data, full, rest), rest + 1);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function base64ToBytes(value) {
|
|
34
|
+
if (value.length % 4 !== 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) return;
|
|
35
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
36
|
+
const bytes = new Uint8Array(value.length / 4 * 3 - padding);
|
|
37
|
+
let output = 0;
|
|
38
|
+
for (let index = 0; index < value.length; index += 4) {
|
|
39
|
+
const a = TABLE.indexOf(value[index] ?? "");
|
|
40
|
+
const b = TABLE.indexOf(value[index + 1] ?? "");
|
|
41
|
+
const c = value[index + 2] === "=" ? 0 : TABLE.indexOf(value[index + 2] ?? "");
|
|
42
|
+
const d = value[index + 3] === "=" ? 0 : TABLE.indexOf(value[index + 3] ?? "");
|
|
43
|
+
const triplet = a << 18 | b << 12 | c << 6 | d;
|
|
44
|
+
if (output < bytes.length) bytes[output++] = triplet >> 16 & 255;
|
|
45
|
+
if (output < bytes.length) bytes[output++] = triplet >> 8 & 255;
|
|
46
|
+
if (output < bytes.length) bytes[output++] = triplet & 255;
|
|
47
|
+
}
|
|
48
|
+
return bytes;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { base64ToBytes, bytesToBase64 };
|
package/dist/bindings.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { SanitizeOptions } from "./sanitize.js";
|
|
2
2
|
//#region src/bindings.d.ts
|
|
3
3
|
export declare const BINDINGS: readonly ["value", "href", "src", "each", "when", "present", "width"];
|
|
4
|
+
export declare function bindingRoot(path: string): string;
|
|
4
5
|
export declare function bindingNames(prefix: string): Record<(typeof BINDINGS)[number], string>;
|
|
5
6
|
export interface FillOptions extends Pick<SanitizeOptions, 'linkOrigins'> {
|
|
6
7
|
prefix: string;
|
|
7
8
|
data: Record<string, unknown>;
|
|
8
9
|
unavailable?: Iterable<string>;
|
|
9
10
|
preserveUnavailable?: boolean;
|
|
11
|
+
registerHrefOrigins?: boolean;
|
|
12
|
+
deferPaths?: Iterable<string>;
|
|
10
13
|
}
|
|
11
14
|
export interface Filled {
|
|
12
15
|
html: string;
|
package/dist/bindings.js
CHANGED
|
@@ -10,6 +10,9 @@ const BINDINGS = [
|
|
|
10
10
|
"present",
|
|
11
11
|
"width"
|
|
12
12
|
];
|
|
13
|
+
function bindingRoot(path) {
|
|
14
|
+
return path.replace(/^(?:\.\.\/)+/, "").split(".")[0] ?? "";
|
|
15
|
+
}
|
|
13
16
|
function bindingNames(prefix) {
|
|
14
17
|
return Object.fromEntries(BINDINGS.map((binding) => [binding, `data-${prefix}-${binding}`]));
|
|
15
18
|
}
|
|
@@ -57,6 +60,13 @@ function safeHref(value, origins) {
|
|
|
57
60
|
return false;
|
|
58
61
|
}
|
|
59
62
|
}
|
|
63
|
+
function registerHrefOrigin(origins, attribute, value, register) {
|
|
64
|
+
if (attribute !== "href" || !register || typeof value !== "string") return;
|
|
65
|
+
try {
|
|
66
|
+
const url = new URL(value);
|
|
67
|
+
if (url.protocol === "https:") origins.add(url.origin);
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
60
70
|
function hasBindings(html, prefix) {
|
|
61
71
|
const names = new Set(Object.values(bindingNames(prefix)));
|
|
62
72
|
return elements(parse(html).children).some((element) => Object.keys(element.attribs).some((name) => names.has(name)));
|
|
@@ -66,6 +76,7 @@ function fill(html, options) {
|
|
|
66
76
|
const origins = new Set([...options.linkOrigins ?? []].map((origin) => new URL(origin).origin));
|
|
67
77
|
const unavailable = new Set(options.unavailable ?? []);
|
|
68
78
|
const values = [];
|
|
79
|
+
const deferred = new Set(options.deferPaths ?? []);
|
|
69
80
|
const document = parse(sanitize(html, { linkOrigins: options.linkOrigins }));
|
|
70
81
|
const resolve = (path, stack) => {
|
|
71
82
|
const { context, rest } = contextAt(stack, path);
|
|
@@ -75,6 +86,8 @@ function fill(html, options) {
|
|
|
75
86
|
};
|
|
76
87
|
const take = (element, name) => {
|
|
77
88
|
const value = element.attribs[name];
|
|
89
|
+
const root = value === void 0 ? void 0 : bindingRoot(value);
|
|
90
|
+
if (root && deferred.has(root)) return void 0;
|
|
78
91
|
delete element.attribs[name];
|
|
79
92
|
return value;
|
|
80
93
|
};
|
|
@@ -113,6 +126,7 @@ function fill(html, options) {
|
|
|
113
126
|
if (path === void 0) continue;
|
|
114
127
|
const value = resolve(path, stack);
|
|
115
128
|
if (preserves(value)) continue;
|
|
129
|
+
registerHrefOrigin(origins, attribute, value, options.registerHrefOrigins);
|
|
116
130
|
if (typeof value === "string" && (attribute === "href" ? safeHref(value, origins) : /^data:image\//i.test(value))) element.attribs[attribute] = value;
|
|
117
131
|
else {
|
|
118
132
|
delete element.attribs[attribute];
|
|
@@ -155,4 +169,4 @@ function fill(html, options) {
|
|
|
155
169
|
};
|
|
156
170
|
}
|
|
157
171
|
//#endregion
|
|
158
|
-
export { BINDINGS, bindingNames, fill, hasBindings };
|
|
172
|
+
export { BINDINGS, bindingNames, bindingRoot, fill, hasBindings };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/control-char.ts
|
|
2
|
+
function hasControlChar(value) {
|
|
3
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
4
|
+
const code = value.charCodeAt(i);
|
|
5
|
+
if (code <= 31 || code === 127) return true;
|
|
6
|
+
}
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { hasControlChar };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AppFont, FontFace } from "@segnavia/format";
|
|
2
|
+
//#region src/font-css.d.ts
|
|
3
|
+
export declare class FontCssRefusedError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function fontFaceCss(font: FontFace): string;
|
|
7
|
+
export declare function fontCss(options: {
|
|
8
|
+
app: Map<string, AppFont>;
|
|
9
|
+
local?: readonly FontFace[];
|
|
10
|
+
}): string;
|
|
11
|
+
//#endregion
|
package/dist/font-css.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { bytesToBase64 } from "./base64.js";
|
|
2
|
+
import { hasControlChar } from "./control-char.js";
|
|
3
|
+
import { UNICODE_RANGE_CLEARANCE, parseUnicodeRange } from "./unicode-range.js";
|
|
4
|
+
import { APP_FONT_NAME, FONT_FORMATS } from "@segnavia/format";
|
|
5
|
+
//#region src/font-css.ts
|
|
6
|
+
var FontCssRefusedError = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "FontCssRefusedError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const CSS_WEIGHT_KEYWORD = /^(?:normal|bold|bolder|lighter)$/;
|
|
13
|
+
const CSS_WEIGHT_NUMBER = /^(?:[1-9]\d{0,2}|1000)$/;
|
|
14
|
+
function mimeType(format) {
|
|
15
|
+
if (format === "truetype") return "ttf";
|
|
16
|
+
if (format === "opentype") return "otf";
|
|
17
|
+
return format;
|
|
18
|
+
}
|
|
19
|
+
function unicodeRangeDeclaration(font) {
|
|
20
|
+
const range = font.unicodeRange;
|
|
21
|
+
if (range === void 0) return "";
|
|
22
|
+
const refused = (reason) => new FontCssRefusedError(`The unicode-range ${JSON.stringify(range)} on face ${JSON.stringify(font.family)} (${font.weight ?? "normal"}, ${font.style ?? "normal"}) is malformed: ${reason}. Supply a CSS unicode-range list (${UNICODE_RANGE_CLEARANCE}) to clear this refusal.`);
|
|
23
|
+
if (typeof range !== "string") throw refused(`it is not a string (${typeof range})`);
|
|
24
|
+
const parsed = parseUnicodeRange(range);
|
|
25
|
+
if (!parsed.ok) throw refused(parsed.reason);
|
|
26
|
+
return `unicode-range:${parsed.value};`;
|
|
27
|
+
}
|
|
28
|
+
function faceRefusal(font, problem, remedy) {
|
|
29
|
+
return new FontCssRefusedError(`The face ${JSON.stringify(font.family)} could not establish ${problem}; ${remedy} to clear this refusal.`);
|
|
30
|
+
}
|
|
31
|
+
function isCssWeight(value) {
|
|
32
|
+
if (value === void 0) return true;
|
|
33
|
+
if (typeof value === "number") return Number.isInteger(value) && value >= 1 && value <= 1e3;
|
|
34
|
+
return CSS_WEIGHT_KEYWORD.test(value) || CSS_WEIGHT_NUMBER.test(value) && Number(value) >= 1 && Number(value) <= 1e3;
|
|
35
|
+
}
|
|
36
|
+
function isFontFormat(value) {
|
|
37
|
+
return FONT_FORMATS.some((format) => format === value);
|
|
38
|
+
}
|
|
39
|
+
function quoteFamily(family) {
|
|
40
|
+
if (hasControlChar(family)) throw new FontCssRefusedError(`The family ${JSON.stringify(family)} contains a control character; remove control characters from the family name to clear this refusal.`);
|
|
41
|
+
let quoted = "\"";
|
|
42
|
+
for (const ch of family) if (ch === "\\" || ch === "\"") quoted += `\\${ch}`;
|
|
43
|
+
else if (ch === "<") quoted += "\\3c ";
|
|
44
|
+
else quoted += ch;
|
|
45
|
+
return `${quoted}"`;
|
|
46
|
+
}
|
|
47
|
+
function validatedFace(font) {
|
|
48
|
+
if (!isFontFormat(font.format)) throw faceRefusal(font, `a font format that is one of ${FONT_FORMATS.join(", ")}`, `supply one of ${FONT_FORMATS.join(", ")}`);
|
|
49
|
+
if (!isCssWeight(font.weight)) throw faceRefusal(font, "a CSS font-weight", "supply a number 1-1000 or a CSS weight keyword");
|
|
50
|
+
if (font.style !== void 0 && font.style !== "normal" && font.style !== "italic") throw faceRefusal(font, "a font-style of 'normal' or 'italic'", "supply 'normal' or 'italic'");
|
|
51
|
+
return font;
|
|
52
|
+
}
|
|
53
|
+
function fontFaceCss(font) {
|
|
54
|
+
const face = validatedFace(font);
|
|
55
|
+
return `@font-face{font-family:${quoteFamily(face.family)};src:url(data:font/${mimeType(face.format)};base64,${bytesToBase64(face.data)}) format(${JSON.stringify(face.format)});font-weight:${face.weight ?? "normal"};font-style:${face.style ?? "normal"};${unicodeRangeDeclaration(face)}font-display:block}`;
|
|
56
|
+
}
|
|
57
|
+
function customProperty(name, font) {
|
|
58
|
+
if (!APP_FONT_NAME.test(name)) throw new FontCssRefusedError(`The app font name ${JSON.stringify(name)} could not establish an app font name matching ${APP_FONT_NAME}; use a lowercase name of letters, digits and hyphens to clear this refusal.`);
|
|
59
|
+
const generic = font.generic ? `,${font.generic}` : "";
|
|
60
|
+
return `--segnavia-font-${name}:${quoteFamily(font.family)}${generic}`;
|
|
61
|
+
}
|
|
62
|
+
function fontCss(options) {
|
|
63
|
+
const faces = [];
|
|
64
|
+
const properties = [];
|
|
65
|
+
for (const [name, font] of options.app) {
|
|
66
|
+
faces.push(...font.faces);
|
|
67
|
+
properties.push(customProperty(name, font));
|
|
68
|
+
}
|
|
69
|
+
for (const font of options.local ?? []) faces.push(font);
|
|
70
|
+
const rules = faces.map(fontFaceCss).join("");
|
|
71
|
+
if (properties.length === 0) return rules;
|
|
72
|
+
return `${rules}:root{${properties.join(";")}}`;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { FontCssRefusedError, fontCss, fontFaceCss };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/font-format.ts
|
|
2
|
+
function fontFormat(data) {
|
|
3
|
+
if (data.length < 4) return;
|
|
4
|
+
const tag = String.fromCharCode(data[0] ?? 0, data[1] ?? 0, data[2] ?? 0, data[3] ?? 0);
|
|
5
|
+
if (tag === "wOF2") return "woff2";
|
|
6
|
+
if (tag === "wOFF") return "woff";
|
|
7
|
+
if (tag === "OTTO") return "opentype";
|
|
8
|
+
if (tag === "true" || data[0] === 0 && data[1] === 1 && data[2] === 0 && data[3] === 0) return "truetype";
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { fontFormat };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { elements, parse } from "./sanitize.js";
|
|
2
|
+
import { hasControlChar } from "./control-char.js";
|
|
3
|
+
import { APP_FONT_NAME } from "@segnavia/format";
|
|
4
|
+
//#region src/font-references.ts
|
|
5
|
+
const FONT_PROPERTY = /(?:^|[;{])\s*(font-family|font)\s*:([^;{}]+)/gi;
|
|
6
|
+
const FONT_PREFIX_KEYWORD = /^(?:italic|oblique|normal|small-caps|bold|bolder|lighter|(?:ultra-|extra-|semi-)?(?:condensed|expanded)|[1-9]00)\s+/i;
|
|
7
|
+
const FONT_SIZE = /^(?:xx-small|x-small|small|medium|large|x-large|xx-large|xxx-large|smaller|larger|\d+(?:\.\d+)?(?:px|em|rem|pt|pc|in|cm|mm|ex|ch|vw|vh|%))(?:\s*\/\s*(?:normal|\d+(?:\.\d+)?(?:px|em|rem|pt|pc|in|cm|mm|ex|ch|vw|vh|%)?))?\s*/i;
|
|
8
|
+
const FONT_VAR = /var\(\s*--segnavia-font-([A-Za-z0-9-]+)\s*\)/g;
|
|
9
|
+
function refusal(place, problem, remedy) {
|
|
10
|
+
return `${place} could not establish ${problem}; ${remedy} to clear this refusal.`;
|
|
11
|
+
}
|
|
12
|
+
function referencePlace(ref) {
|
|
13
|
+
if ("app" in ref) return `font reference {app:${JSON.stringify(ref.app)}}`;
|
|
14
|
+
return `font reference {local:${JSON.stringify(ref.local)}, fallback:${JSON.stringify(ref.fallback)}}`;
|
|
15
|
+
}
|
|
16
|
+
function appName(ref) {
|
|
17
|
+
return "app" in ref ? ref.app : void 0;
|
|
18
|
+
}
|
|
19
|
+
function localRef(ref) {
|
|
20
|
+
return "local" in ref ? {
|
|
21
|
+
local: ref.local,
|
|
22
|
+
fallback: ref.fallback
|
|
23
|
+
} : void 0;
|
|
24
|
+
}
|
|
25
|
+
function styleText(element) {
|
|
26
|
+
return element.children.map((child) => "data" in child ? child.data : "").join("");
|
|
27
|
+
}
|
|
28
|
+
function cssChunks(html) {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
for (const element of elements(parse(html).children)) {
|
|
31
|
+
const style = element.attribs.style;
|
|
32
|
+
if (style) chunks.push(style);
|
|
33
|
+
if (element.name.toLowerCase() === "style") chunks.push(styleText(element));
|
|
34
|
+
}
|
|
35
|
+
return chunks;
|
|
36
|
+
}
|
|
37
|
+
function skipSpace(value, index) {
|
|
38
|
+
let i = index;
|
|
39
|
+
while (i < value.length && /\s/.test(value[i] ?? "")) i += 1;
|
|
40
|
+
return i;
|
|
41
|
+
}
|
|
42
|
+
function readQuoted(value, index) {
|
|
43
|
+
const quote = value[index] ?? "";
|
|
44
|
+
let i = index + 1;
|
|
45
|
+
let text = "";
|
|
46
|
+
while (i < value.length) {
|
|
47
|
+
const ch = value[i] ?? "";
|
|
48
|
+
if (ch === "\\" && i + 1 < value.length) {
|
|
49
|
+
text += value[i + 1] ?? "";
|
|
50
|
+
i += 2;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === quote) return {
|
|
54
|
+
text,
|
|
55
|
+
next: i + 1
|
|
56
|
+
};
|
|
57
|
+
text += ch;
|
|
58
|
+
i += 1;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
text,
|
|
62
|
+
next: i
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function readVar(value, index) {
|
|
66
|
+
let depth = 0;
|
|
67
|
+
let i = index;
|
|
68
|
+
while (i < value.length) {
|
|
69
|
+
const ch = value[i] ?? "";
|
|
70
|
+
if (ch === "(") depth += 1;
|
|
71
|
+
if (ch === ")") {
|
|
72
|
+
depth -= 1;
|
|
73
|
+
if (depth === 0) return {
|
|
74
|
+
text: value.slice(index, i + 1).replace(/\s+/g, ""),
|
|
75
|
+
next: i + 1
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
i += 1;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
text: value.slice(index).replace(/\s+/g, ""),
|
|
82
|
+
next: value.length
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function readIdent(value, index) {
|
|
86
|
+
let i = index;
|
|
87
|
+
while (i < value.length && value[i] !== ",") i += 1;
|
|
88
|
+
return {
|
|
89
|
+
text: value.slice(index, i).trim(),
|
|
90
|
+
next: i
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function stripFontShorthand(value) {
|
|
94
|
+
let rest = value.trim();
|
|
95
|
+
while (FONT_PREFIX_KEYWORD.test(rest)) rest = rest.replace(FONT_PREFIX_KEYWORD, "");
|
|
96
|
+
if (!FONT_SIZE.test(rest)) return value.trim();
|
|
97
|
+
return rest.replace(FONT_SIZE, "");
|
|
98
|
+
}
|
|
99
|
+
function familyItems(value) {
|
|
100
|
+
const items = [];
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < value.length) {
|
|
103
|
+
i = skipSpace(value, i);
|
|
104
|
+
if (i >= value.length) break;
|
|
105
|
+
const next = value.startsWith("var(", i) ? readVar(value, i) : value[i] === "\"" || value[i] === "'" ? readQuoted(value, i) : readIdent(value, i);
|
|
106
|
+
if (next.text) items.push(next.text);
|
|
107
|
+
i = next.next;
|
|
108
|
+
i = skipSpace(value, i);
|
|
109
|
+
if (value[i] === ",") i += 1;
|
|
110
|
+
}
|
|
111
|
+
return items;
|
|
112
|
+
}
|
|
113
|
+
function familyLists(css) {
|
|
114
|
+
const lists = [];
|
|
115
|
+
FONT_PROPERTY.lastIndex = 0;
|
|
116
|
+
for (const match of css.matchAll(FONT_PROPERTY)) {
|
|
117
|
+
const property = (match[1] ?? "").toLowerCase();
|
|
118
|
+
const value = match[2] ?? "";
|
|
119
|
+
const items = familyItems(property === "font" ? stripFontShorthand(value) : value);
|
|
120
|
+
if (items.length > 0) lists.push(items);
|
|
121
|
+
}
|
|
122
|
+
return lists;
|
|
123
|
+
}
|
|
124
|
+
function appNames(references) {
|
|
125
|
+
const names = /* @__PURE__ */ new Set();
|
|
126
|
+
for (const ref of references) {
|
|
127
|
+
const name = appName(ref);
|
|
128
|
+
if (name) names.add(name);
|
|
129
|
+
}
|
|
130
|
+
return names;
|
|
131
|
+
}
|
|
132
|
+
function referenceRefusals(references, known) {
|
|
133
|
+
const refusals = [];
|
|
134
|
+
for (const ref of references) {
|
|
135
|
+
const app = appName(ref);
|
|
136
|
+
const local = localRef(ref);
|
|
137
|
+
const name = app ?? local?.fallback ?? "";
|
|
138
|
+
if (!APP_FONT_NAME.test(name)) {
|
|
139
|
+
refusals.push(refusal(referencePlace(ref), `an app font name matching ${APP_FONT_NAME}`, "use a lowercase name of letters, digits and hyphens"));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!known.has(name)) refusals.push(refusal(referencePlace(ref), `app font ${JSON.stringify(name)}`, "make that font available from the source"));
|
|
143
|
+
if (local && hasControlChar(local.local)) refusals.push(refusal(referencePlace(ref), "a local family without control characters", "remove control characters from the family name"));
|
|
144
|
+
}
|
|
145
|
+
return refusals;
|
|
146
|
+
}
|
|
147
|
+
function fallbackVar(fallback) {
|
|
148
|
+
return `var(--segnavia-font-${fallback})`;
|
|
149
|
+
}
|
|
150
|
+
function localCssRefusals(lists, references) {
|
|
151
|
+
const refusals = [];
|
|
152
|
+
for (const ref of references) {
|
|
153
|
+
const local = localRef(ref);
|
|
154
|
+
if (!local) continue;
|
|
155
|
+
const expected = fallbackVar(local.fallback);
|
|
156
|
+
for (const items of lists) {
|
|
157
|
+
const index = items.indexOf(local.local);
|
|
158
|
+
if (index < 0) continue;
|
|
159
|
+
const next = items[index + 1];
|
|
160
|
+
if (next === expected) continue;
|
|
161
|
+
const place = `page CSS font-family ${JSON.stringify(items.join(", "))}`;
|
|
162
|
+
if (!next?.startsWith("var(--segnavia-font-")) refusals.push(refusal(place, `the declared fallback after ${JSON.stringify(local.local)}`, `write font-family: ${JSON.stringify(local.local)}, ${expected}`));
|
|
163
|
+
else refusals.push(refusal(place, `fallback ${next} matching declared ${expected}`, `write font-family: ${JSON.stringify(local.local)}, ${expected}`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return refusals;
|
|
167
|
+
}
|
|
168
|
+
function undeclaredVarRefusals(chunks, declared) {
|
|
169
|
+
const refusals = [];
|
|
170
|
+
const seen = /* @__PURE__ */ new Set();
|
|
171
|
+
for (const css of chunks) {
|
|
172
|
+
FONT_VAR.lastIndex = 0;
|
|
173
|
+
for (const match of css.matchAll(FONT_VAR)) {
|
|
174
|
+
const name = match[1] ?? "";
|
|
175
|
+
if (declared.has(name) || seen.has(name)) continue;
|
|
176
|
+
seen.add(name);
|
|
177
|
+
refusals.push(refusal(`page CSS ${match[0]}`, `a fonts reference {app:${JSON.stringify(name)}}`, `add {app:${JSON.stringify(name)}} to the page or placed motif fonts`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return refusals;
|
|
181
|
+
}
|
|
182
|
+
function fontReferenceRefusals(html, references, known) {
|
|
183
|
+
const chunks = cssChunks(html);
|
|
184
|
+
const lists = chunks.flatMap(familyLists);
|
|
185
|
+
return [
|
|
186
|
+
...referenceRefusals(references, known),
|
|
187
|
+
...localCssRefusals(lists, references),
|
|
188
|
+
...undeclaredVarRefusals(chunks, appNames(references))
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
export { fontReferenceRefusals };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
import { base64ToBytes, bytesToBase64 } from "./base64.js";
|
|
1
2
|
import { REFUSED_ELEMENTS, SanitizeOptions, findActiveContent, sanitize } from "./sanitize.js";
|
|
2
|
-
import { BINDINGS, FillOptions, Filled, bindingNames, fill, hasBindings } from "./bindings.js";
|
|
3
|
+
import { BINDINGS, FillOptions, Filled, bindingNames, bindingRoot, fill, hasBindings } from "./bindings.js";
|
|
3
4
|
import { BookDocument, BookDocumentOptions, BookHeading, BookPrintPart, BookSectionInfo, bookPrintPart } from "./book/types.js";
|
|
4
5
|
import { bookDocument } from "./book/book-document.js";
|
|
6
|
+
import { FontCssRefusedError, fontCss, fontFaceCss } from "./font-css.js";
|
|
7
|
+
import { fontFormat } from "./font-format.js";
|
|
8
|
+
import { fontReferenceRefusals } from "./font-references.js";
|
|
9
|
+
import { ExpandMotifsOptions, ExpandedMotifs, MOTIF_DEPTH_LIMIT, MotifRefusedError, PlacedMotif, expandMotifs, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements } from "./motifs.js";
|
|
10
|
+
import { BookPageFacts, BookPrintedPagePlan, BookPrintedPageRole, PrintedPagePlan, PrintedPageSide, planBookPages, planPrintedPages, printedPageAt } from "./printed-pages.js";
|
|
5
11
|
import { SlotFillEntry, SlotFillRefusal, SlotFillsConflictError, SlotFillsRefusedError, SlotPrintOptions, assertSingleSlotCopy, assertSlotPrintOptions, collectSlotPrintCopies, isSlotCopyRefusal, slotFillCopyId, slotFillsConflict } from "./slot-print.js";
|
|
6
12
|
import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
|
|
7
13
|
import { MAX_SLOT_DECLARATIONS, PreparedSlots, SlotFillRefusedError, hasSlotWork, prepareSlotCollection, prepareSlots, slotFillRefusal } from "./slots.js";
|
|
14
|
+
import { UNICODE_RANGE_CLEARANCE, UnicodeRangeParse, parseUnicodeRange } from "./unicode-range.js";
|
|
8
15
|
//#region src/index.d.ts
|
|
9
16
|
export type HtmlKind = 'plain' | 'live-data' | 'scripted' | 'refused';
|
|
10
17
|
export declare function classify(html: string, prefix: string): HtmlKind;
|
|
11
18
|
export declare function searchText(html: string): string;
|
|
12
19
|
//#endregion
|
|
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 };
|
|
20
|
+
export { BINDINGS, type BookDocument, type BookDocumentOptions, type BookHeading, BookPageFacts, type BookPrintPart, BookPrintedPagePlan, BookPrintedPageRole, type BookSectionInfo, type ExpandMotifsOptions, type ExpandedMotifs, type FillOptions, type Filled, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, MotifRefusedError, type PlacedMotif, type PreparedSlots, PrintedPagePlan, PrintedPageSide, REFUSED_ELEMENTS, RequiredSlotsRefusedError, type SanitizeOptions, type SlotFillEntry, type SlotFillRefusal, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, type SlotPrintOptions, UNICODE_RANGE_CLEARANCE, type UnicodeRangeParse, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bookDocument, bookPrintPart, bytesToBase64, collectSlotPrintCopies, expandMotifs, fill, findActiveContent, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, hasBindings, hasMotifPlacement, hasSlotWork, isSlotCopyRefusal, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, parseUnicodeRange, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, sanitize, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { REFUSED_ELEMENTS, elements, findActiveContent, parse, sanitize } from "./sanitize.js";
|
|
2
|
-
import { BINDINGS, bindingNames, fill, hasBindings } from "./bindings.js";
|
|
2
|
+
import { BINDINGS, bindingNames, bindingRoot, fill, hasBindings } from "./bindings.js";
|
|
3
|
+
import { base64ToBytes, bytesToBase64 } from "./base64.js";
|
|
3
4
|
import { bookPrintPart } from "./book/types.js";
|
|
4
5
|
import { bookDocument } from "./book/book-document.js";
|
|
6
|
+
import { UNICODE_RANGE_CLEARANCE, parseUnicodeRange } from "./unicode-range.js";
|
|
7
|
+
import { FontCssRefusedError, fontCss, fontFaceCss } from "./font-css.js";
|
|
8
|
+
import { fontFormat } from "./font-format.js";
|
|
9
|
+
import { fontReferenceRefusals } from "./font-references.js";
|
|
10
|
+
import { MOTIF_DEPTH_LIMIT, MotifRefusedError, expandMotifs, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements } from "./motifs.js";
|
|
11
|
+
import { planBookPages, planPrintedPages, printedPageAt } from "./printed-pages.js";
|
|
5
12
|
import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
|
|
6
13
|
import { MAX_SLOT_DECLARATIONS, SlotFillRefusedError, hasSlotWork, prepareSlotCollection, prepareSlots, slotFillRefusal } from "./slots.js";
|
|
7
14
|
import { SlotFillsConflictError, SlotFillsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, collectSlotPrintCopies, isSlotCopyRefusal, slotFillCopyId, slotFillsConflict } from "./slot-print.js";
|
|
@@ -17,4 +24,4 @@ function searchText(html) {
|
|
|
17
24
|
return html.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
18
25
|
}
|
|
19
26
|
//#endregion
|
|
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 };
|
|
27
|
+
export { BINDINGS, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, MotifRefusedError, REFUSED_ELEMENTS, RequiredSlotsRefusedError, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, UNICODE_RANGE_CLEARANCE, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bookDocument, bookPrintPart, bytesToBase64, classify, collectSlotPrintCopies, expandMotifs, fill, findActiveContent, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, hasBindings, hasMotifPlacement, hasSlotWork, isSlotCopyRefusal, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, parseUnicodeRange, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, sanitize, searchText, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal };
|
package/dist/motifs.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { FontReference, MotifPlacement, MotifSource, Scripts } from "@segnavia/format";
|
|
2
|
+
//#region src/motifs.d.ts
|
|
3
|
+
export declare const MOTIF_DEPTH_LIMIT = 8;
|
|
4
|
+
export interface ExpandMotifsOptions {
|
|
5
|
+
source: MotifSource;
|
|
6
|
+
pins?: Record<string, string>;
|
|
7
|
+
lang?: string;
|
|
8
|
+
scripts: Scripts;
|
|
9
|
+
prefix: string;
|
|
10
|
+
linkOrigins?: Iterable<string>;
|
|
11
|
+
signal: AbortSignal;
|
|
12
|
+
deferPaths?: Iterable<string>;
|
|
13
|
+
}
|
|
14
|
+
export interface PlacedMotif extends MotifPlacement {
|
|
15
|
+
version: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ExpandedMotifs {
|
|
18
|
+
html: string;
|
|
19
|
+
placed: PlacedMotif[];
|
|
20
|
+
fonts: FontReference[];
|
|
21
|
+
unavailable: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare class MotifRefusedError extends Error {
|
|
24
|
+
constructor(message: string);
|
|
25
|
+
}
|
|
26
|
+
export declare function libraryMotifPlacementRefusal(html: string): string | undefined;
|
|
27
|
+
export declare function hasMotifPlacement(html: string): boolean;
|
|
28
|
+
export declare function renderMotifPlacements(html: string, replacement: (key: string) => string | undefined): string;
|
|
29
|
+
export declare function expandMotifs(html: string, options: ExpandMotifsOptions): Promise<ExpandedMotifs>;
|
|
30
|
+
export declare function motifPlacementRefusals(html: string, options: ExpandMotifsOptions): Promise<string[]>;
|
|
31
|
+
//#endregion
|
package/dist/motifs.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { elements, parse, sanitize, serialize } from "./sanitize.js";
|
|
2
|
+
import { bindingNames, bindingRoot, fill, hasBindings } from "./bindings.js";
|
|
3
|
+
import { isTag } from "domhandler";
|
|
4
|
+
import { LIBRARY_MOTIF_PREFIX, MOTIF_ATTRIBUTE, PRINTED_PAGE_CONTEXT, chooseMotifVersion, motifFieldAttribute, resolveDirection } from "@segnavia/format";
|
|
5
|
+
//#region src/motifs.ts
|
|
6
|
+
const MOTIF_DEPTH_LIMIT = 8;
|
|
7
|
+
var MotifRefusedError = class extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "MotifRefusedError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
function placementName(element) {
|
|
14
|
+
return `motif placement ${Object.entries(element.attribs).filter(([name]) => name === MOTIF_ATTRIBUTE || name.startsWith(`${MOTIF_ATTRIBUTE}-`)).sort(([left], [right]) => left.localeCompare(right)).map(([name, value]) => `${name}=${JSON.stringify(value)}`).join(", ") || `${MOTIF_ATTRIBUTE}=""`}`;
|
|
15
|
+
}
|
|
16
|
+
function refusal(element, problem, remedy) {
|
|
17
|
+
return `${placementName(element)} could not establish ${problem}; ${remedy} to clear this refusal.`;
|
|
18
|
+
}
|
|
19
|
+
function motifElements(html) {
|
|
20
|
+
return elements(parse(html).children).filter((element) => Object.hasOwn(element.attribs, MOTIF_ATTRIBUTE));
|
|
21
|
+
}
|
|
22
|
+
function libraryMotifPlacementRefusal(html) {
|
|
23
|
+
const placed = motifElements(html).find((element) => (element.attribs[MOTIF_ATTRIBUTE] ?? "").startsWith(LIBRARY_MOTIF_PREFIX));
|
|
24
|
+
return placed ? refusal(placed, `a host motif key because "${LIBRARY_MOTIF_PREFIX}" is reserved for library motifs`, "rename the motif key") : void 0;
|
|
25
|
+
}
|
|
26
|
+
function hasMotifPlacement(html) {
|
|
27
|
+
return motifElements(html).length > 0;
|
|
28
|
+
}
|
|
29
|
+
function renderMotifPlacements(html, replacement) {
|
|
30
|
+
const document = parse(html);
|
|
31
|
+
const visit = (nodes) => {
|
|
32
|
+
for (const node of [...nodes]) {
|
|
33
|
+
if (!isTag(node)) continue;
|
|
34
|
+
if (Object.hasOwn(node.attribs, MOTIF_ATTRIBUTE)) {
|
|
35
|
+
const rendered = replacement(node.attribs[MOTIF_ATTRIBUTE] ?? "");
|
|
36
|
+
if (rendered === void 0) {
|
|
37
|
+
const index = nodes.indexOf(node);
|
|
38
|
+
if (index >= 0) nodes.splice(index, 1);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
delete node.attribs[MOTIF_ATTRIBUTE];
|
|
42
|
+
replaceChildren(node, rendered);
|
|
43
|
+
}
|
|
44
|
+
visit(node.children);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
visit(document.children);
|
|
48
|
+
return serialize(document);
|
|
49
|
+
}
|
|
50
|
+
function refuse(state, element, problem, remedy) {
|
|
51
|
+
const message = refusal(element, problem, remedy);
|
|
52
|
+
if (!state.validateOnly) throw new MotifRefusedError(message);
|
|
53
|
+
state.refusals.push(message);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function placementValues(element, motif, inheritedLang, state) {
|
|
57
|
+
const known = new Set(motif.fields.map((field) => motifFieldAttribute(field.key).toLowerCase()));
|
|
58
|
+
known.add(motifFieldAttribute("lang").toLowerCase());
|
|
59
|
+
const unknown = Object.keys(element.attribs).filter((name) => name.startsWith(`${MOTIF_ATTRIBUTE}-`) && !known.has(name));
|
|
60
|
+
if (unknown.length > 0) {
|
|
61
|
+
refuse(state, element, `fields for ${unknown.join(", ")}`, "remove the unknown field attributes");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const values = {};
|
|
65
|
+
for (const field of motif.fields) {
|
|
66
|
+
const value = element.attribs[motifFieldAttribute(field.key).toLowerCase()] ?? field.default;
|
|
67
|
+
if (value === void 0) {
|
|
68
|
+
if (field.required) {
|
|
69
|
+
refuse(state, element, `a value for required field "${field.key}"`, `set ${motifFieldAttribute(field.key)}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (field.choices && !field.choices.includes(value)) {
|
|
75
|
+
refuse(state, element, `an allowed value for field "${field.key}"`, `set ${motifFieldAttribute(field.key)} to one of ${field.choices.join(", ")}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
values[field.key] = value;
|
|
79
|
+
}
|
|
80
|
+
const lang = element.attribs[motifFieldAttribute("lang")] ?? inheritedLang;
|
|
81
|
+
if (lang !== void 0) values.lang = lang;
|
|
82
|
+
return values;
|
|
83
|
+
}
|
|
84
|
+
function replaceChildren(element, html) {
|
|
85
|
+
const children = parse(html).children;
|
|
86
|
+
for (const child of children) child.parent = element;
|
|
87
|
+
element.children = children;
|
|
88
|
+
}
|
|
89
|
+
function needsHostData(html, options) {
|
|
90
|
+
const deferred = new Set(options.deferPaths ?? []);
|
|
91
|
+
const names = new Set(Object.values(bindingNames(options.prefix)));
|
|
92
|
+
return elements(parse(html).children).some((held) => Object.entries(held.attribs).some(([name, path]) => names.has(name) && !deferred.has(bindingRoot(path))));
|
|
93
|
+
}
|
|
94
|
+
async function fillVersion(html, placement, element, state) {
|
|
95
|
+
const data = await state.options.source.data?.(placement, { signal: state.options.signal });
|
|
96
|
+
state.options.signal.throwIfAborted();
|
|
97
|
+
if (data && new Set(state.options.deferPaths ?? []).has(PRINTED_PAGE_CONTEXT) && Object.hasOwn(data, PRINTED_PAGE_CONTEXT)) {
|
|
98
|
+
refuse(state, element, `the reserved ${PRINTED_PAGE_CONTEXT} context because host data contains that key`, `remove ${PRINTED_PAGE_CONTEXT} from MotifSource.data`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const filled = fill(html, {
|
|
102
|
+
prefix: state.options.prefix,
|
|
103
|
+
data: data ?? {},
|
|
104
|
+
linkOrigins: state.options.linkOrigins,
|
|
105
|
+
registerHrefOrigins: true,
|
|
106
|
+
deferPaths: state.options.deferPaths
|
|
107
|
+
});
|
|
108
|
+
for (const path of filled.unavailable) state.unavailable.add(path);
|
|
109
|
+
return filled.html;
|
|
110
|
+
}
|
|
111
|
+
async function motifFor(element, state) {
|
|
112
|
+
const key = element.attribs[MOTIF_ATTRIBUTE] ?? "";
|
|
113
|
+
if (!key) {
|
|
114
|
+
refuse(state, element, "a motif key", `set ${MOTIF_ATTRIBUTE} to a known motif key`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (key.startsWith(LIBRARY_MOTIF_PREFIX)) {
|
|
118
|
+
refuse(state, element, `a host motif key because "${LIBRARY_MOTIF_PREFIX}" is reserved for library motifs`, "rename the motif key");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const revision = state.options.pins && Object.hasOwn(state.options.pins, key) ? state.options.pins[key] : void 0;
|
|
122
|
+
const cacheKey = JSON.stringify([key, revision]);
|
|
123
|
+
let request = state.motifCache.get(cacheKey);
|
|
124
|
+
if (!request) {
|
|
125
|
+
request = state.options.source.motif(key, {
|
|
126
|
+
...revision === void 0 ? {} : { revision },
|
|
127
|
+
signal: state.options.signal
|
|
128
|
+
});
|
|
129
|
+
state.motifCache.set(cacheKey, request);
|
|
130
|
+
}
|
|
131
|
+
const motif = await request;
|
|
132
|
+
state.options.signal.throwIfAborted();
|
|
133
|
+
if (!motif) {
|
|
134
|
+
refuse(state, element, `motif "${key}"${revision ? ` at revision "${revision}"` : ""}`, "make that motif available from the source");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (motif.key.startsWith(LIBRARY_MOTIF_PREFIX)) {
|
|
138
|
+
refuse(state, element, `a host MotifSource result key because "${LIBRARY_MOTIF_PREFIX}" is reserved for library motifs`, "rename the motif key");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
return motif;
|
|
142
|
+
}
|
|
143
|
+
async function expandPlacement(element, inheritedLang, stack, state) {
|
|
144
|
+
state.options.signal.throwIfAborted();
|
|
145
|
+
state.placements += 1;
|
|
146
|
+
if (state.placements > 256) {
|
|
147
|
+
refuse(state, element, `at most 256 expanded motif placements`, "reduce the number of motif placements");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const motif = await motifFor(element, state);
|
|
151
|
+
if (!motif) return;
|
|
152
|
+
if (stack.includes(motif.key)) {
|
|
153
|
+
refuse(state, element, `an acyclic motif path because "${motif.key}" occurs in ${[...stack, motif.key].join(" -> ")}`, "remove the motif cycle");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (stack.length >= 8) {
|
|
157
|
+
refuse(state, element, `a nesting depth within 8`, "reduce the motif nesting depth");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const values = placementValues(element, motif, inheritedLang, state);
|
|
161
|
+
if (!values) return;
|
|
162
|
+
const choice = chooseMotifVersion(motif, values);
|
|
163
|
+
if (!choice.ok) {
|
|
164
|
+
refuse(state, element, `a unique matching version: ${choice.reason}`, "add or revise a matching motif version or its placement values");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const bound = hasBindings(choice.version.html, state.options.prefix);
|
|
168
|
+
if (bound && state.options.scripts !== "refuse") {
|
|
169
|
+
refuse(state, element, "a data-free motif version for a scripted page", "remove its bindings or refuse page scripts");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (needsHostData(choice.version.html, state.options) && !state.options.source.data) {
|
|
173
|
+
refuse(state, element, "data for the motif version bindings", "provide MotifSource.data or remove the bindings");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const lang = choice.version.lang;
|
|
177
|
+
const fields = Object.fromEntries(Object.entries(values).filter(([key]) => key !== "lang"));
|
|
178
|
+
const placement = {
|
|
179
|
+
key: motif.key,
|
|
180
|
+
revision: motif.revision,
|
|
181
|
+
lang,
|
|
182
|
+
fields
|
|
183
|
+
};
|
|
184
|
+
let versionHtml = sanitize(choice.version.html, { linkOrigins: state.options.linkOrigins });
|
|
185
|
+
if (bound && !state.validateOnly) {
|
|
186
|
+
const filled = await fillVersion(versionHtml, placement, element, state);
|
|
187
|
+
if (filled === void 0) return;
|
|
188
|
+
versionHtml = filled;
|
|
189
|
+
}
|
|
190
|
+
element.attribs.lang = lang;
|
|
191
|
+
element.attribs.dir = choice.version.dir ?? resolveDirection(void 0, lang, versionHtml);
|
|
192
|
+
replaceChildren(element, versionHtml);
|
|
193
|
+
if (!state.validateOnly) {
|
|
194
|
+
state.placed.push({
|
|
195
|
+
...placement,
|
|
196
|
+
version: choice.index
|
|
197
|
+
});
|
|
198
|
+
for (const font of motif.fonts) state.fonts.set(JSON.stringify(font), font);
|
|
199
|
+
}
|
|
200
|
+
await expandNodes(element.children, lang, [...stack, motif.key], state);
|
|
201
|
+
}
|
|
202
|
+
async function expandNodes(nodes, inheritedLang, stack, state) {
|
|
203
|
+
for (const node of [...nodes]) {
|
|
204
|
+
state.options.signal.throwIfAborted();
|
|
205
|
+
if (!isTag(node)) continue;
|
|
206
|
+
if (Object.hasOwn(node.attribs, MOTIF_ATTRIBUTE)) await expandPlacement(node, inheritedLang, stack, state);
|
|
207
|
+
else await expandNodes(node.children, inheritedLang, stack, state);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function run(html, options, validateOnly) {
|
|
211
|
+
const document = parse(html);
|
|
212
|
+
const state = {
|
|
213
|
+
options,
|
|
214
|
+
validateOnly,
|
|
215
|
+
refusals: [],
|
|
216
|
+
placed: [],
|
|
217
|
+
fonts: /* @__PURE__ */ new Map(),
|
|
218
|
+
unavailable: /* @__PURE__ */ new Set(),
|
|
219
|
+
motifCache: /* @__PURE__ */ new Map(),
|
|
220
|
+
placements: 0
|
|
221
|
+
};
|
|
222
|
+
await expandNodes(document.children, options.lang, [], state);
|
|
223
|
+
return {
|
|
224
|
+
document,
|
|
225
|
+
state
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function expandMotifs(html, options) {
|
|
229
|
+
const { document, state } = await run(html, options, false);
|
|
230
|
+
return {
|
|
231
|
+
html: serialize(document),
|
|
232
|
+
placed: state.placed,
|
|
233
|
+
fonts: [...state.fonts.values()],
|
|
234
|
+
unavailable: [...state.unavailable]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function motifPlacementRefusals(html, options) {
|
|
238
|
+
return (await run(html, options, true)).state.refusals;
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
241
|
+
export { MOTIF_DEPTH_LIMIT, MotifRefusedError, expandMotifs, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NumberSystem, PrintedPageRole, PrintedPageRules } from "@segnavia/format";
|
|
2
|
+
//#region src/printed-pages.d.ts
|
|
3
|
+
export type PrintedPageSide = 'left' | 'right';
|
|
4
|
+
export interface PrintedPagePlan {
|
|
5
|
+
index: number;
|
|
6
|
+
role: PrintedPageRole;
|
|
7
|
+
parent: string;
|
|
8
|
+
side: PrintedPageSide;
|
|
9
|
+
number: string;
|
|
10
|
+
total: string;
|
|
11
|
+
}
|
|
12
|
+
export interface BookPageFacts {
|
|
13
|
+
blank: boolean;
|
|
14
|
+
sectionOpening: boolean;
|
|
15
|
+
folio?: string;
|
|
16
|
+
runningHead?: string;
|
|
17
|
+
}
|
|
18
|
+
export type BookPrintedPageRole = 'recto' | 'verso' | 'opener' | 'blank';
|
|
19
|
+
export interface BookPrintedPagePlan extends Omit<BookPageFacts, 'blank' | 'sectionOpening'> {
|
|
20
|
+
index: number;
|
|
21
|
+
role: BookPrintedPageRole;
|
|
22
|
+
parent: string;
|
|
23
|
+
side: PrintedPageSide;
|
|
24
|
+
}
|
|
25
|
+
export declare function printedPageAt(rules: PrintedPageRules, index: number, count: number, direction: 'ltr' | 'rtl'): Pick<PrintedPagePlan, 'role' | 'parent' | 'side'>;
|
|
26
|
+
export declare function planPrintedPages(rules: PrintedPageRules, count: number, direction: 'ltr' | 'rtl', numbering?: NumberSystem): PrintedPagePlan[];
|
|
27
|
+
export declare function planBookPages(facts: BookPageFacts[], direction: 'ltr' | 'rtl'): BookPrintedPagePlan[];
|
|
28
|
+
//#endregion
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { BOOK_PARENT_NAMES, formatNumber } from "@segnavia/format";
|
|
2
|
+
//#region src/printed-pages.ts
|
|
3
|
+
function printedPageAt(rules, index, count, direction) {
|
|
4
|
+
const recto = index % 2 === 1;
|
|
5
|
+
const side = recto === (direction === "ltr") ? "right" : "left";
|
|
6
|
+
let role = "every";
|
|
7
|
+
let parent = rules.every;
|
|
8
|
+
if (recto && rules.recto) {
|
|
9
|
+
role = "recto";
|
|
10
|
+
parent = rules.recto;
|
|
11
|
+
} else if (!recto && rules.verso) {
|
|
12
|
+
role = "verso";
|
|
13
|
+
parent = rules.verso;
|
|
14
|
+
}
|
|
15
|
+
if (index === count && rules.last) {
|
|
16
|
+
role = "last";
|
|
17
|
+
parent = rules.last;
|
|
18
|
+
}
|
|
19
|
+
if (index === 1 && rules.first) {
|
|
20
|
+
role = "first";
|
|
21
|
+
parent = rules.first;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
role,
|
|
25
|
+
parent,
|
|
26
|
+
side
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function planPrintedPages(rules, count, direction, numbering = "arabic") {
|
|
30
|
+
if (!Number.isSafeInteger(count) || count < 1) throw new RangeError("A printed-page plan needs a positive safe page count.");
|
|
31
|
+
const total = formatNumber(count, numbering);
|
|
32
|
+
return Array.from({ length: count }, (_, offset) => {
|
|
33
|
+
const index = offset + 1;
|
|
34
|
+
return {
|
|
35
|
+
index,
|
|
36
|
+
...printedPageAt(rules, index, count, direction),
|
|
37
|
+
number: formatNumber(index, numbering),
|
|
38
|
+
total
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const BOOK_RULES = {
|
|
43
|
+
every: BOOK_PARENT_NAMES.recto,
|
|
44
|
+
recto: BOOK_PARENT_NAMES.recto,
|
|
45
|
+
verso: BOOK_PARENT_NAMES.verso
|
|
46
|
+
};
|
|
47
|
+
function planBookPages(facts, direction) {
|
|
48
|
+
return facts.map((fact, offset) => {
|
|
49
|
+
const index = offset + 1;
|
|
50
|
+
const selected = printedPageAt(BOOK_RULES, index, facts.length, direction);
|
|
51
|
+
if (fact.blank) return {
|
|
52
|
+
index,
|
|
53
|
+
role: "blank",
|
|
54
|
+
parent: BOOK_PARENT_NAMES.blank,
|
|
55
|
+
side: selected.side
|
|
56
|
+
};
|
|
57
|
+
if (fact.sectionOpening) return {
|
|
58
|
+
index,
|
|
59
|
+
role: "opener",
|
|
60
|
+
parent: BOOK_PARENT_NAMES.opener,
|
|
61
|
+
side: selected.side,
|
|
62
|
+
...fact.folio === void 0 ? {} : { folio: fact.folio }
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
index,
|
|
66
|
+
role: selected.role === "verso" ? "verso" : "recto",
|
|
67
|
+
parent: selected.parent,
|
|
68
|
+
side: selected.side,
|
|
69
|
+
...fact.folio === void 0 ? {} : { folio: fact.folio },
|
|
70
|
+
...fact.runningHead === void 0 ? {} : { runningHead: fact.runningHead }
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { planBookPages, planPrintedPages, printedPageAt };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/unicode-range.d.ts
|
|
2
|
+
export type UnicodeRangeParse = {
|
|
3
|
+
ok: true;
|
|
4
|
+
value: string;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const UNICODE_RANGE_CLEARANCE: string;
|
|
10
|
+
export declare function parseUnicodeRange(range: string): UnicodeRangeParse;
|
|
11
|
+
//#endregion
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//#region src/unicode-range.ts
|
|
2
|
+
const MAX_CODE_POINT = 1114111;
|
|
3
|
+
const MAX_CODE_POINT_HEX = MAX_CODE_POINT.toString(16).toUpperCase();
|
|
4
|
+
const DESCRIPTOR_FORM = "U+hex, U+hex-hex, or U+hex with ? wildcards";
|
|
5
|
+
const UNICODE_RANGE_CLEARANCE = `${DESCRIPTOR_FORM}; code points at most ${MAX_CODE_POINT_HEX}`;
|
|
6
|
+
function codePoint(hex) {
|
|
7
|
+
return Number.parseInt(hex, 16);
|
|
8
|
+
}
|
|
9
|
+
function aboveMax(hex, token) {
|
|
10
|
+
if (codePoint(hex) > MAX_CODE_POINT) return {
|
|
11
|
+
ok: false,
|
|
12
|
+
reason: `${token} has a code point above ${MAX_CODE_POINT_HEX}`
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function malformed(token) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
reason: `${token} is not a CSS unicode-range descriptor (${DESCRIPTOR_FORM})`
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function parseRange(token) {
|
|
22
|
+
const match = /^[Uu]\+([0-9A-Fa-f]{1,6})-([0-9A-Fa-f]{1,6})$/.exec(token);
|
|
23
|
+
if (!match) return;
|
|
24
|
+
const start = match[1] ?? "";
|
|
25
|
+
const end = match[2] ?? "";
|
|
26
|
+
const high = aboveMax(start, token) ?? aboveMax(end, token);
|
|
27
|
+
if (high) return high;
|
|
28
|
+
if (codePoint(start) > codePoint(end)) return {
|
|
29
|
+
ok: false,
|
|
30
|
+
reason: `${token} has start greater than end`
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
value: `U+${start.toUpperCase()}-${end.toUpperCase()}`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function parseWildcard(token) {
|
|
38
|
+
const match = /^[Uu]\+([0-9A-Fa-f]{0,5})(\?+)$/.exec(token);
|
|
39
|
+
if (!match) return;
|
|
40
|
+
const hex = match[1] ?? "";
|
|
41
|
+
const marks = match[2] ?? "";
|
|
42
|
+
if (hex.length + marks.length > 6) return malformed(token);
|
|
43
|
+
const high = aboveMax(`${hex}${"0".repeat(marks.length)}`, token) ?? aboveMax(`${hex}${"F".repeat(marks.length)}`, token);
|
|
44
|
+
if (high) return high;
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
value: `U+${hex.toUpperCase()}${marks}`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function parseSingle(token) {
|
|
51
|
+
const match = /^[Uu]\+([0-9A-Fa-f]{1,6})$/.exec(token);
|
|
52
|
+
if (!match) return;
|
|
53
|
+
const hex = match[1] ?? "";
|
|
54
|
+
const high = aboveMax(hex, token);
|
|
55
|
+
if (high) return high;
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
value: `U+${hex.toUpperCase()}`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function parseDescriptor(token) {
|
|
62
|
+
return parseRange(token) ?? parseWildcard(token) ?? parseSingle(token) ?? malformed(token);
|
|
63
|
+
}
|
|
64
|
+
function parseUnicodeRange(range) {
|
|
65
|
+
if (range.trim() === "") return {
|
|
66
|
+
ok: false,
|
|
67
|
+
reason: "the list is empty"
|
|
68
|
+
};
|
|
69
|
+
const tokens = range.split(",").map((part) => part.trim());
|
|
70
|
+
if (tokens.some((token) => token.length === 0)) return {
|
|
71
|
+
ok: false,
|
|
72
|
+
reason: "the list has an empty entry"
|
|
73
|
+
};
|
|
74
|
+
const parsed = [];
|
|
75
|
+
for (const token of tokens) {
|
|
76
|
+
const result = parseDescriptor(token);
|
|
77
|
+
if (!result.ok) return result;
|
|
78
|
+
parsed.push(result.value);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
value: parsed.join(", ")
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { UNICODE_RANGE_CLEARANCE, parseUnicodeRange };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segnavia/html",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@segnavia/format": "0.
|
|
29
|
+
"@segnavia/format": "0.27.0",
|
|
30
30
|
"dom-serializer": "^2.0.0",
|
|
31
31
|
"domhandler": "^5.0.3",
|
|
32
32
|
"htmlparser2": "^10.0.0",
|