@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.
- package/dist/base64.d.ts +4 -0
- package/dist/base64.js +51 -0
- package/dist/bindings.d.ts +4 -1
- package/dist/bindings.js +19 -2
- package/dist/book/book-document.js +146 -65
- package/dist/book/book-held.js +182 -0
- package/dist/book/book-view.d.ts +5 -0
- package/dist/book/book-view.js +26 -0
- package/dist/book/furniture.d.ts +14 -0
- package/dist/book/furniture.js +36 -0
- package/dist/book/notes.js +18 -9
- package/dist/book/paginate-book.d.ts +11 -0
- package/dist/book/paginate-book.js +195 -0
- package/dist/book/paginate-columns.d.ts +8 -0
- package/dist/book/paginate-columns.js +119 -0
- package/dist/book/paginate-keeps.d.ts +4 -0
- package/dist/book/paginate-keeps.js +80 -0
- package/dist/book/paginate-marks.d.ts +7 -0
- package/dist/book/paginate-marks.js +34 -0
- package/dist/book/paginate-notes.d.ts +21 -0
- package/dist/book/paginate-notes.js +105 -0
- package/dist/book/paginate-sequence.d.ts +4 -0
- package/dist/book/paginate-sequence.js +267 -0
- package/dist/book/paginate-target.d.ts +13 -0
- package/dist/book/paginate-target.js +36 -0
- package/dist/book/paginate-types.d.ts +170 -0
- package/dist/book/types.d.ts +1 -1
- package/dist/book-css.d.ts +5 -0
- package/dist/book-css.js +14 -0
- package/dist/control-char.js +10 -0
- package/dist/css-tokens.js +189 -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 +15 -2
- package/dist/index.js +14 -2
- package/dist/motifs.d.ts +32 -0
- package/dist/motifs.js +245 -0
- package/dist/printed-pages.d.ts +42 -0
- package/dist/printed-pages.js +97 -0
- package/dist/sanitize-css.d.ts +6 -0
- package/dist/sanitize-css.js +308 -0
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +37 -15
- 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
|
-
export interface FillOptions extends Pick<SanitizeOptions, 'linkOrigins'> {
|
|
6
|
+
export interface FillOptions extends Pick<SanitizeOptions, 'linkOrigins' | 'dataFonts'> {
|
|
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,7 +76,11 @@ 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 = [];
|
|
69
|
-
const
|
|
79
|
+
const deferred = new Set(options.deferPaths ?? []);
|
|
80
|
+
const document = parse(sanitize(html, {
|
|
81
|
+
linkOrigins: options.linkOrigins,
|
|
82
|
+
dataFonts: options.dataFonts
|
|
83
|
+
}));
|
|
70
84
|
const resolve = (path, stack) => {
|
|
71
85
|
const { context, rest } = contextAt(stack, path);
|
|
72
86
|
const value = atPath(context, rest);
|
|
@@ -75,6 +89,8 @@ function fill(html, options) {
|
|
|
75
89
|
};
|
|
76
90
|
const take = (element, name) => {
|
|
77
91
|
const value = element.attribs[name];
|
|
92
|
+
const root = value === void 0 ? void 0 : bindingRoot(value);
|
|
93
|
+
if (root && deferred.has(root)) return void 0;
|
|
78
94
|
delete element.attribs[name];
|
|
79
95
|
return value;
|
|
80
96
|
};
|
|
@@ -113,6 +129,7 @@ function fill(html, options) {
|
|
|
113
129
|
if (path === void 0) continue;
|
|
114
130
|
const value = resolve(path, stack);
|
|
115
131
|
if (preserves(value)) continue;
|
|
132
|
+
registerHrefOrigin(origins, attribute, value, options.registerHrefOrigins);
|
|
116
133
|
if (typeof value === "string" && (attribute === "href" ? safeHref(value, origins) : /^data:image\//i.test(value))) element.attribs[attribute] = value;
|
|
117
134
|
else {
|
|
118
135
|
delete element.attribs[attribute];
|
|
@@ -155,4 +172,4 @@ function fill(html, options) {
|
|
|
155
172
|
};
|
|
156
173
|
}
|
|
157
174
|
//#endregion
|
|
158
|
-
export { BINDINGS, bindingNames, fill, hasBindings };
|
|
175
|
+
export { BINDINGS, bindingNames, bindingRoot, fill, hasBindings };
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
import { elements, parse, serialize } from "../sanitize.js";
|
|
2
|
-
import { element, linkChildren, text
|
|
3
|
-
import {
|
|
2
|
+
import { element, linkChildren, text } from "./dom.js";
|
|
3
|
+
import { collectHeadings, designedSection, flowAttribs, markPlacementIssues, sectionNode, templateHolders, templateRegionNameIssues, templatedSection, undeclaredRegionIssues } from "./book-held.js";
|
|
4
|
+
import { endnotesInOrder, noteState, transformNotes } from "./notes.js";
|
|
4
5
|
import { bookPrintPart } from "./types.js";
|
|
5
6
|
import { isTag } from "domhandler";
|
|
6
|
-
import { isBookContents, noteMarkerIssues, resolveDirection } from "@segnavia/format";
|
|
7
|
+
import { isBookContents, isDesignedSection, isTemplatedSection, noteMarkerIssues, resolveDirection } from "@segnavia/format";
|
|
7
8
|
//#region src/book/book-document.ts
|
|
8
|
-
const
|
|
9
|
+
const CONTENTS_CSS = ":where(.segnavia-contents-list){list-style:none;padding:0;margin:0}:where(.segnavia-contents-entry){display:flex}:where(.segnavia-contents-page){margin-inline-start:auto}:where(.segnavia-contents-entry[data-level=\"2\"]){padding-inline-start:1em}:where(.segnavia-contents-entry[data-level=\"3\"]){padding-inline-start:2em}:where(.segnavia-contents-entry[data-level=\"4\"]){padding-inline-start:3em}:where(.segnavia-contents-entry[data-level=\"5\"]){padding-inline-start:4em}:where(.segnavia-contents-entry[data-level=\"6\"]){padding-inline-start:5em}";
|
|
10
|
+
const PRINT_CSS = `${CONTENTS_CSS}@media print{:where(.segnavia-front,.segnavia-contents)~:where(.segnavia-front,.segnavia-contents),:where(.segnavia-chapter,.segnavia-endnotes,.segnavia-blank){break-before:page}:where(.segnavia-blank){break-after:page}:where(.segnavia-targets){position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%)}}`;
|
|
11
|
+
const GALLEY_CSS = `${CONTENTS_CSS}:where([data-segnavia-galleys]){position:relative}:where([data-segnavia-flow]){orphans:inherit;widows:inherit}:where([data-segnavia-kind="fixed"],[data-segnavia-kind="templated"],[data-segnavia-template]){display:none}:where([data-segnavia-notes-galley]){border-block-start:1px solid;padding-block-start:8px}:where([data-segnavia-note]){break-inside:avoid}:where([data-segnavia-galleys] img,[data-segnavia-window] img,[data-segnavia-page-note] img){display:block;max-block-size:var(--segnavia-text-height);max-inline-size:100%;block-size:auto;inline-size:auto}`;
|
|
9
12
|
function direction(book, value) {
|
|
10
13
|
return resolveDirection(book.dir, book.lang, value);
|
|
11
14
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}, children);
|
|
17
|
-
}
|
|
18
|
-
function authoredSection(section, id) {
|
|
19
|
-
const node = sectionNode("front", id, parse(section.html).children);
|
|
15
|
+
function authoredSection(section, id, galley = false) {
|
|
16
|
+
const document = parse(section.html);
|
|
17
|
+
const runningHead = section.title;
|
|
18
|
+
const node = sectionNode("front", id, document.children, galley, runningHead, flowAttribs(section.opens, section.parents));
|
|
20
19
|
return {
|
|
21
20
|
info: {
|
|
22
21
|
id,
|
|
23
22
|
kind: "front",
|
|
24
23
|
title: section.title,
|
|
25
|
-
runningHead
|
|
24
|
+
runningHead
|
|
26
25
|
},
|
|
27
26
|
node
|
|
28
27
|
};
|
|
29
28
|
}
|
|
30
|
-
function contentsSection(
|
|
29
|
+
function contentsSection(entry, galley = false) {
|
|
31
30
|
const id = "segnavia-contents";
|
|
31
|
+
const title = entry.title;
|
|
32
32
|
const children = title !== void 0 ? [element("h1", {
|
|
33
33
|
class: "segnavia-contents-title",
|
|
34
34
|
dir: "ltr"
|
|
@@ -40,39 +40,39 @@ function contentsSection(title) {
|
|
|
40
40
|
title,
|
|
41
41
|
runningHead: title
|
|
42
42
|
},
|
|
43
|
-
node: sectionNode("contents", id, children)
|
|
43
|
+
node: sectionNode("contents", id, children, galley, title, flowAttribs(entry.opens, entry.parents))
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
-
function chapterSection(chapter, index) {
|
|
46
|
+
function chapterSection(chapter, index, galley = false) {
|
|
47
47
|
const id = `segnavia-chapter-${index}`;
|
|
48
48
|
const document = parse(chapter.html);
|
|
49
|
+
const runningHead = chapter.runningHead ?? chapter.title;
|
|
49
50
|
return {
|
|
50
51
|
info: {
|
|
51
52
|
id,
|
|
52
53
|
kind: "chapter",
|
|
53
54
|
title: chapter.title,
|
|
54
|
-
runningHead
|
|
55
|
+
runningHead
|
|
55
56
|
},
|
|
56
|
-
node: sectionNode("chapter", id, document.children)
|
|
57
|
+
node: sectionNode("chapter", id, document.children, galley, runningHead, flowAttribs(chapter.opens, chapter.parents))
|
|
57
58
|
};
|
|
58
59
|
}
|
|
60
|
+
function appendHeld(built, heldHeadings, extraIssues, book, notes, galley, sections, headings, issues, notesOwner = 0) {
|
|
61
|
+
sections.push(built);
|
|
62
|
+
issues.push(...reservedIdIssues(built), ...columnIssues(built), ...extraIssues);
|
|
63
|
+
headings.push(...heldHeadings);
|
|
64
|
+
transformNotes(built.node, book, 0, notes, galley, notesOwner);
|
|
65
|
+
}
|
|
66
|
+
function appendDesigned(entry, id, kind, book, galley, notes, sections, headings, issues, notesOwner = 0) {
|
|
67
|
+
const held = designedSection(entry, id, kind, galley, book.contents.depth);
|
|
68
|
+
appendHeld(held.built, held.headings, [], book, notes, galley, sections, headings, issues, notesOwner);
|
|
69
|
+
}
|
|
70
|
+
function appendTemplated(entry, id, kind, book, galley, notes, templates, sections, headings, issues, notesOwner = 0) {
|
|
71
|
+
const held = templatedSection(entry, id, kind, galley, book.contents.depth);
|
|
72
|
+
appendHeld(held.built, held.headings, undeclaredRegionIssues(entry, id, templates), book, notes, galley, sections, headings, issues, notesOwner);
|
|
73
|
+
}
|
|
59
74
|
function chapterHeadings(section, chapter, depth) {
|
|
60
|
-
|
|
61
|
-
return elements(section.node.children).flatMap((heading) => {
|
|
62
|
-
const match = /^h([2-6])$/.exec(heading.name);
|
|
63
|
-
if (!match || heading.attribs["data-contents"] === "exclude") return [];
|
|
64
|
-
const level = Number(match[1]);
|
|
65
|
-
if (level > depth) return [];
|
|
66
|
-
generated += 1;
|
|
67
|
-
const id = heading.attribs.id ?? `segnavia-h-${chapter}-${generated}`;
|
|
68
|
-
heading.attribs.id = id;
|
|
69
|
-
return [{
|
|
70
|
-
id,
|
|
71
|
-
level,
|
|
72
|
-
text: textContent(heading, true).replace(/\s+/g, " ").trim(),
|
|
73
|
-
sectionId: section.info.id
|
|
74
|
-
}];
|
|
75
|
-
});
|
|
75
|
+
return collectHeadings(section.node.children, section.info.id, depth, 2, (n) => `segnavia-h-${chapter}-${n}`, { n: 0 });
|
|
76
76
|
}
|
|
77
77
|
function contentsList(book, headings, options) {
|
|
78
78
|
const entries = headings.map((heading) => {
|
|
@@ -125,23 +125,44 @@ function reservedIdIssues(section) {
|
|
|
125
125
|
return [`authored id ${id} in ${section.info.title ?? section.info.id} (${section.info.id}) uses the reserved segnavia- prefix`];
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
|
-
function
|
|
129
|
-
|
|
128
|
+
function columnsValueOk(value) {
|
|
129
|
+
if (!/^[0-9]+$/.test(value)) return false;
|
|
130
|
+
const count = Number(value);
|
|
131
|
+
return count >= 2 && count <= 4;
|
|
132
|
+
}
|
|
133
|
+
function columnIssues(section) {
|
|
134
|
+
const where = `${section.info.title ?? section.info.id} (${section.info.id})`;
|
|
135
|
+
return elements(section.node.children).flatMap((node) => {
|
|
136
|
+
const value = node.attribs["data-columns"];
|
|
137
|
+
if (value === void 0 || columnsValueOk(value)) return [];
|
|
138
|
+
return [`The ${node.name} in ${where} could not establish data-columns ${JSON.stringify(value)}; set it to an integer from 2 to 4 to clear this`];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function endnotesHeading(book) {
|
|
130
142
|
const titleValue = book.endnotes.title;
|
|
131
|
-
|
|
143
|
+
return element("h1", {
|
|
132
144
|
class: "segnavia-endnotes-title",
|
|
133
145
|
dir: direction(book, titleValue)
|
|
134
146
|
}, [text(titleValue)]);
|
|
147
|
+
}
|
|
148
|
+
function endnotesSection(book, paragraphs, galley, id, runningHead, opens) {
|
|
135
149
|
return {
|
|
136
150
|
info: {
|
|
137
151
|
id,
|
|
138
152
|
kind: "endnotes",
|
|
139
|
-
title:
|
|
140
|
-
runningHead
|
|
153
|
+
title: book.endnotes.title,
|
|
154
|
+
runningHead
|
|
141
155
|
},
|
|
142
|
-
node: sectionNode("endnotes", id, [
|
|
156
|
+
node: sectionNode("endnotes", id, [endnotesHeading(book), ...paragraphs], galley, runningHead, opens ? flowAttribs(opens) : {})
|
|
143
157
|
};
|
|
144
158
|
}
|
|
159
|
+
function notesGalley(paragraphs) {
|
|
160
|
+
const notes = paragraphs.map((paragraph) => element("div", { "data-segnavia-note": paragraph.attribs.id ?? "" }, [paragraph]));
|
|
161
|
+
return element("aside", {
|
|
162
|
+
"data-segnavia-notes-galley": "",
|
|
163
|
+
"aria-hidden": "true"
|
|
164
|
+
}, notes);
|
|
165
|
+
}
|
|
145
166
|
function withBlankPages(sections, blankBefore) {
|
|
146
167
|
const counts = /* @__PURE__ */ new Map();
|
|
147
168
|
for (const id of blankBefore ?? []) counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
@@ -150,50 +171,108 @@ function withBlankPages(sections, blankBefore) {
|
|
|
150
171
|
"aria-hidden": "true"
|
|
151
172
|
})), section.node]);
|
|
152
173
|
}
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
function takeHeldNotesOwner(build) {
|
|
175
|
+
build.heldNotesOwner -= 1;
|
|
176
|
+
return build.heldNotesOwner;
|
|
177
|
+
}
|
|
178
|
+
function pushChapterEndnotes(book, build, id, owner) {
|
|
179
|
+
if (book.endnotes.placement !== "chapter") return;
|
|
180
|
+
const paragraphs = build.notes.chapterEndnotes.get(owner) ?? [];
|
|
181
|
+
if (paragraphs.length === 0) return;
|
|
182
|
+
const runningHead = build.sections.at(-1)?.info.runningHead;
|
|
183
|
+
build.sections.push(endnotesSection(book, paragraphs, build.galley, id, runningHead, "any"));
|
|
184
|
+
}
|
|
185
|
+
function addFrontMatter(book, build) {
|
|
159
186
|
let contents;
|
|
187
|
+
let frontNumber = 0;
|
|
160
188
|
for (const entry of book.frontMatter) {
|
|
161
189
|
if (isBookContents(entry)) {
|
|
162
|
-
contents = contentsSection(entry.
|
|
163
|
-
sections.push(contents);
|
|
190
|
+
contents = contentsSection(entry, build.galley);
|
|
191
|
+
build.sections.push(contents);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (isDesignedSection(entry)) {
|
|
195
|
+
build.designedNumber += 1;
|
|
196
|
+
appendDesigned(entry, `segnavia-designed-${build.designedNumber}`, "front", book, build.galley, build.notes, build.sections, build.headings, build.issues);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (isTemplatedSection(entry)) {
|
|
200
|
+
build.templatedNumber += 1;
|
|
201
|
+
appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "front", book, build.galley, build.notes, build.templates, build.sections, build.headings, build.issues);
|
|
164
202
|
continue;
|
|
165
203
|
}
|
|
166
204
|
frontNumber += 1;
|
|
167
|
-
const section = authoredSection(entry, `segnavia-front-${frontNumber}
|
|
168
|
-
sections.push(section);
|
|
169
|
-
issues.push(...reservedIdIssues(section));
|
|
170
|
-
if (entry.contents && entry.title !== void 0) headings.push({
|
|
205
|
+
const section = authoredSection(entry, `segnavia-front-${frontNumber}`, build.galley);
|
|
206
|
+
build.sections.push(section);
|
|
207
|
+
build.issues.push(...reservedIdIssues(section), ...columnIssues(section));
|
|
208
|
+
if (entry.contents && entry.title !== void 0) build.headings.push({
|
|
171
209
|
id: section.info.id,
|
|
172
210
|
level: 1,
|
|
173
211
|
text: entry.title,
|
|
174
212
|
sectionId: section.info.id
|
|
175
213
|
});
|
|
176
|
-
transformNotes(section.node, book, 0, notes);
|
|
214
|
+
transformNotes(section.node, book, 0, build.notes, build.galley);
|
|
177
215
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
216
|
+
return contents;
|
|
217
|
+
}
|
|
218
|
+
function addChapters(book, build) {
|
|
219
|
+
let chapterNumber = 0;
|
|
220
|
+
for (const entry of book.chapters) {
|
|
221
|
+
if (isDesignedSection(entry)) {
|
|
222
|
+
build.designedNumber += 1;
|
|
223
|
+
const owner = takeHeldNotesOwner(build);
|
|
224
|
+
appendDesigned(entry, `segnavia-designed-${build.designedNumber}`, "designed", book, build.galley, build.notes, build.sections, build.headings, build.issues, owner);
|
|
225
|
+
pushChapterEndnotes(book, build, `segnavia-endnotes-designed-${build.designedNumber}`, owner);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (isTemplatedSection(entry)) {
|
|
229
|
+
build.templatedNumber += 1;
|
|
230
|
+
const owner = takeHeldNotesOwner(build);
|
|
231
|
+
appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "templated", book, build.galley, build.notes, build.templates, build.sections, build.headings, build.issues, owner);
|
|
232
|
+
pushChapterEndnotes(book, build, `segnavia-endnotes-templated-${build.templatedNumber}`, owner);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
chapterNumber += 1;
|
|
236
|
+
const section = chapterSection(entry, chapterNumber, build.galley);
|
|
237
|
+
build.sections.push(section);
|
|
238
|
+
build.issues.push(...reservedIdIssues(section), ...columnIssues(section));
|
|
239
|
+
build.headings.push({
|
|
184
240
|
id: section.info.id,
|
|
185
241
|
level: 1,
|
|
186
|
-
text:
|
|
242
|
+
text: entry.title,
|
|
187
243
|
sectionId: section.info.id
|
|
188
|
-
}, ...chapterHeadings(section,
|
|
189
|
-
transformNotes(section.node, book,
|
|
244
|
+
}, ...chapterHeadings(section, chapterNumber, book.contents.depth));
|
|
245
|
+
transformNotes(section.node, book, chapterNumber, build.notes, build.galley);
|
|
246
|
+
pushChapterEndnotes(book, build, `segnavia-endnotes-${chapterNumber}`, chapterNumber);
|
|
190
247
|
}
|
|
191
|
-
|
|
192
|
-
|
|
248
|
+
}
|
|
249
|
+
function bookDocument(book, options = {}) {
|
|
250
|
+
const build = {
|
|
251
|
+
galley: book.mode === "paginated",
|
|
252
|
+
notes: noteState(),
|
|
253
|
+
sections: [],
|
|
254
|
+
headings: [],
|
|
255
|
+
issues: [],
|
|
256
|
+
templates: new Map(book.templates.map((template) => [template.name, template])),
|
|
257
|
+
designedNumber: 0,
|
|
258
|
+
templatedNumber: 0,
|
|
259
|
+
heldNotesOwner: 0
|
|
260
|
+
};
|
|
261
|
+
const contents = addFrontMatter(book, build);
|
|
262
|
+
addChapters(book, build);
|
|
263
|
+
const { galley, notes, sections, headings, issues } = build;
|
|
264
|
+
if (book.endnotes.placement === "chapter") {
|
|
265
|
+
const leftover = notes.chapterEndnotes.get(0) ?? [];
|
|
266
|
+
if (leftover.length > 0) sections.push(endnotesSection(book, leftover, galley, "segnavia-endnotes", book.endnotes.title));
|
|
267
|
+
} else if (notes.endnotes) sections.push(endnotesSection(book, endnotesInOrder(notes), galley, "segnavia-endnotes", book.endnotes.title));
|
|
193
268
|
if (contents) addContents(book, contents, headings, options);
|
|
194
269
|
const visible = sections.filter((section) => options.part === void 0 || bookPrintPart(section.info.kind) === options.part);
|
|
195
270
|
const document = parse("");
|
|
196
|
-
document.children = [
|
|
271
|
+
document.children = galley ? [element("style", {}, [text(GALLEY_CSS)]), element("div", { "data-segnavia-galleys": "" }, [
|
|
272
|
+
...templateHolders(book.templates),
|
|
273
|
+
...visible.map((section) => section.node),
|
|
274
|
+
...notes.footnoteParagraphs.length > 0 ? [notesGalley(notes.footnoteParagraphs)] : []
|
|
275
|
+
])] : [
|
|
197
276
|
element("style", {}, [text(PRINT_CSS)]),
|
|
198
277
|
...options.targets ? [targetLinks(visible)] : [],
|
|
199
278
|
...withBlankPages(visible, options.blankBefore)
|
|
@@ -208,6 +287,8 @@ function bookDocument(book, options = {}) {
|
|
|
208
287
|
footnotes: notes.footnotes,
|
|
209
288
|
endnotes: notes.endnotes
|
|
210
289
|
}),
|
|
290
|
+
...markPlacementIssues(book.parentPages),
|
|
291
|
+
...templateRegionNameIssues(book.templates),
|
|
211
292
|
...issues,
|
|
212
293
|
...duplicateIdIssues(sections, headings)
|
|
213
294
|
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { elements, parse } from "../sanitize.js";
|
|
2
|
+
import { element, textContent } from "./dom.js";
|
|
3
|
+
import { BOOK_FURNITURE, MOTIF_ATTRIBUTE, REGION_NAME, motifFieldAttribute } from "@segnavia/format";
|
|
4
|
+
//#region src/book/book-held.ts
|
|
5
|
+
function flowAttribs(opens, parents, extra = {}) {
|
|
6
|
+
const attribs = { ...extra };
|
|
7
|
+
if (opens) attribs["data-opens"] = opens;
|
|
8
|
+
if (!parents) return attribs;
|
|
9
|
+
for (const role of [
|
|
10
|
+
"opener",
|
|
11
|
+
"recto",
|
|
12
|
+
"verso",
|
|
13
|
+
"blank",
|
|
14
|
+
"fixed"
|
|
15
|
+
]) {
|
|
16
|
+
const name = parents[role];
|
|
17
|
+
if (name) attribs[`data-parent-${role}`] = name;
|
|
18
|
+
}
|
|
19
|
+
return attribs;
|
|
20
|
+
}
|
|
21
|
+
function sectionNode(kind, id, children = [], galley = false, runningHead, extra = {}) {
|
|
22
|
+
const attribs = {
|
|
23
|
+
class: `segnavia-section segnavia-${kind}`,
|
|
24
|
+
id,
|
|
25
|
+
...extra
|
|
26
|
+
};
|
|
27
|
+
if (galley) attribs["data-segnavia-flow"] = id;
|
|
28
|
+
if (galley && runningHead !== void 0) attribs["data-running-head"] = runningHead;
|
|
29
|
+
return element("section", attribs, children);
|
|
30
|
+
}
|
|
31
|
+
function collectHeadings(children, sectionId, depth, firstLevel, idFor, generated) {
|
|
32
|
+
return elements(children).flatMap((heading) => {
|
|
33
|
+
const match = /^h([1-6])$/.exec(heading.name);
|
|
34
|
+
if (!match || heading.attribs["data-contents"] === "exclude") return [];
|
|
35
|
+
const level = Number(match[1]);
|
|
36
|
+
if (level < firstLevel || level > depth) return [];
|
|
37
|
+
generated.n += 1;
|
|
38
|
+
const id = heading.attribs.id ?? idFor(generated.n);
|
|
39
|
+
heading.attribs.id = id;
|
|
40
|
+
return [{
|
|
41
|
+
id,
|
|
42
|
+
level,
|
|
43
|
+
text: textContent(heading, true).replace(/\s+/g, " ").trim(),
|
|
44
|
+
sectionId
|
|
45
|
+
}];
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function heldHeadings(children, sectionId, depth, generated) {
|
|
49
|
+
return collectHeadings(children, sectionId, depth, 1, (n) => `segnavia-h-${sectionId.replace(/^segnavia-/, "")}-${n}`, generated);
|
|
50
|
+
}
|
|
51
|
+
function designedSection(entry, id, kind, galley, depth) {
|
|
52
|
+
const extra = flowAttribs(entry.opens, entry.parents, {
|
|
53
|
+
"data-segnavia-kind": "fixed",
|
|
54
|
+
...kind === "front" ? { "data-sequence": "front" } : {}
|
|
55
|
+
});
|
|
56
|
+
const headings = [];
|
|
57
|
+
const generated = { n: 0 };
|
|
58
|
+
const holders = entry.pages.map((page, offset) => {
|
|
59
|
+
const document = parse(page.html);
|
|
60
|
+
headings.push(...heldHeadings(document.children, id, depth, generated));
|
|
61
|
+
return element("div", {
|
|
62
|
+
"data-segnavia-designed-page": String(offset + 1),
|
|
63
|
+
hidden: ""
|
|
64
|
+
}, document.children);
|
|
65
|
+
});
|
|
66
|
+
if (entry.contents && entry.title !== void 0) headings.unshift({
|
|
67
|
+
id,
|
|
68
|
+
level: 1,
|
|
69
|
+
text: entry.title,
|
|
70
|
+
sectionId: id
|
|
71
|
+
});
|
|
72
|
+
const runningHead = entry.title;
|
|
73
|
+
return {
|
|
74
|
+
built: {
|
|
75
|
+
info: {
|
|
76
|
+
id,
|
|
77
|
+
kind,
|
|
78
|
+
title: entry.title,
|
|
79
|
+
runningHead
|
|
80
|
+
},
|
|
81
|
+
node: sectionNode(kind, id, holders, galley, runningHead, extra)
|
|
82
|
+
},
|
|
83
|
+
headings
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function declaredRegions(html) {
|
|
87
|
+
return new Set(elements(parse(html).children).flatMap((node) => node.attribs["data-region"] ? [node.attribs["data-region"]] : []));
|
|
88
|
+
}
|
|
89
|
+
const regionNameClear = "use a letter followed by letters, digits, hyphens or underscores to clear this refusal";
|
|
90
|
+
function templateRegionNameIssues(templates) {
|
|
91
|
+
return templates.flatMap((template) => [...declaredRegions(template.html)].flatMap((key) => REGION_NAME.test(key) ? [] : [`template "${template.name}" marks region "${key}" which is not an HTML name token; ${regionNameClear}`]));
|
|
92
|
+
}
|
|
93
|
+
const markFurniture = BOOK_FURNITURE.find((item) => item.key === "segnavia:mark");
|
|
94
|
+
const markFields = new Set(markFurniture.fields.map((field) => motifFieldAttribute(field.key)));
|
|
95
|
+
const allowedMarkFields = markFurniture.fields.map((field) => field.key).join(" and ");
|
|
96
|
+
function markNodeIssues(parent, node) {
|
|
97
|
+
if (node.attribs[MOTIF_ATTRIBUTE] !== markFurniture.key) return [];
|
|
98
|
+
const issues = Object.keys(node.attribs).filter((attribute) => attribute.startsWith(`${MOTIF_ATTRIBUTE}-`) && !markFields.has(attribute)).map((attribute) => {
|
|
99
|
+
const field = attribute.slice(`${MOTIF_ATTRIBUTE}-`.length);
|
|
100
|
+
return `parent page "${parent.name}" mark has unknown field "${field}"; use only ${allowedMarkFields} to clear this refusal`;
|
|
101
|
+
});
|
|
102
|
+
for (const field of markFurniture.fields) {
|
|
103
|
+
const attribute = motifFieldAttribute(field.key);
|
|
104
|
+
const value = node.attribs[attribute] ?? ("default" in field ? field.default : void 0);
|
|
105
|
+
if ("required" in field && field.required && (value === void 0 || value.trim() === "")) issues.push(`parent page "${parent.name}" mark has an empty ${field.key}; set a non-empty ${attribute} on the segnavia:mark placement to clear this refusal`);
|
|
106
|
+
if ("choices" in field && value !== void 0) {
|
|
107
|
+
const choices = field.choices;
|
|
108
|
+
if (!choices.includes(value)) issues.push(`parent page "${parent.name}" mark has unknown ${field.key} value "${value}"; use ${choices.join(" or ")} to clear this refusal`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return issues;
|
|
112
|
+
}
|
|
113
|
+
function markPlacementIssues(parents) {
|
|
114
|
+
return parents.flatMap((parent) => ["head", "foot"].flatMap((region) => {
|
|
115
|
+
const html = parent[region];
|
|
116
|
+
if (html === void 0) return [];
|
|
117
|
+
return elements(parse(html).children).flatMap((node) => markNodeIssues(parent, node));
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
function undeclaredRegionIssues(entry, id, templates) {
|
|
121
|
+
const label = entry.title ?? id;
|
|
122
|
+
return entry.pages.flatMap((page, index) => {
|
|
123
|
+
const name = page.template ?? entry.template;
|
|
124
|
+
const template = templates.get(name);
|
|
125
|
+
if (!template) return [];
|
|
126
|
+
const declared = declaredRegions(template.html);
|
|
127
|
+
return Object.keys(page.regions).flatMap((region) => declared.has(region) ? [] : [`section ${label} page ${index + 1} fills region "${region}" which template "${name}" does not declare; add that region to the template or remove the fill to clear this refusal`]);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function templatedSection(entry, id, kind, galley, depth) {
|
|
131
|
+
const extra = flowAttribs(entry.opens, entry.parents, {
|
|
132
|
+
"data-segnavia-kind": "templated",
|
|
133
|
+
"data-template": entry.template,
|
|
134
|
+
...kind === "front" ? { "data-sequence": "front" } : {}
|
|
135
|
+
});
|
|
136
|
+
const headings = [];
|
|
137
|
+
const generated = { n: 0 };
|
|
138
|
+
const holders = entry.pages.map((page, offset) => {
|
|
139
|
+
const name = page.template ?? entry.template;
|
|
140
|
+
const regions = Object.entries(page.regions).map(([region, html]) => {
|
|
141
|
+
const document = parse(html);
|
|
142
|
+
headings.push(...heldHeadings(document.children, id, depth, generated));
|
|
143
|
+
return element("div", {
|
|
144
|
+
"data-region": region,
|
|
145
|
+
hidden: ""
|
|
146
|
+
}, document.children);
|
|
147
|
+
});
|
|
148
|
+
return element("div", {
|
|
149
|
+
"data-segnavia-templated-page": String(offset + 1),
|
|
150
|
+
"data-template": name,
|
|
151
|
+
hidden: ""
|
|
152
|
+
}, regions);
|
|
153
|
+
});
|
|
154
|
+
if (entry.contents && entry.title !== void 0) headings.unshift({
|
|
155
|
+
id,
|
|
156
|
+
level: 1,
|
|
157
|
+
text: entry.title,
|
|
158
|
+
sectionId: id
|
|
159
|
+
});
|
|
160
|
+
const runningHead = entry.title;
|
|
161
|
+
return {
|
|
162
|
+
built: {
|
|
163
|
+
info: {
|
|
164
|
+
id,
|
|
165
|
+
kind,
|
|
166
|
+
title: entry.title,
|
|
167
|
+
runningHead
|
|
168
|
+
},
|
|
169
|
+
node: sectionNode(kind, id, holders, galley, runningHead, extra)
|
|
170
|
+
},
|
|
171
|
+
headings
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function templateHolders(templates) {
|
|
175
|
+
return templates.map((template) => element("div", {
|
|
176
|
+
"data-segnavia-template": template.name,
|
|
177
|
+
hidden: "",
|
|
178
|
+
...flowAttribs(void 0, template.parents)
|
|
179
|
+
}, parse(template.html).children));
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
export { collectHeadings, designedSection, flowAttribs, markPlacementIssues, sectionNode, templateHolders, templateRegionNameIssues, templatedSection, undeclaredRegionIssues };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { bookDocument } from "./book-document.js";
|
|
2
|
+
import { resolveBookPagination } from "@segnavia/format";
|
|
3
|
+
//#region src/book/book-view.ts
|
|
4
|
+
function bookViewFromHtml(book, html) {
|
|
5
|
+
const view = {
|
|
6
|
+
html,
|
|
7
|
+
title: book.title,
|
|
8
|
+
scripts: book.scripts,
|
|
9
|
+
...book.dir === void 0 ? {} : { dir: book.dir },
|
|
10
|
+
...book.lang === void 0 ? {} : { lang: book.lang },
|
|
11
|
+
...book.fonts === void 0 ? {} : { fonts: book.fonts },
|
|
12
|
+
...book.css === void 0 ? {} : { css: book.css }
|
|
13
|
+
};
|
|
14
|
+
const pagination = resolveBookPagination(book);
|
|
15
|
+
return pagination ? {
|
|
16
|
+
...view,
|
|
17
|
+
pagination
|
|
18
|
+
} : view;
|
|
19
|
+
}
|
|
20
|
+
function bookView(book) {
|
|
21
|
+
const document = bookDocument(book);
|
|
22
|
+
if (document.issues.length > 0) throw new Error(`The book cannot be shown: ${document.issues.join("; ")}.`);
|
|
23
|
+
return bookViewFromHtml(book, document.html);
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { bookView, bookViewFromHtml };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PaginatePageMarks } from "./paginate-types.js";
|
|
2
|
+
//#region src/book/furniture.d.ts
|
|
3
|
+
export interface BookFurnitureValues {
|
|
4
|
+
folio?: string;
|
|
5
|
+
runningHead?: string;
|
|
6
|
+
marks?: PaginatePageMarks;
|
|
7
|
+
}
|
|
8
|
+
export declare function fillBookFurniture(html: string, values: BookFurnitureValues, dir?: string, lang?: string): string;
|
|
9
|
+
export declare function embedFunction(name: string, fn: {
|
|
10
|
+
name: string;
|
|
11
|
+
toString(): string;
|
|
12
|
+
}): string;
|
|
13
|
+
export declare function furnitureScript(): string;
|
|
14
|
+
//#endregion
|