@segnavia/html 0.26.0 → 0.28.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.
Files changed (50) hide show
  1. package/dist/base64.d.ts +4 -0
  2. package/dist/base64.js +51 -0
  3. package/dist/bindings.d.ts +4 -1
  4. package/dist/bindings.js +19 -2
  5. package/dist/book/book-document.js +146 -65
  6. package/dist/book/book-held.js +182 -0
  7. package/dist/book/book-view.d.ts +5 -0
  8. package/dist/book/book-view.js +26 -0
  9. package/dist/book/furniture.d.ts +14 -0
  10. package/dist/book/furniture.js +36 -0
  11. package/dist/book/notes.js +18 -9
  12. package/dist/book/paginate-book.d.ts +11 -0
  13. package/dist/book/paginate-book.js +195 -0
  14. package/dist/book/paginate-columns.d.ts +8 -0
  15. package/dist/book/paginate-columns.js +119 -0
  16. package/dist/book/paginate-keeps.d.ts +4 -0
  17. package/dist/book/paginate-keeps.js +80 -0
  18. package/dist/book/paginate-marks.d.ts +7 -0
  19. package/dist/book/paginate-marks.js +34 -0
  20. package/dist/book/paginate-notes.d.ts +21 -0
  21. package/dist/book/paginate-notes.js +105 -0
  22. package/dist/book/paginate-sequence.d.ts +4 -0
  23. package/dist/book/paginate-sequence.js +267 -0
  24. package/dist/book/paginate-target.d.ts +13 -0
  25. package/dist/book/paginate-target.js +36 -0
  26. package/dist/book/paginate-types.d.ts +170 -0
  27. package/dist/book/types.d.ts +1 -1
  28. package/dist/book-css.d.ts +5 -0
  29. package/dist/book-css.js +14 -0
  30. package/dist/control-char.js +10 -0
  31. package/dist/css-tokens.js +189 -0
  32. package/dist/font-css.d.ts +11 -0
  33. package/dist/font-css.js +75 -0
  34. package/dist/font-format.d.ts +4 -0
  35. package/dist/font-format.js +11 -0
  36. package/dist/font-references.d.ts +4 -0
  37. package/dist/font-references.js +192 -0
  38. package/dist/index.d.ts +15 -2
  39. package/dist/index.js +14 -2
  40. package/dist/motifs.d.ts +32 -0
  41. package/dist/motifs.js +245 -0
  42. package/dist/printed-pages.d.ts +42 -0
  43. package/dist/printed-pages.js +97 -0
  44. package/dist/sanitize-css.d.ts +6 -0
  45. package/dist/sanitize-css.js +308 -0
  46. package/dist/sanitize.d.ts +1 -0
  47. package/dist/sanitize.js +37 -15
  48. package/dist/unicode-range.d.ts +11 -0
  49. package/dist/unicode-range.js +86 -0
  50. package/package.json +2 -2
