@segnavia/html 0.27.0 → 0.29.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/bindings.d.ts +1 -1
- package/dist/bindings.js +4 -1
- package/dist/book/book-document.d.ts +2 -0
- package/dist/book/book-document.js +149 -66
- 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/css-tokens.js +189 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +8 -3
- package/dist/motifs.d.ts +1 -0
- package/dist/motifs.js +5 -1
- package/dist/printed-pages.d.ts +18 -4
- package/dist/printed-pages.js +41 -19
- 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/package.json +2 -2
package/dist/bindings.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { SanitizeOptions } from "./sanitize.js";
|
|
|
3
3
|
export declare const BINDINGS: readonly ["value", "href", "src", "each", "when", "present", "width"];
|
|
4
4
|
export declare function bindingRoot(path: string): string;
|
|
5
5
|
export declare function bindingNames(prefix: string): Record<(typeof BINDINGS)[number], string>;
|
|
6
|
-
export interface FillOptions extends Pick<SanitizeOptions, 'linkOrigins'> {
|
|
6
|
+
export interface FillOptions extends Pick<SanitizeOptions, 'linkOrigins' | 'dataFonts'> {
|
|
7
7
|
prefix: string;
|
|
8
8
|
data: Record<string, unknown>;
|
|
9
9
|
unavailable?: Iterable<string>;
|
package/dist/bindings.js
CHANGED
|
@@ -77,7 +77,10 @@ function fill(html, options) {
|
|
|
77
77
|
const unavailable = new Set(options.unavailable ?? []);
|
|
78
78
|
const values = [];
|
|
79
79
|
const deferred = new Set(options.deferPaths ?? []);
|
|
80
|
-
const document = parse(sanitize(html, {
|
|
80
|
+
const document = parse(sanitize(html, {
|
|
81
|
+
linkOrigins: options.linkOrigins,
|
|
82
|
+
dataFonts: options.dataFonts
|
|
83
|
+
}));
|
|
81
84
|
const resolve = (path, stack) => {
|
|
82
85
|
const { context, rest } = contextAt(stack, path);
|
|
83
86
|
const value = atPath(context, rest);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { BookDocument, BookDocumentOptions } from "./types.js";
|
|
2
2
|
import { Book } from "@segnavia/format";
|
|
3
3
|
//#region src/book/book-document.d.ts
|
|
4
|
+
export declare const BOOK_COLUMN_MIN = 2;
|
|
5
|
+
export declare const BOOK_COLUMN_MAX = 4;
|
|
4
6
|
export declare function bookDocument(book: Book, options?: BookDocumentOptions): BookDocument;
|
|
5
7
|
//#endregion
|
|
@@ -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,46 @@ 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
|
-
|
|
129
|
-
|
|
128
|
+
const BOOK_COLUMN_MIN = 2;
|
|
129
|
+
const BOOK_COLUMN_MAX = 4;
|
|
130
|
+
function columnsValueOk(value) {
|
|
131
|
+
if (!/^[0-9]+$/.test(value)) return false;
|
|
132
|
+
const count = Number(value);
|
|
133
|
+
return count >= 2 && count <= 4;
|
|
134
|
+
}
|
|
135
|
+
function columnIssues(section) {
|
|
136
|
+
const where = `${section.info.title ?? section.info.id} (${section.info.id})`;
|
|
137
|
+
return elements(section.node.children).flatMap((node) => {
|
|
138
|
+
const value = node.attribs["data-columns"];
|
|
139
|
+
if (value === void 0 || columnsValueOk(value)) return [];
|
|
140
|
+
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`];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function endnotesHeading(book) {
|
|
130
144
|
const titleValue = book.endnotes.title;
|
|
131
|
-
|
|
145
|
+
return element("h1", {
|
|
132
146
|
class: "segnavia-endnotes-title",
|
|
133
147
|
dir: direction(book, titleValue)
|
|
134
148
|
}, [text(titleValue)]);
|
|
149
|
+
}
|
|
150
|
+
function endnotesSection(book, paragraphs, galley, id, runningHead, opens) {
|
|
135
151
|
return {
|
|
136
152
|
info: {
|
|
137
153
|
id,
|
|
138
154
|
kind: "endnotes",
|
|
139
|
-
title:
|
|
140
|
-
runningHead
|
|
155
|
+
title: book.endnotes.title,
|
|
156
|
+
runningHead
|
|
141
157
|
},
|
|
142
|
-
node: sectionNode("endnotes", id, [
|
|
158
|
+
node: sectionNode("endnotes", id, [endnotesHeading(book), ...paragraphs], galley, runningHead, opens ? flowAttribs(opens) : {})
|
|
143
159
|
};
|
|
144
160
|
}
|
|
161
|
+
function notesGalley(paragraphs) {
|
|
162
|
+
const notes = paragraphs.map((paragraph) => element("div", { "data-segnavia-note": paragraph.attribs.id ?? "" }, [paragraph]));
|
|
163
|
+
return element("aside", {
|
|
164
|
+
"data-segnavia-notes-galley": "",
|
|
165
|
+
"aria-hidden": "true"
|
|
166
|
+
}, notes);
|
|
167
|
+
}
|
|
145
168
|
function withBlankPages(sections, blankBefore) {
|
|
146
169
|
const counts = /* @__PURE__ */ new Map();
|
|
147
170
|
for (const id of blankBefore ?? []) counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
@@ -150,50 +173,108 @@ function withBlankPages(sections, blankBefore) {
|
|
|
150
173
|
"aria-hidden": "true"
|
|
151
174
|
})), section.node]);
|
|
152
175
|
}
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
176
|
+
function takeHeldNotesOwner(build) {
|
|
177
|
+
build.heldNotesOwner -= 1;
|
|
178
|
+
return build.heldNotesOwner;
|
|
179
|
+
}
|
|
180
|
+
function pushChapterEndnotes(book, build, id, owner) {
|
|
181
|
+
if (book.endnotes.placement !== "chapter") return;
|
|
182
|
+
const paragraphs = build.notes.chapterEndnotes.get(owner) ?? [];
|
|
183
|
+
if (paragraphs.length === 0) return;
|
|
184
|
+
const runningHead = build.sections.at(-1)?.info.runningHead;
|
|
185
|
+
build.sections.push(endnotesSection(book, paragraphs, build.galley, id, runningHead, "any"));
|
|
186
|
+
}
|
|
187
|
+
function addFrontMatter(book, build) {
|
|
159
188
|
let contents;
|
|
189
|
+
let frontNumber = 0;
|
|
160
190
|
for (const entry of book.frontMatter) {
|
|
161
191
|
if (isBookContents(entry)) {
|
|
162
|
-
contents = contentsSection(entry.
|
|
163
|
-
sections.push(contents);
|
|
192
|
+
contents = contentsSection(entry, build.galley);
|
|
193
|
+
build.sections.push(contents);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (isDesignedSection(entry)) {
|
|
197
|
+
build.designedNumber += 1;
|
|
198
|
+
appendDesigned(entry, `segnavia-designed-${build.designedNumber}`, "front", book, build.galley, build.notes, build.sections, build.headings, build.issues);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (isTemplatedSection(entry)) {
|
|
202
|
+
build.templatedNumber += 1;
|
|
203
|
+
appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "front", book, build.galley, build.notes, build.templates, build.sections, build.headings, build.issues);
|
|
164
204
|
continue;
|
|
165
205
|
}
|
|
166
206
|
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({
|
|
207
|
+
const section = authoredSection(entry, `segnavia-front-${frontNumber}`, build.galley);
|
|
208
|
+
build.sections.push(section);
|
|
209
|
+
build.issues.push(...reservedIdIssues(section), ...columnIssues(section));
|
|
210
|
+
if (entry.contents && entry.title !== void 0) build.headings.push({
|
|
171
211
|
id: section.info.id,
|
|
172
212
|
level: 1,
|
|
173
213
|
text: entry.title,
|
|
174
214
|
sectionId: section.info.id
|
|
175
215
|
});
|
|
176
|
-
transformNotes(section.node, book, 0, notes);
|
|
216
|
+
transformNotes(section.node, book, 0, build.notes, build.galley);
|
|
177
217
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
218
|
+
return contents;
|
|
219
|
+
}
|
|
220
|
+
function addChapters(book, build) {
|
|
221
|
+
let chapterNumber = 0;
|
|
222
|
+
for (const entry of book.chapters) {
|
|
223
|
+
if (isDesignedSection(entry)) {
|
|
224
|
+
build.designedNumber += 1;
|
|
225
|
+
const owner = takeHeldNotesOwner(build);
|
|
226
|
+
appendDesigned(entry, `segnavia-designed-${build.designedNumber}`, "designed", book, build.galley, build.notes, build.sections, build.headings, build.issues, owner);
|
|
227
|
+
pushChapterEndnotes(book, build, `segnavia-endnotes-designed-${build.designedNumber}`, owner);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (isTemplatedSection(entry)) {
|
|
231
|
+
build.templatedNumber += 1;
|
|
232
|
+
const owner = takeHeldNotesOwner(build);
|
|
233
|
+
appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "templated", book, build.galley, build.notes, build.templates, build.sections, build.headings, build.issues, owner);
|
|
234
|
+
pushChapterEndnotes(book, build, `segnavia-endnotes-templated-${build.templatedNumber}`, owner);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
chapterNumber += 1;
|
|
238
|
+
const section = chapterSection(entry, chapterNumber, build.galley);
|
|
239
|
+
build.sections.push(section);
|
|
240
|
+
build.issues.push(...reservedIdIssues(section), ...columnIssues(section));
|
|
241
|
+
build.headings.push({
|
|
184
242
|
id: section.info.id,
|
|
185
243
|
level: 1,
|
|
186
|
-
text:
|
|
244
|
+
text: entry.title,
|
|
187
245
|
sectionId: section.info.id
|
|
188
|
-
}, ...chapterHeadings(section,
|
|
189
|
-
transformNotes(section.node, book,
|
|
246
|
+
}, ...chapterHeadings(section, chapterNumber, book.contents.depth));
|
|
247
|
+
transformNotes(section.node, book, chapterNumber, build.notes, build.galley);
|
|
248
|
+
pushChapterEndnotes(book, build, `segnavia-endnotes-${chapterNumber}`, chapterNumber);
|
|
190
249
|
}
|
|
191
|
-
|
|
192
|
-
|
|
250
|
+
}
|
|
251
|
+
function bookDocument(book, options = {}) {
|
|
252
|
+
const build = {
|
|
253
|
+
galley: book.mode === "paginated",
|
|
254
|
+
notes: noteState(),
|
|
255
|
+
sections: [],
|
|
256
|
+
headings: [],
|
|
257
|
+
issues: [],
|
|
258
|
+
templates: new Map(book.templates.map((template) => [template.name, template])),
|
|
259
|
+
designedNumber: 0,
|
|
260
|
+
templatedNumber: 0,
|
|
261
|
+
heldNotesOwner: 0
|
|
262
|
+
};
|
|
263
|
+
const contents = addFrontMatter(book, build);
|
|
264
|
+
addChapters(book, build);
|
|
265
|
+
const { galley, notes, sections, headings, issues } = build;
|
|
266
|
+
if (book.endnotes.placement === "chapter") {
|
|
267
|
+
const leftover = notes.chapterEndnotes.get(0) ?? [];
|
|
268
|
+
if (leftover.length > 0) sections.push(endnotesSection(book, leftover, galley, "segnavia-endnotes", book.endnotes.title));
|
|
269
|
+
} else if (notes.endnotes) sections.push(endnotesSection(book, endnotesInOrder(notes), galley, "segnavia-endnotes", book.endnotes.title));
|
|
193
270
|
if (contents) addContents(book, contents, headings, options);
|
|
194
271
|
const visible = sections.filter((section) => options.part === void 0 || bookPrintPart(section.info.kind) === options.part);
|
|
195
272
|
const document = parse("");
|
|
196
|
-
document.children = [
|
|
273
|
+
document.children = galley ? [element("style", {}, [text(GALLEY_CSS)]), element("div", { "data-segnavia-galleys": "" }, [
|
|
274
|
+
...templateHolders(book.templates),
|
|
275
|
+
...visible.map((section) => section.node),
|
|
276
|
+
...notes.footnoteParagraphs.length > 0 ? [notesGalley(notes.footnoteParagraphs)] : []
|
|
277
|
+
])] : [
|
|
197
278
|
element("style", {}, [text(PRINT_CSS)]),
|
|
198
279
|
...options.targets ? [targetLinks(visible)] : [],
|
|
199
280
|
...withBlankPages(visible, options.blankBefore)
|
|
@@ -208,10 +289,12 @@ function bookDocument(book, options = {}) {
|
|
|
208
289
|
footnotes: notes.footnotes,
|
|
209
290
|
endnotes: notes.endnotes
|
|
210
291
|
}),
|
|
292
|
+
...markPlacementIssues(book.parentPages),
|
|
293
|
+
...templateRegionNameIssues(book.templates),
|
|
211
294
|
...issues,
|
|
212
295
|
...duplicateIdIssues(sections, headings)
|
|
213
296
|
]
|
|
214
297
|
};
|
|
215
298
|
}
|
|
216
299
|
//#endregion
|
|
217
|
-
export { bookDocument };
|
|
300
|
+
export { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, bookDocument };
|
|
@@ -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
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { resolveDirection, textDirection } from "@segnavia/format";
|
|
2
|
+
//#region src/book/furniture.ts
|
|
3
|
+
function fillBookFurniture(html, values, dir, lang) {
|
|
4
|
+
const motifField = {
|
|
5
|
+
"segnavia:folio": "folio",
|
|
6
|
+
"segnavia:running-head": "runningHead"
|
|
7
|
+
};
|
|
8
|
+
const declared = dir === "ltr" || dir === "rtl" || dir === "auto" ? dir : void 0;
|
|
9
|
+
function attribute(attrs, name) {
|
|
10
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
+
const found = new RegExp(`\\s${escaped}\\s*=\\s*(?:(["'])([^"']*)\\1|([^\\s>]+))`, "i").exec(attrs);
|
|
12
|
+
return found?.[2] ?? found?.[3];
|
|
13
|
+
}
|
|
14
|
+
function escapeHtml(value) {
|
|
15
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
16
|
+
}
|
|
17
|
+
return html.replace(/<([a-zA-Z][\w:-]*)([^>]*?)\sdata-motif\s*=\s*(?:(["'])([^"']+)\3|([^\s>]+))([^>]*)>([\s\S]*?)<\/\1>/gi, (match, tag, before, _quote, quoted, unquoted, after) => {
|
|
18
|
+
const key = quoted ?? unquoted;
|
|
19
|
+
const field = motifField[key];
|
|
20
|
+
if (!field && key !== "segnavia:mark") return match;
|
|
21
|
+
const attrs = `${before}${after}`;
|
|
22
|
+
const name = key === "segnavia:mark" ? attribute(attrs, "data-motif-name") : void 0;
|
|
23
|
+
const which = key === "segnavia:mark" && attribute(attrs, "data-motif-which") === "last" ? "last" : "first";
|
|
24
|
+
const value = field ? values[field] : name === void 0 ? void 0 : values.marks?.[name]?.[which];
|
|
25
|
+
if (value === void 0) return "";
|
|
26
|
+
return `<${tag}${attrs.replace(/\sdata-motif(?:-[\w-]+)?\s*=\s*(?:(["'])([^"']*)\1|[^\s>]+)/gi, "")}><bdi dir="${resolveDirection(declared, lang, value)}">${escapeHtml(value)}</bdi></${tag}>`;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function embedFunction(name, fn) {
|
|
30
|
+
return `var ${fn.name}=${fn.toString()};var ${name}=${fn.name};`;
|
|
31
|
+
}
|
|
32
|
+
function furnitureScript() {
|
|
33
|
+
return `${embedFunction("textDirection", textDirection)}\n${embedFunction("resolveDirection", resolveDirection)}\n${embedFunction("fillBookFurniture", fillBookFurniture)}`;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { embedFunction, fillBookFurniture, furnitureScript };
|