package/dist/motifs.js ADDED
@@ -0,0 +1,245 @@
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
+ dataFonts: state.options.dataFonts,
106
+ registerHrefOrigins: true,
107
+ deferPaths: state.options.deferPaths
108
+ });
109
+ for (const path of filled.unavailable) state.unavailable.add(path);
110
+ return filled.html;
111
+ }
112
+ async function motifFor(element, state) {
113
+ const key = element.attribs[MOTIF_ATTRIBUTE] ?? "";
114
+ if (!key) {
115
+ refuse(state, element, "a motif key", `set ${MOTIF_ATTRIBUTE} to a known motif key`);
116
+ return;
117
+ }
118
+ if (key.startsWith(LIBRARY_MOTIF_PREFIX)) {
119
+ refuse(state, element, `a host motif key because "${LIBRARY_MOTIF_PREFIX}" is reserved for library motifs`, "rename the motif key");
120
+ return;
121
+ }
122
+ const revision = state.options.pins && Object.hasOwn(state.options.pins, key) ? state.options.pins[key] : void 0;
123
+ const cacheKey = JSON.stringify([key, revision]);
124
+ let request = state.motifCache.get(cacheKey);
125
+ if (!request) {
126
+ request = state.options.source.motif(key, {
127
+ ...revision === void 0 ? {} : { revision },
128
+ signal: state.options.signal
129
+ });
130
+ state.motifCache.set(cacheKey, request);
131
+ }
132
+ const motif = await request;
133
+ state.options.signal.throwIfAborted();
134
+ if (!motif) {
135
+ refuse(state, element, `motif "${key}"${revision ? ` at revision "${revision}"` : ""}`, "make that motif available from the source");
136
+ return;
137
+ }
138
+ if (motif.key.startsWith(LIBRARY_MOTIF_PREFIX)) {
139
+ refuse(state, element, `a host MotifSource result key because "${LIBRARY_MOTIF_PREFIX}" is reserved for library motifs`, "rename the motif key");
140
+ return;
141
+ }
142
+ return motif;
143
+ }
144
+ async function expandPlacement(element, inheritedLang, stack, state) {
145
+ state.options.signal.throwIfAborted();
146
+ state.placements += 1;
147
+ if (state.placements > 256) {
148
+ refuse(state, element, `at most 256 expanded motif placements`, "reduce the number of motif placements");
149
+ return;
150
+ }
151
+ const motif = await motifFor(element, state);
152
+ if (!motif) return;
153
+ if (stack.includes(motif.key)) {
154
+ refuse(state, element, `an acyclic motif path because "${motif.key}" occurs in ${[...stack, motif.key].join(" -> ")}`, "remove the motif cycle");
155
+ return;
156
+ }
157
+ if (stack.length >= 8) {
158
+ refuse(state, element, `a nesting depth within 8`, "reduce the motif nesting depth");
159
+ return;
160
+ }
161
+ const values = placementValues(element, motif, inheritedLang, state);
162
+ if (!values) return;
163
+ const choice = chooseMotifVersion(motif, values);
164
+ if (!choice.ok) {
165
+ refuse(state, element, `a unique matching version: ${choice.reason}`, "add or revise a matching motif version or its placement values");
166
+ return;
167
+ }
168
+ const bound = hasBindings(choice.version.html, state.options.prefix);
169
+ if (bound && state.options.scripts !== "refuse") {
170
+ refuse(state, element, "a data-free motif version for a scripted page", "remove its bindings or refuse page scripts");
171
+ return;
172
+ }
173
+ if (needsHostData(choice.version.html, state.options) && !state.options.source.data) {
174
+ refuse(state, element, "data for the motif version bindings", "provide MotifSource.data or remove the bindings");
175
+ return;
176
+ }
177
+ const lang = choice.version.lang;
178
+ const fields = Object.fromEntries(Object.entries(values).filter(([key]) => key !== "lang"));
179
+ const placement = {
180
+ key: motif.key,
181
+ revision: motif.revision,
182
+ lang,
183
+ fields
184
+ };
185
+ let versionHtml = sanitize(choice.version.html, {
186
+ linkOrigins: state.options.linkOrigins,
187
+ dataFonts: state.options.dataFonts
188
+ });
189
+ if (bound && !state.validateOnly) {
190
+ const filled = await fillVersion(versionHtml, placement, element, state);
191
+ if (filled === void 0) return;
192
+ versionHtml = filled;
193
+ }
194
+ element.attribs.lang = lang;
195
+ element.attribs.dir = choice.version.dir ?? resolveDirection(void 0, lang, versionHtml);
196
+ replaceChildren(element, versionHtml);
197
+ if (!state.validateOnly) {
198
+ state.placed.push({
199
+ ...placement,
200
+ version: choice.index
201
+ });
202
+ for (const font of motif.fonts) state.fonts.set(JSON.stringify(font), font);
203
+ }
204
+ await expandNodes(element.children, lang, [...stack, motif.key], state);
205
+ }
206
+ async function expandNodes(nodes, inheritedLang, stack, state) {
207
+ for (const node of [...nodes]) {
208
+ state.options.signal.throwIfAborted();
209
+ if (!isTag(node)) continue;
210
+ if (Object.hasOwn(node.attribs, MOTIF_ATTRIBUTE)) await expandPlacement(node, inheritedLang, stack, state);
211
+ else await expandNodes(node.children, inheritedLang, stack, state);
212
+ }
213
+ }
214
+ async function run(html, options, validateOnly) {
215
+ const document = parse(html);
216
+ const state = {
217
+ options,
218
+ validateOnly,
219
+ refusals: [],
220
+ placed: [],
221
+ fonts: /* @__PURE__ */ new Map(),
222
+ unavailable: /* @__PURE__ */ new Set(),
223
+ motifCache: /* @__PURE__ */ new Map(),
224
+ placements: 0
225
+ };
226
+ await expandNodes(document.children, options.lang, [], state);
227
+ return {
228
+ document,
229
+ state
230
+ };
231
+ }
232
+ async function expandMotifs(html, options) {
233
+ const { document, state } = await run(html, options, false);
234
+ return {
235
+ html: serialize(document),
236
+ placed: state.placed,
237
+ fonts: [...state.fonts.values()],
238
+ unavailable: [...state.unavailable]
239
+ };
240
+ }
241
+ async function motifPlacementRefusals(html, options) {
242
+ return (await run(html, options, true)).state.refusals;
243
+ }
244
+ //#endregion
245
+ export { MOTIF_DEPTH_LIMIT, MotifRefusedError, expandMotifs, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements };
@@ -0,0 +1,42 @@
1
+ import { BookSectionParents as BookSectionParents$1, 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 type BookPrintedPageRole = 'recto' | 'verso' | 'opener' | 'blank' | 'fixed';
13
+ export interface BookPageFacts {
14
+ blank: boolean;
15
+ sectionOpening: boolean;
16
+ folio?: string;
17
+ runningHead?: string;
18
+ role?: BookPrintedPageRole | 'body';
19
+ parents?: BookSectionParents$1;
20
+ }
21
+ export interface BookPrintedPagePlan extends Omit<BookPageFacts, 'blank' | 'sectionOpening'> {
22
+ index: number;
23
+ role: BookPrintedPageRole;
24
+ parent: string;
25
+ side: PrintedPageSide;
26
+ }
27
+ export declare function printedPageAt(rules: PrintedPageRules, index: number, count: number, direction: 'ltr' | 'rtl'): Pick<PrintedPagePlan, 'role' | 'parent' | 'side'>;
28
+ export declare function planPrintedPages(rules: PrintedPageRules, count: number, direction: 'ltr' | 'rtl', numbering?: NumberSystem): PrintedPagePlan[];
29
+ export declare function selectBookParent(fact: BookPageFacts, index: number, count: number, direction: 'ltr' | 'rtl', names?: {
30
+ readonly recto: "segnavia:recto";
31
+ readonly verso: "segnavia:verso";
32
+ readonly opener: "segnavia:opener";
33
+ readonly blank: "segnavia:blank";
34
+ }): Pick<BookPrintedPagePlan, 'role' | 'parent' | 'side'>;
35
+ export declare function planBookPages(facts: BookPageFacts[], direction: 'ltr' | 'rtl', names?: {
36
+ readonly recto: "segnavia:recto";
37
+ readonly verso: "segnavia:verso";
38
+ readonly opener: "segnavia:opener";
39
+ readonly blank: "segnavia:blank";
40
+ }): BookPrintedPagePlan[];
41
+ //#endregion
42
+ export type { BookSectionParents$1 as BookSectionParents };
@@ -0,0 +1,97 @@
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
+ function selectBookParent(fact, index, count, direction, names = BOOK_PARENT_NAMES) {
43
+ const physical = printedPageAt({
44
+ every: names.recto,
45
+ recto: names.recto,
46
+ verso: names.verso
47
+ }, index, count, direction);
48
+ const named = fact.parents;
49
+ if (fact.blank) return {
50
+ role: "blank",
51
+ parent: named?.blank ?? names.blank,
52
+ side: physical.side
53
+ };
54
+ if (fact.role === "fixed") return {
55
+ role: "fixed",
56
+ parent: named?.fixed ?? names.blank,
57
+ side: physical.side
58
+ };
59
+ if (fact.sectionOpening && fact.role !== "body") return {
60
+ role: "opener",
61
+ parent: named?.opener ?? names.opener,
62
+ side: physical.side
63
+ };
64
+ if (physical.role === "verso") return {
65
+ role: "verso",
66
+ parent: named?.verso ?? names.verso,
67
+ side: physical.side
68
+ };
69
+ return {
70
+ role: "recto",
71
+ parent: named?.recto ?? names.recto,
72
+ side: physical.side
73
+ };
74
+ }
75
+ function planBookPages(facts, direction, names = BOOK_PARENT_NAMES) {
76
+ return facts.map((fact, offset) => {
77
+ const index = offset + 1;
78
+ const selected = selectBookParent(fact, index, facts.length, direction, names);
79
+ if (selected.role === "blank") return {
80
+ index,
81
+ ...selected
82
+ };
83
+ if (selected.role === "opener") return {
84
+ index,
85
+ ...selected,
86
+ ...fact.folio === void 0 ? {} : { folio: fact.folio }
87
+ };
88
+ return {
89
+ index,
90
+ ...selected,
91
+ ...fact.folio === void 0 ? {} : { folio: fact.folio },
92
+ ...fact.runningHead === void 0 ? {} : { runningHead: fact.runningHead }
93
+ };
94
+ });
95
+ }
96
+ //#endregion
97
+ export { planBookPages, planPrintedPages, printedPageAt, selectBookParent };
@@ -0,0 +1,6 @@
1
+ //#region src/sanitize-css.d.ts
2
+ export interface CssSanitizeResult {
3
+ css: string;
4
+ removed: string[];
5
+ }
6
+ //#endregion