@segnavia/html 0.38.0 → 0.39.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/book/book-contents.js +4 -4
- package/dist/book/book-document-nodes.js +9 -2
- package/dist/book/book-document.js +31 -4
- package/dist/book/book-flow.js +1 -1
- package/dist/book/book-issues.js +27 -9
- package/dist/book/book-page-reference.d.ts +7 -0
- package/dist/book/book-page-reference.js +19 -0
- package/dist/book/notes.js +22 -10
- package/dist/book/paginate-notes.js +19 -7
- package/dist/book/paginate-types.d.ts +8 -0
- package/dist/book/types.d.ts +1 -0
- package/dist/font-css.js +13 -2
- package/dist/font-format.js +2 -6
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/placements.d.ts +89 -0
- package/dist/placements.js +189 -0
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +5 -2
- package/package.json +2 -2
|
@@ -14,11 +14,11 @@ function contentsList(book, headings, options) {
|
|
|
14
14
|
return element("li", attributes, [element("a", {
|
|
15
15
|
href: options.crossLinks ? `segnavia:target/${heading.id}` : `#${heading.id}`,
|
|
16
16
|
dir: resolveDirection(book.dir, book.lang, heading.text)
|
|
17
|
-
}, [text(heading.text)]), element("
|
|
17
|
+
}, [text(heading.text)]), book.contents.pageNumbers ? element("a", {
|
|
18
18
|
class: "segnavia-contents-page",
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}, [text(page)])]);
|
|
19
|
+
href: `#${heading.id}`,
|
|
20
|
+
"data-ref": "page"
|
|
21
|
+
}, [text(page)]) : element("span", { class: "segnavia-contents-page" }, [])]);
|
|
22
22
|
});
|
|
23
23
|
return element("ol", { class: "segnavia-contents-list" }, entries);
|
|
24
24
|
}
|
|
@@ -11,13 +11,20 @@ function targetLinks(sections) {
|
|
|
11
11
|
tabindex: "-1"
|
|
12
12
|
}, [text("")])));
|
|
13
13
|
}
|
|
14
|
-
function notesGalley(paragraphs) {
|
|
14
|
+
function notesGalley(series, paragraphs) {
|
|
15
15
|
const notes = paragraphs.map((paragraph) => element("div", { "data-segnavia-note": paragraph.attribs.id ?? "" }, [paragraph]));
|
|
16
16
|
return element("aside", {
|
|
17
17
|
"data-segnavia-notes-galley": "",
|
|
18
|
+
"data-segnavia-note-series": series,
|
|
18
19
|
"aria-hidden": "true"
|
|
19
20
|
}, notes);
|
|
20
21
|
}
|
|
22
|
+
function notesGalleys(series, paragraphs) {
|
|
23
|
+
return series.flatMap((item) => {
|
|
24
|
+
const held = paragraphs.get(item.name);
|
|
25
|
+
return item.placement === "page" && held?.length ? [notesGalley(item.name, held)] : [];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
21
28
|
function withBlankPages(sections, blankBefore) {
|
|
22
29
|
const counts = /* @__PURE__ */ new Map();
|
|
23
30
|
for (const id of blankBefore ?? []) counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
@@ -27,4 +34,4 @@ function withBlankPages(sections, blankBefore) {
|
|
|
27
34
|
})), section.node]);
|
|
28
35
|
}
|
|
29
36
|
//#endregion
|
|
30
|
-
export {
|
|
37
|
+
export { notesGalleys, targetLinks, withBlankPages };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { elements, parse, serialize } from "../sanitize.js";
|
|
2
2
|
import { element, linkChildren, text } from "./dom.js";
|
|
3
3
|
import { addContents } from "./book-contents.js";
|
|
4
|
-
import {
|
|
4
|
+
import { notesGalleys, targetLinks, withBlankPages } from "./book-document-nodes.js";
|
|
5
5
|
import { bookDocumentCss, bookFlowPageGeometry, markBlockPositions, resolveBookBuild } from "./book-flow.js";
|
|
6
6
|
import { authoredSection, collectHeadings, designedSection, flowAttribs, markPlacementIssues, sectionNode, templateHolders, templateRegionIssues, templatedSection, templatedSectionIssues } from "./book-held.js";
|
|
7
|
-
import {
|
|
7
|
+
import { pageReferenceNodes, pageReferenceTarget, pageReferenceTargets } from "./book-page-reference.js";
|
|
8
|
+
import { duplicateIdIssues, pageReferenceIssues, reservedIdIssues } from "./book-issues.js";
|
|
8
9
|
import { addPart, attachPendingPartHeadings, partNames } from "./book-part-build.js";
|
|
9
10
|
import { noteState, transformNotes } from "./notes.js";
|
|
10
11
|
import { bookPrintPart } from "./types.js";
|
|
@@ -159,6 +160,7 @@ function addFrontMatter(book, build) {
|
|
|
159
160
|
appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "front", book, build, sectionEntry);
|
|
160
161
|
continue;
|
|
161
162
|
}
|
|
163
|
+
if (!("html" in entry)) continue;
|
|
162
164
|
frontNumber += 1;
|
|
163
165
|
const section = authoredSection(entry, `segnavia-front-${frontNumber}`, build.build);
|
|
164
166
|
build.sections.push(section);
|
|
@@ -238,6 +240,17 @@ function insertCollectedSections(book, build) {
|
|
|
238
240
|
build.sections.splice(index < 0 ? build.sections.length : index, 0, notesSection(book, notes, build.build, collectedSectionId(notes), notes.series.title));
|
|
239
241
|
}
|
|
240
242
|
}
|
|
243
|
+
function writePageReferenceNumbers(book, roots, options) {
|
|
244
|
+
for (const node of pageReferenceNodes(roots)) {
|
|
245
|
+
const id = pageReferenceTarget(node.attribs);
|
|
246
|
+
if (!id) continue;
|
|
247
|
+
const page = options.pageNumbers?.[id] ?? "";
|
|
248
|
+
node.children = [text(page)];
|
|
249
|
+
node.attribs.dir = resolveDirection(book.dir, book.lang, page);
|
|
250
|
+
if (options.crossLinks) node.attribs.href = `segnavia:target/${id}`;
|
|
251
|
+
linkChildren(node);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
241
254
|
function bookDocument(book, options = {}) {
|
|
242
255
|
const { build: requestedBuild, issue: buildIssue } = resolveBookBuild(book, options.build);
|
|
243
256
|
const build = {
|
|
@@ -259,6 +272,18 @@ function bookDocument(book, options = {}) {
|
|
|
259
272
|
const { notes, sections, headings, issues } = build;
|
|
260
273
|
insertCollectedSections(book, build);
|
|
261
274
|
if (contents) addContents(book, contents, headings, options);
|
|
275
|
+
const noteGalleys = notesGalleys(book.notes, notes.pageParagraphs);
|
|
276
|
+
const scopes = [...sections.map((section) => ({
|
|
277
|
+
owner: `${section.info.title ?? section.info.id} (${section.info.id})`,
|
|
278
|
+
roots: [section.node]
|
|
279
|
+
})), ...noteGalleys.map((galley) => ({
|
|
280
|
+
owner: `page-placed note series ${galley.attribs["data-segnavia-note-series"] ?? ""}`,
|
|
281
|
+
roots: [galley]
|
|
282
|
+
}))];
|
|
283
|
+
const referenceIssues = pageReferenceIssues(sections, scopes);
|
|
284
|
+
const referenceRoots = scopes.flatMap((scope) => scope.roots);
|
|
285
|
+
const referenceTargets = pageReferenceTargets(referenceRoots);
|
|
286
|
+
writePageReferenceNumbers(book, referenceRoots, options);
|
|
262
287
|
if (requestedBuild !== "print") markBlockPositions(sections);
|
|
263
288
|
const visible = sections.filter((section) => options.part === void 0 || bookPrintPart(section.info.division) === options.part);
|
|
264
289
|
const document = parse("");
|
|
@@ -268,7 +293,7 @@ function bookDocument(book, options = {}) {
|
|
|
268
293
|
element("div", { "data-segnavia-galleys": "" }, [
|
|
269
294
|
...templateHolders(book.templates),
|
|
270
295
|
...visible.map((section) => section.node),
|
|
271
|
-
...
|
|
296
|
+
...noteGalleys
|
|
272
297
|
])
|
|
273
298
|
] : [
|
|
274
299
|
element("style", {}, [text(bookDocumentCss(book, requestedBuild))]),
|
|
@@ -280,6 +305,7 @@ function bookDocument(book, options = {}) {
|
|
|
280
305
|
html: serialize(document),
|
|
281
306
|
sections: sections.map((section) => section.info),
|
|
282
307
|
headings,
|
|
308
|
+
pageReferenceTargets: referenceTargets,
|
|
283
309
|
issues: [
|
|
284
310
|
...buildIssue ? [buildIssue] : [],
|
|
285
311
|
...noteMarkerIssues(book.notes, notes.used),
|
|
@@ -287,7 +313,8 @@ function bookDocument(book, options = {}) {
|
|
|
287
313
|
...markPlacementIssues(book.parentPages),
|
|
288
314
|
...templateRegionIssues(book.templates),
|
|
289
315
|
...issues,
|
|
290
|
-
...duplicateIdIssues(sections, headings)
|
|
316
|
+
...duplicateIdIssues(sections, headings),
|
|
317
|
+
...referenceIssues
|
|
291
318
|
]
|
|
292
319
|
};
|
|
293
320
|
}
|
package/dist/book/book-flow.js
CHANGED
|
@@ -6,7 +6,7 @@ const BOOK_COLUMN_MIN = 2;
|
|
|
6
6
|
const BOOK_COLUMN_MAX = 4;
|
|
7
7
|
const CONTENTS_INDENTS = Array.from({ length: 5 }, (_, index) => index + 2).map((level) => `:where(.segnavia-contents-entry[data-level="${level}"]){padding-inline-start:${level - 1}em}`).join("");
|
|
8
8
|
const DEEP_CONTENTS_INDENTS = Array.from({ length: MAX_PART_DEPTH }, (_, index) => index + 7).map((level) => `:where(.segnavia-contents-entry[data-level="${level}"]){padding-inline-start:${level - 1}em}`).join("");
|
|
9
|
-
const CONTENTS_PREFIX = `: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}${CONTENTS_INDENTS}`;
|
|
9
|
+
const CONTENTS_PREFIX = `: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([data-ref="page"]){unicode-bidi:isolate}${CONTENTS_INDENTS}`;
|
|
10
10
|
const CONTENTS_SUFFIX = ":where(.segnavia-targets){position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%)}";
|
|
11
11
|
const GALLEY_SUFFIX = ":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}";
|
|
12
12
|
function hasParts(book) {
|
package/dist/book/book-issues.js
CHANGED
|
@@ -1,24 +1,42 @@
|
|
|
1
1
|
import { elements } from "../sanitize.js";
|
|
2
|
+
import { pageReferenceNodes, pageReferenceTarget } from "./book-page-reference.js";
|
|
2
3
|
//#region src/book/book-issues.ts
|
|
4
|
+
function sectionIds(section) {
|
|
5
|
+
return [section.info.id, ...elements(section.node.children).flatMap((node) => node.attribs.id ? [node.attribs.id] : [])];
|
|
6
|
+
}
|
|
7
|
+
function sectionName(section) {
|
|
8
|
+
return `${section.title ?? section.id} (${section.id})`;
|
|
9
|
+
}
|
|
10
|
+
function ownersById(sections) {
|
|
11
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const section of sections) for (const id of sectionIds(section)) occurrences.set(id, [...occurrences.get(id) ?? [], section.info]);
|
|
13
|
+
return occurrences;
|
|
14
|
+
}
|
|
3
15
|
function duplicateIdIssues(sections, headings) {
|
|
4
16
|
const listed = new Set(headings.map((heading) => heading.id));
|
|
5
|
-
|
|
6
|
-
for (const section of sections) {
|
|
7
|
-
const ids = [section.info.id, ...elements(section.node.children).flatMap((node) => node.attribs.id ? [node.attribs.id] : [])];
|
|
8
|
-
for (const id of ids) occurrences.set(id, [...occurrences.get(id) ?? [], section.info]);
|
|
9
|
-
}
|
|
10
|
-
return [...occurrences].flatMap(([id, owners]) => {
|
|
17
|
+
return [...ownersById(sections)].flatMap(([id, owners]) => {
|
|
11
18
|
if (!listed.has(id) || owners.length < 2) return [];
|
|
12
|
-
const named = owners.slice(0, 2).map(
|
|
19
|
+
const named = owners.slice(0, 2).map(sectionName);
|
|
13
20
|
return [`destination id ${id} is used in both ${named[0]} and ${named[1]}; listed heading and generated destination ids must be unique`];
|
|
14
21
|
});
|
|
15
22
|
}
|
|
23
|
+
function pageReferenceIssues(sections, scopes) {
|
|
24
|
+
const targets = ownersById(sections);
|
|
25
|
+
return scopes.flatMap((scope) => pageReferenceNodes(scope.roots).flatMap((node) => {
|
|
26
|
+
const id = pageReferenceTarget(node.attribs);
|
|
27
|
+
if (!id) return [];
|
|
28
|
+
const owners = targets.get(id) ?? [];
|
|
29
|
+
if (owners.length === 0) return [`page reference to ${id} in ${scope.owner} has no target; add the id or remove the reference`];
|
|
30
|
+
if (owners.length === 1) return [];
|
|
31
|
+
return [`page reference target ${id} is used in ${owners.length} sections: ${owners.map(sectionName).join(", ")}; make the target id unique to clear this refusal`];
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
16
34
|
function reservedIdIssues(section) {
|
|
17
35
|
return elements(section.node.children).flatMap((node) => {
|
|
18
36
|
const id = node.attribs.id;
|
|
19
37
|
if (!id?.startsWith("segnavia-")) return [];
|
|
20
|
-
return [`authored id ${id} in ${
|
|
38
|
+
return [`authored id ${id} in ${sectionName(section.info)} uses the reserved segnavia- prefix`];
|
|
21
39
|
});
|
|
22
40
|
}
|
|
23
41
|
//#endregion
|
|
24
|
-
export { duplicateIdIssues, reservedIdIssues };
|
|
42
|
+
export { duplicateIdIssues, pageReferenceIssues, reservedIdIssues };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Element } from "domhandler";
|
|
2
|
+
//#region src/book/book-page-reference.d.ts
|
|
3
|
+
export declare const PAGE_REFERENCE_ATTRIBUTE = "data-ref";
|
|
4
|
+
export declare const PAGE_REFERENCE_VALUE = "page";
|
|
5
|
+
export declare const PAGE_REFERENCE_SELECTOR = "a[data-ref=\"page\"]";
|
|
6
|
+
export declare function pageReferenceTarget(attribs: Record<string, string>): string | undefined;
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { elements } from "../sanitize.js";
|
|
2
|
+
//#region src/book/book-page-reference.ts
|
|
3
|
+
const PAGE_REFERENCE_ATTRIBUTE = "data-ref";
|
|
4
|
+
const PAGE_REFERENCE_VALUE = "page";
|
|
5
|
+
const PAGE_REFERENCE_SELECTOR = "a[data-ref=\"page\"]";
|
|
6
|
+
function pageReferenceTarget(attribs) {
|
|
7
|
+
if (attribs["data-ref"] !== "page") return void 0;
|
|
8
|
+
const href = attribs.href;
|
|
9
|
+
if (href?.startsWith("#") && href.length > 1) return href.slice(1);
|
|
10
|
+
return href?.startsWith("segnavia:target/") && href.length > 16 ? href.slice(16) : void 0;
|
|
11
|
+
}
|
|
12
|
+
function pageReferenceNodes(roots) {
|
|
13
|
+
return roots.flatMap((root) => [root, ...elements(root.children)]).filter((node) => node.name === "a" && pageReferenceTarget(node.attribs) !== void 0);
|
|
14
|
+
}
|
|
15
|
+
function pageReferenceTargets(roots) {
|
|
16
|
+
return [...new Set(pageReferenceNodes(roots).flatMap((node) => pageReferenceTarget(node.attribs) ?? []))];
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { PAGE_REFERENCE_ATTRIBUTE, PAGE_REFERENCE_SELECTOR, PAGE_REFERENCE_VALUE, pageReferenceNodes, pageReferenceTarget, pageReferenceTargets };
|
package/dist/book/notes.js
CHANGED
|
@@ -35,7 +35,7 @@ function noteState() {
|
|
|
35
35
|
used: /* @__PURE__ */ new Set(),
|
|
36
36
|
counters: /* @__PURE__ */ new Map(),
|
|
37
37
|
collected: /* @__PURE__ */ new Map(),
|
|
38
|
-
pageParagraphs:
|
|
38
|
+
pageParagraphs: /* @__PURE__ */ new Map(),
|
|
39
39
|
authored: /* @__PURE__ */ new Map(),
|
|
40
40
|
repeatCount: 0,
|
|
41
41
|
issues: []
|
|
@@ -87,8 +87,9 @@ function noteIds(series, scope, chapter, number) {
|
|
|
87
87
|
refId: `segnavia-${series.idStem}ref${scopePart}-${number}`
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
-
function addPageBlock(block, paragraph, blocks, series) {
|
|
91
|
-
const
|
|
90
|
+
function addPageBlock(block, paragraph, blocks, series, seriesOrder) {
|
|
91
|
+
const blockSeries = blocks.get(block) ?? /* @__PURE__ */ new Map();
|
|
92
|
+
const existing = blockSeries.get(series.name);
|
|
92
93
|
if (existing) {
|
|
93
94
|
append(existing, paragraph);
|
|
94
95
|
return;
|
|
@@ -98,9 +99,16 @@ function addPageBlock(block, paragraph, blocks, series) {
|
|
|
98
99
|
...series.markBlockSeries ? { "data-segnavia-note-series": series.name } : {}
|
|
99
100
|
};
|
|
100
101
|
const notes = element("div", attributes, [paragraph]);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
const index = seriesOrder.get(series.name) ?? seriesOrder.size;
|
|
103
|
+
const next = [...blockSeries.entries()].filter(([name]) => (seriesOrder.get(name) ?? seriesOrder.size) > index).sort(([left], [right]) => (seriesOrder.get(left) ?? seriesOrder.size) - (seriesOrder.get(right) ?? seriesOrder.size))[0]?.[1];
|
|
104
|
+
if (next?.prev) insertAfter(next.prev, notes);
|
|
105
|
+
else if (INSIDE_BLOCKS.has(block.name)) append(block, notes);
|
|
106
|
+
else {
|
|
107
|
+
const previous = [...blockSeries.entries()].filter(([name]) => (seriesOrder.get(name) ?? seriesOrder.size) < index).sort(([left], [right]) => (seriesOrder.get(right) ?? seriesOrder.size) - (seriesOrder.get(left) ?? seriesOrder.size))[0]?.[1];
|
|
108
|
+
insertAfter(previous ?? block, notes);
|
|
109
|
+
}
|
|
110
|
+
blockSeries.set(series.name, notes);
|
|
111
|
+
blocks.set(block, blockSeries);
|
|
104
112
|
}
|
|
105
113
|
function collectedNotes(series, context, state) {
|
|
106
114
|
const key = placementKey(series, context);
|
|
@@ -119,7 +127,7 @@ function collectedNotes(series, context, state) {
|
|
|
119
127
|
state.collected.set(mapKey, collected);
|
|
120
128
|
return collected;
|
|
121
129
|
}
|
|
122
|
-
function transformAuthoredNote(note, series, context, state, blocks, galley) {
|
|
130
|
+
function transformAuthoredNote(note, series, context, state, blocks, seriesOrder, galley) {
|
|
123
131
|
const block = nearestBlock(note);
|
|
124
132
|
if (!block) return;
|
|
125
133
|
const { number, scope } = nextNumber(series, context, state);
|
|
@@ -130,8 +138,11 @@ function transformAuthoredNote(note, series, context, state, blocks, galley) {
|
|
|
130
138
|
replace(note, element("sup", { class: "segnavia-note-ref" }, [markerLink(refId, noteId, marker)]));
|
|
131
139
|
const paragraph = noteParagraph(series.bodyClass, noteId, refId, marker, content);
|
|
132
140
|
if (series.placement === "page") {
|
|
133
|
-
if (galley)
|
|
134
|
-
|
|
141
|
+
if (galley) {
|
|
142
|
+
const paragraphs = state.pageParagraphs.get(series.name) ?? [];
|
|
143
|
+
paragraphs.push(paragraph);
|
|
144
|
+
state.pageParagraphs.set(series.name, paragraphs);
|
|
145
|
+
} else addPageBlock(block, paragraph, blocks, series, seriesOrder);
|
|
135
146
|
} else collectedNotes(series, context, state).paragraphs.push(paragraph);
|
|
136
147
|
if (authoredId) state.authored.set(authoredId, {
|
|
137
148
|
noteId,
|
|
@@ -156,6 +167,7 @@ function transformRepeat(node, state) {
|
|
|
156
167
|
function transformNotes(root, book, context, state, galley = false) {
|
|
157
168
|
const blocks = /* @__PURE__ */ new Map();
|
|
158
169
|
const byName = new Map(book.notes.map((series) => [series.name, series]));
|
|
170
|
+
const seriesOrder = new Map(book.notes.map((series, index) => [series.name, index]));
|
|
159
171
|
const nodes = elements(root.children).filter((node) => node.name === "span" && node.attribs["data-note"] !== void 0 || node.name === "a" && node.attribs["data-note-ref"] !== void 0);
|
|
160
172
|
for (const node of nodes) {
|
|
161
173
|
if (node.name === "a") {
|
|
@@ -168,7 +180,7 @@ function transformNotes(root, book, context, state, galley = false) {
|
|
|
168
180
|
state.issues.push(`data-note names unknown note series ${name}; declare the series in notes or use a declared series name`);
|
|
169
181
|
continue;
|
|
170
182
|
}
|
|
171
|
-
transformAuthoredNote(node, series, context, state, blocks, galley);
|
|
183
|
+
transformAuthoredNote(node, series, context, state, blocks, seriesOrder, galley);
|
|
172
184
|
}
|
|
173
185
|
}
|
|
174
186
|
//#endregion
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
function paginateNotes(payload) {
|
|
3
3
|
const notesById = new Map(payload.notes.map((note) => [note.id, note]));
|
|
4
4
|
const notesGap = payload.geometry.notesGap;
|
|
5
|
+
const noteSeries = payload.geometry.noteSeries;
|
|
5
6
|
const textHeight = payload.geometry.textHeight;
|
|
7
|
+
const seriesOrder = new Map(noteSeries?.map((series, index) => [series.name, index]));
|
|
8
|
+
function ordered(fragments) {
|
|
9
|
+
if (!noteSeries) return fragments;
|
|
10
|
+
return fragments.slice().sort((left, right) => (seriesOrder.get(left.series) ?? noteSeries.length) - (seriesOrder.get(right.series) ?? noteSeries.length));
|
|
11
|
+
}
|
|
6
12
|
function noteEnd(id) {
|
|
7
13
|
return notesById.get(id)?.lines.length ?? 0;
|
|
8
14
|
}
|
|
@@ -15,8 +21,13 @@ function paginateNotes(payload) {
|
|
|
15
21
|
function notesSpace(fragments) {
|
|
16
22
|
if (fragments.length === 0) return 0;
|
|
17
23
|
let height = 0;
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
let previous;
|
|
25
|
+
for (const fragment of noteSeries ? ordered(fragments) : fragments) {
|
|
26
|
+
if (fragment.series !== previous) height += noteSeries?.find(({ name }) => name === fragment.series)?.gap ?? notesGap;
|
|
27
|
+
height += fragmentHeight(fragment);
|
|
28
|
+
previous = fragment.series;
|
|
29
|
+
}
|
|
30
|
+
return height;
|
|
20
31
|
}
|
|
21
32
|
function measure(placed, fragments) {
|
|
22
33
|
if (payload.op === "continue" || payload.op === "allocate" || payload.op === "recompute") return payload.used(placed, fragments);
|
|
@@ -25,13 +36,14 @@ function paginateNotes(payload) {
|
|
|
25
36
|
function makeFragment(id, start, end) {
|
|
26
37
|
return {
|
|
27
38
|
id,
|
|
39
|
+
series: notesById.get(id)?.series ?? "",
|
|
28
40
|
whole: start === 0 && end === noteEnd(id),
|
|
29
41
|
start,
|
|
30
42
|
end
|
|
31
43
|
};
|
|
32
44
|
}
|
|
33
45
|
function leftoverFrom(fragments) {
|
|
34
|
-
return fragments.flatMap((fragment) => fragment.end < noteEnd(fragment.id) ? [makeFragment(fragment.id, fragment.end, noteEnd(fragment.id))] : []);
|
|
46
|
+
return ordered(fragments.flatMap((fragment) => fragment.end < noteEnd(fragment.id) ? [makeFragment(fragment.id, fragment.end, noteEnd(fragment.id))] : []));
|
|
35
47
|
}
|
|
36
48
|
function takeContinued(continued) {
|
|
37
49
|
if (continued.length === 0) return {
|
|
@@ -41,7 +53,7 @@ function paginateNotes(payload) {
|
|
|
41
53
|
const fragments = [];
|
|
42
54
|
const leftover = [];
|
|
43
55
|
let pending = true;
|
|
44
|
-
for (const item of continued) {
|
|
56
|
+
for (const item of ordered(continued)) {
|
|
45
57
|
const total = noteEnd(item.id);
|
|
46
58
|
if (!pending) {
|
|
47
59
|
leftover.push(item);
|
|
@@ -59,8 +71,8 @@ function paginateNotes(payload) {
|
|
|
59
71
|
pending = false;
|
|
60
72
|
}
|
|
61
73
|
return {
|
|
62
|
-
fragments,
|
|
63
|
-
leftover
|
|
74
|
+
fragments: ordered(fragments),
|
|
75
|
+
leftover: ordered(leftover)
|
|
64
76
|
};
|
|
65
77
|
}
|
|
66
78
|
function fitNote(trial, fragments, id) {
|
|
@@ -84,7 +96,7 @@ function paginateNotes(payload) {
|
|
|
84
96
|
if (!fragment) return void 0;
|
|
85
97
|
fragments.push(fragment);
|
|
86
98
|
}
|
|
87
|
-
return fragments;
|
|
99
|
+
return ordered(fragments);
|
|
88
100
|
}
|
|
89
101
|
function notesFor(placed, seed) {
|
|
90
102
|
let fragments = seed;
|
|
@@ -53,13 +53,19 @@ export type PaginateFlow = (PaginateFlowBase & {
|
|
|
53
53
|
});
|
|
54
54
|
export interface PaginateNote {
|
|
55
55
|
id: string;
|
|
56
|
+
series: string;
|
|
56
57
|
lines: PaginateLine[];
|
|
57
58
|
}
|
|
59
|
+
export interface PaginateNoteSeriesGeometry {
|
|
60
|
+
name: string;
|
|
61
|
+
gap: number;
|
|
62
|
+
}
|
|
58
63
|
export interface PaginateBookInput {
|
|
59
64
|
flows: PaginateFlow[];
|
|
60
65
|
notes: PaginateNote[];
|
|
61
66
|
geometry: {
|
|
62
67
|
notesGap: number;
|
|
68
|
+
noteSeries?: PaginateNoteSeriesGeometry[];
|
|
63
69
|
};
|
|
64
70
|
parents: Record<string, TextArea>;
|
|
65
71
|
parentNames: typeof BOOK_PARENT_NAMES;
|
|
@@ -81,6 +87,7 @@ export interface PaginateWindow {
|
|
|
81
87
|
}
|
|
82
88
|
export interface PaginatePageNote {
|
|
83
89
|
id: string;
|
|
90
|
+
series: string;
|
|
84
91
|
whole: boolean;
|
|
85
92
|
start: number;
|
|
86
93
|
end: number;
|
|
@@ -148,6 +155,7 @@ interface PaginateNotesBase {
|
|
|
148
155
|
geometry: {
|
|
149
156
|
textHeight: number;
|
|
150
157
|
notesGap: number;
|
|
158
|
+
noteSeries?: PaginateNoteSeriesGeometry[];
|
|
151
159
|
};
|
|
152
160
|
}
|
|
153
161
|
export type PaginateNotesOp = (PaginateNotesBase & {
|
package/dist/book/types.d.ts
CHANGED
package/dist/font-css.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { bytesToBase64 } from "./base64.js";
|
|
2
2
|
import { hasControlChar } from "./control-char.js";
|
|
3
3
|
import { UNICODE_RANGE_CLEARANCE, parseUnicodeRange } from "./unicode-range.js";
|
|
4
|
-
import { APP_FONT_NAME, FONT_FORMATS } from "@segnavia/format";
|
|
4
|
+
import { APP_FONT_NAME, FONT_FORMATS, FontFaceRefusedError, resolveFontFace } from "@segnavia/format";
|
|
5
5
|
//#region src/font-css.ts
|
|
6
6
|
var FontCssRefusedError = class extends Error {
|
|
7
7
|
constructor(message) {
|
|
@@ -59,7 +59,18 @@ function validatedFace(font) {
|
|
|
59
59
|
return font;
|
|
60
60
|
}
|
|
61
61
|
function fontFaceCss(font) {
|
|
62
|
-
const
|
|
62
|
+
const validated = validatedFace(font);
|
|
63
|
+
let resolved;
|
|
64
|
+
try {
|
|
65
|
+
resolved = resolveFontFace(validated.data, validated.format, validated.postscriptName);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof FontFaceRefusedError) throw new FontCssRefusedError(error.message);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
const face = {
|
|
71
|
+
...validated,
|
|
72
|
+
...resolved
|
|
73
|
+
};
|
|
63
74
|
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}`;
|
|
64
75
|
}
|
|
65
76
|
function customProperty(name, font) {
|
package/dist/font-format.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
+
import { classifyFont } from "@segnavia/format";
|
|
1
2
|
//#region src/font-format.ts
|
|
2
3
|
function fontFormat(data) {
|
|
3
|
-
|
|
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";
|
|
4
|
+
return classifyFont(data);
|
|
9
5
|
}
|
|
10
6
|
//#endregion
|
|
11
7
|
export { fontFormat };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { base64ToBytes, bytesToBase64 } from "./base64.js";
|
|
2
|
-
import { REFUSED_ELEMENTS, SanitizeOptions, findActiveContent, sanitize } from "./sanitize.js";
|
|
2
|
+
import { REFUSED_ELEMENTS, SanitizeOptions, findActiveContent, safeImageSource, sanitize } from "./sanitize.js";
|
|
3
3
|
import { BINDINGS, FillOptions, Filled, bindingNames, bindingRoot, fill, hasBindings } from "./bindings.js";
|
|
4
4
|
import { BookBuild, BookDocument, BookDocumentOptions, BookHeading, BookPrintPart, BookSectionInfo, bookPrintPart } from "./book/types.js";
|
|
5
5
|
import { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, BookPositionKey, parseBookPositionKey } from "./book/book-flow.js";
|
|
6
6
|
import { bookDocument } from "./book/book-document.js";
|
|
7
|
+
import { PAGE_REFERENCE_ATTRIBUTE, PAGE_REFERENCE_SELECTOR, PAGE_REFERENCE_VALUE, pageReferenceTarget } from "./book/book-page-reference.js";
|
|
7
8
|
import { bookView, bookViewFromHtml } from "./book/book-view.js";
|
|
8
|
-
import { PaginateBlock, PaginateBookInput, PaginateBookPage, PaginateBookRefusal, PaginateBookResult, PaginateFlow, PaginateFlowParents, PaginateLine, PaginateMark, PaginateNote, PaginateNoteRef, PaginateOpens, PaginatePageMarks, PaginatePageNote, PaginatePageRole, PaginateSequence, PaginateWindow } from "./book/paginate-types.js";
|
|
9
|
+
import { PaginateBlock, PaginateBookInput, PaginateBookPage, PaginateBookRefusal, PaginateBookResult, PaginateFlow, PaginateFlowParents, PaginateLine, PaginateMark, PaginateNote, PaginateNoteRef, PaginateNoteSeriesGeometry, PaginateOpens, PaginatePageMarks, PaginatePageNote, PaginatePageRole, PaginateSequence, PaginateWindow } from "./book/paginate-types.js";
|
|
9
10
|
import { BookFurnitureValues, embedFunction, fillBookFurniture, furnitureScript } from "./book/furniture.js";
|
|
10
11
|
import { BookPageFacts, BookPrintedPagePlan, BookPrintedPageRole, BookSectionParents, PrintedPagePlan, PrintedPageSide, planBookPages, planPrintedPages, printedPageAt, selectBookParent } from "./printed-pages.js";
|
|
11
12
|
import { PAGINATE_FUNCTIONS, paginateBook, paginateScript } from "./book/paginate-book.js";
|
|
@@ -16,6 +17,7 @@ import { FontCssRefusedError, fontCss, fontFaceCss } from "./font-css.js";
|
|
|
16
17
|
import { fontFormat } from "./font-format.js";
|
|
17
18
|
import { fontReferenceRefusals } from "./font-references.js";
|
|
18
19
|
import { ExpandMotifsOptions, ExpandedMotifs, MOTIF_DEPTH_LIMIT, MotifRefusedError, PlacedMotif, documentLanguage, expandMotifs, hasHostMotifPlacement, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements } from "./motifs.js";
|
|
20
|
+
import { MediaAnswer, PLACEMENT_ATTRIBUTES, PlacementPlan, PlacementPlanEntry, PlacementRefusal, PlacementReport, PlacementsOptions, ResolvePlacement, ResolvedPlacement, classifyMediaAnswer, placementCropEdges, placementPlaceholderSource, placementPlan, resolvePlacements } from "./placements.js";
|
|
19
21
|
import { SlotFillEntry, SlotFillRefusal, SlotFillsConflictError, SlotFillsRefusedError, SlotPrintOptions, assertSingleSlotCopy, assertSlotPrintOptions, collectSlotPrintCopies, isSlotCopyRefusal, slotFillCopyId, slotFillsConflict } from "./slot-print.js";
|
|
20
22
|
import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
|
|
21
23
|
import { MAX_SLOT_DECLARATIONS, PreparedSlots, SlotFillRefusedError, hasSlotWork, prepareSlotCollection, prepareSlots, slotFillRefusal } from "./slots.js";
|
|
@@ -25,4 +27,4 @@ export type HtmlKind = 'plain' | 'live-data' | 'scripted' | 'refused';
|
|
|
25
27
|
export declare function classify(html: string, prefix: string): HtmlKind;
|
|
26
28
|
export declare function searchText(html: string): string;
|
|
27
29
|
//#endregion
|
|
28
|
-
export { ABSOLUTE_PX, ABSOLUTE_UNITS, BINDINGS, BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, type BookBuild, type BookDocument, type BookDocumentOptions, type BookFurnitureValues, type BookHeading, BookPageFacts, type BookPositionKey, type BookPrintPart, BookPrintedPagePlan, BookPrintedPageRole, type BookSectionInfo, type BookSectionParents, type ExpandMotifsOptions, type ExpandedMotifs, type FillOptions, type Filled, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, MotifRefusedError, PAGINATE_FUNCTIONS, type PaginateBlock, type PaginateBookInput, type PaginateBookPage, type PaginateBookRefusal, type PaginateBookResult, type PaginateFlow, type PaginateFlowParents, type PaginateLine, type PaginateMark, type PaginateNote, type PaginateNoteRef, type PaginateOpens, type PaginatePageMarks, type PaginatePageNote, type PaginatePageRole, type PaginateSequence, type PaginateTarget, type PaginateTargetPage, type PaginateWindow, 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, bookStylesheet, bookView, bookViewFromHtml, bytesToBase64, collectSlotPrintCopies, documentLanguage, embedFunction, expandMotifs, fill, fillBookFurniture, findActiveContent, fitRegionSize, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, furnitureScript, hasBindings, hasHostMotifPlacement, hasMotifPlacement, hasSlotWork, heldPageNumber, isSlotCopyRefusal, lengthPx, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, openerPageIndex, pageIndexForTarget, paginateBook, paginateScript, parseBookPositionKey, parseCssLength, parseUnicodeRange, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, sanitize, sanitizeBookCss, selectBookParent, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal, windowPageIndex };
|
|
30
|
+
export { ABSOLUTE_PX, ABSOLUTE_UNITS, BINDINGS, BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, type BookBuild, type BookDocument, type BookDocumentOptions, type BookFurnitureValues, type BookHeading, BookPageFacts, type BookPositionKey, type BookPrintPart, BookPrintedPagePlan, BookPrintedPageRole, type BookSectionInfo, type BookSectionParents, type ExpandMotifsOptions, type ExpandedMotifs, type FillOptions, type Filled, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, type MediaAnswer, MotifRefusedError, PAGE_REFERENCE_ATTRIBUTE, PAGE_REFERENCE_SELECTOR, PAGE_REFERENCE_VALUE, PAGINATE_FUNCTIONS, PLACEMENT_ATTRIBUTES, type PaginateBlock, type PaginateBookInput, type PaginateBookPage, type PaginateBookRefusal, type PaginateBookResult, type PaginateFlow, type PaginateFlowParents, type PaginateLine, type PaginateMark, type PaginateNote, type PaginateNoteRef, type PaginateNoteSeriesGeometry, type PaginateOpens, type PaginatePageMarks, type PaginatePageNote, type PaginatePageRole, type PaginateSequence, type PaginateTarget, type PaginateTargetPage, type PaginateWindow, type PlacedMotif, type PlacementPlan, type PlacementPlanEntry, type PlacementRefusal, type PlacementReport, type PlacementsOptions, type PreparedSlots, PrintedPagePlan, PrintedPageSide, REFUSED_ELEMENTS, RequiredSlotsRefusedError, type ResolvePlacement, type ResolvedPlacement, type SanitizeOptions, type SlotFillEntry, type SlotFillRefusal, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, type SlotPrintOptions, UNICODE_RANGE_CLEARANCE, type UnicodeRangeParse, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bookDocument, bookPrintPart, bookStylesheet, bookView, bookViewFromHtml, bytesToBase64, classifyMediaAnswer, collectSlotPrintCopies, documentLanguage, embedFunction, expandMotifs, fill, fillBookFurniture, findActiveContent, fitRegionSize, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, furnitureScript, hasBindings, hasHostMotifPlacement, hasMotifPlacement, hasSlotWork, heldPageNumber, isSlotCopyRefusal, lengthPx, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, openerPageIndex, pageIndexForTarget, pageReferenceTarget, paginateBook, paginateScript, parseBookPositionKey, parseCssLength, parseUnicodeRange, placementCropEdges, placementPlaceholderSource, placementPlan, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, resolvePlacements, safeImageSource, sanitize, sanitizeBookCss, selectBookParent, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal, windowPageIndex };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { REFUSED_ELEMENTS, elements, findActiveContent, parse, sanitize } from "./sanitize.js";
|
|
1
|
+
import { REFUSED_ELEMENTS, elements, findActiveContent, parse, safeImageSource, sanitize } from "./sanitize.js";
|
|
2
2
|
import { BINDINGS, bindingNames, bindingRoot, fill, hasBindings } from "./bindings.js";
|
|
3
3
|
import { base64ToBytes, bytesToBase64 } from "./base64.js";
|
|
4
4
|
import { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, parseBookPositionKey } from "./book/book-flow.js";
|
|
5
5
|
import { ABSOLUTE_PX, ABSOLUTE_UNITS, fitRegionSize, lengthPx, parseCssLength } from "./book/region-fit.js";
|
|
6
|
+
import { PAGE_REFERENCE_ATTRIBUTE, PAGE_REFERENCE_SELECTOR, PAGE_REFERENCE_VALUE, pageReferenceTarget } from "./book/book-page-reference.js";
|
|
6
7
|
import { bookPrintPart } from "./book/types.js";
|
|
7
8
|
import { bookDocument } from "./book/book-document.js";
|
|
8
9
|
import { bookView, bookViewFromHtml } from "./book/book-view.js";
|
|
@@ -16,6 +17,7 @@ import { FontCssRefusedError, fontCss, fontFaceCss } from "./font-css.js";
|
|
|
16
17
|
import { fontFormat } from "./font-format.js";
|
|
17
18
|
import { fontReferenceRefusals } from "./font-references.js";
|
|
18
19
|
import { MOTIF_DEPTH_LIMIT, MotifRefusedError, documentLanguage, expandMotifs, hasHostMotifPlacement, hasMotifPlacement, libraryMotifPlacementRefusal, motifPlacementRefusals, renderMotifPlacements } from "./motifs.js";
|
|
20
|
+
import { PLACEMENT_ATTRIBUTES, classifyMediaAnswer, placementCropEdges, placementPlaceholderSource, placementPlan, resolvePlacements } from "./placements.js";
|
|
19
21
|
import { RequiredSlotsRefusedError, UnmeasuredSlotsRefusedError, mergeSlotFitResults, requiredSlotRefusal, unmeasuredSlotRefusal } from "./slot-policy.js";
|
|
20
22
|
import { MAX_SLOT_DECLARATIONS, SlotFillRefusedError, hasSlotWork, prepareSlotCollection, prepareSlots, slotFillRefusal } from "./slots.js";
|
|
21
23
|
import { SlotFillsConflictError, SlotFillsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, collectSlotPrintCopies, isSlotCopyRefusal, slotFillCopyId, slotFillsConflict } from "./slot-print.js";
|
|
@@ -31,4 +33,4 @@ function searchText(html) {
|
|
|
31
33
|
return html.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
32
34
|
}
|
|
33
35
|
//#endregion
|
|
34
|
-
export { ABSOLUTE_PX, ABSOLUTE_UNITS, BINDINGS, BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, MotifRefusedError, PAGINATE_FUNCTIONS, REFUSED_ELEMENTS, RequiredSlotsRefusedError, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, UNICODE_RANGE_CLEARANCE, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bookDocument, bookPrintPart, bookStylesheet, bookView, bookViewFromHtml, bytesToBase64, classify, collectSlotPrintCopies, documentLanguage, embedFunction, expandMotifs, fill, fillBookFurniture, findActiveContent, fitRegionSize, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, furnitureScript, hasBindings, hasHostMotifPlacement, hasMotifPlacement, hasSlotWork, heldPageNumber, isSlotCopyRefusal, lengthPx, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, openerPageIndex, pageIndexForTarget, paginateBook, paginateScript, parseBookPositionKey, parseCssLength, parseUnicodeRange, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, sanitize, sanitizeBookCss, searchText, selectBookParent, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal, windowPageIndex };
|
|
36
|
+
export { ABSOLUTE_PX, ABSOLUTE_UNITS, BINDINGS, BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, FontCssRefusedError, MAX_SLOT_DECLARATIONS, MOTIF_DEPTH_LIMIT, MotifRefusedError, PAGE_REFERENCE_ATTRIBUTE, PAGE_REFERENCE_SELECTOR, PAGE_REFERENCE_VALUE, PAGINATE_FUNCTIONS, PLACEMENT_ATTRIBUTES, REFUSED_ELEMENTS, RequiredSlotsRefusedError, SlotFillRefusedError, SlotFillsConflictError, SlotFillsRefusedError, UNICODE_RANGE_CLEARANCE, UnmeasuredSlotsRefusedError, assertSingleSlotCopy, assertSlotPrintOptions, base64ToBytes, bindingNames, bindingRoot, bookDocument, bookPrintPart, bookStylesheet, bookView, bookViewFromHtml, bytesToBase64, classify, classifyMediaAnswer, collectSlotPrintCopies, documentLanguage, embedFunction, expandMotifs, fill, fillBookFurniture, findActiveContent, fitRegionSize, fontCss, fontFaceCss, fontFormat, fontReferenceRefusals, furnitureScript, hasBindings, hasHostMotifPlacement, hasMotifPlacement, hasSlotWork, heldPageNumber, isSlotCopyRefusal, lengthPx, libraryMotifPlacementRefusal, mergeSlotFitResults, motifPlacementRefusals, openerPageIndex, pageIndexForTarget, pageReferenceTarget, paginateBook, paginateScript, parseBookPositionKey, parseCssLength, parseUnicodeRange, placementCropEdges, placementPlaceholderSource, placementPlan, planBookPages, planPrintedPages, prepareSlotCollection, prepareSlots, printedPageAt, renderMotifPlacements, requiredSlotRefusal, resolvePlacements, safeImageSource, sanitize, sanitizeBookCss, searchText, selectBookParent, slotFillCopyId, slotFillRefusal, slotFillsConflict, unmeasuredSlotRefusal, windowPageIndex };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Media, Page, Placement } from "@segnavia/format";
|
|
2
|
+
//#region src/placements.d.ts
|
|
3
|
+
export declare const PLACEMENT_ATTRIBUTES: readonly ["data-segnavia-media", "data-segnavia-page", "data-segnavia-box", "data-segnavia-crop"];
|
|
4
|
+
export type ResolvedPlacement = {
|
|
5
|
+
kind: 'media';
|
|
6
|
+
media: Media;
|
|
7
|
+
src: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'page';
|
|
10
|
+
page: Page;
|
|
11
|
+
};
|
|
12
|
+
export type ResolvePlacement<R extends {
|
|
13
|
+
kind: string;
|
|
14
|
+
} = ResolvedPlacement> = (placement: Placement, context: {
|
|
15
|
+
purpose: 'screen' | 'print';
|
|
16
|
+
width?: number;
|
|
17
|
+
signal: AbortSignal;
|
|
18
|
+
}) => Promise<R | undefined>;
|
|
19
|
+
export interface PlacementPlanEntry {
|
|
20
|
+
index: number;
|
|
21
|
+
placement: Placement;
|
|
22
|
+
alt?: string;
|
|
23
|
+
lang?: string;
|
|
24
|
+
dir?: string;
|
|
25
|
+
width?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface PlacementRefusal {
|
|
28
|
+
index: number;
|
|
29
|
+
address?: string;
|
|
30
|
+
page?: string | number;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
export interface PlacementPlan {
|
|
34
|
+
entries: PlacementPlanEntry[];
|
|
35
|
+
refusals: PlacementRefusal[];
|
|
36
|
+
}
|
|
37
|
+
export type PlacementReport = (entry: {
|
|
38
|
+
address: string | undefined;
|
|
39
|
+
page: string | number | undefined;
|
|
40
|
+
reason: 'missing' | 'refused-src' | 'invalid' | 'failed';
|
|
41
|
+
message?: string;
|
|
42
|
+
}) => void;
|
|
43
|
+
export interface PlacementsOptions<R extends {
|
|
44
|
+
kind: string;
|
|
45
|
+
} = ResolvedPlacement> {
|
|
46
|
+
resolve: ResolvePlacement<R>;
|
|
47
|
+
report?: PlacementReport;
|
|
48
|
+
}
|
|
49
|
+
type ResolvedMedia = Extract<ResolvedPlacement, {
|
|
50
|
+
kind: 'media';
|
|
51
|
+
}>;
|
|
52
|
+
export type MediaAnswer = {
|
|
53
|
+
status: 'ok';
|
|
54
|
+
value: ResolvedMedia;
|
|
55
|
+
} | {
|
|
56
|
+
status: 'missing' | 'refused-src';
|
|
57
|
+
message?: undefined;
|
|
58
|
+
} | {
|
|
59
|
+
status: 'invalid';
|
|
60
|
+
message: string;
|
|
61
|
+
} | {
|
|
62
|
+
status: 'failed';
|
|
63
|
+
message: string;
|
|
64
|
+
};
|
|
65
|
+
export declare function classifyMediaAnswer(input: {
|
|
66
|
+
answer: {
|
|
67
|
+
kind: string;
|
|
68
|
+
} | undefined;
|
|
69
|
+
}): Exclude<MediaAnswer, {
|
|
70
|
+
status: 'failed';
|
|
71
|
+
}>;
|
|
72
|
+
export declare function classifyMediaAnswer(input: {
|
|
73
|
+
error: unknown;
|
|
74
|
+
}): Extract<MediaAnswer, {
|
|
75
|
+
status: 'failed';
|
|
76
|
+
}>;
|
|
77
|
+
export declare function placementCropEdges(crop: Placement['crop']): string | undefined;
|
|
78
|
+
export declare function placementPlan(html: string): PlacementPlan;
|
|
79
|
+
export declare function placementPlaceholderSource(width?: number, height?: number): string;
|
|
80
|
+
export declare function resolvePlacements<R extends {
|
|
81
|
+
kind: string;
|
|
82
|
+
}>(html: string, options: {
|
|
83
|
+
resolvePlacement: ResolvePlacement<R>;
|
|
84
|
+
purpose: 'screen' | 'print';
|
|
85
|
+
width: number;
|
|
86
|
+
signal: AbortSignal;
|
|
87
|
+
report?: PlacementReport;
|
|
88
|
+
}): Promise<string>;
|
|
89
|
+
//#endregion
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { elements, parse, safeImageSource, serialize } from "./sanitize.js";
|
|
2
|
+
import { Placement, mediaTextDirection } from "@segnavia/format";
|
|
3
|
+
//#region src/placements.ts
|
|
4
|
+
const PLACEMENT_ATTRIBUTES = [
|
|
5
|
+
"data-segnavia-media",
|
|
6
|
+
"data-segnavia-page",
|
|
7
|
+
"data-segnavia-box",
|
|
8
|
+
"data-segnavia-crop"
|
|
9
|
+
];
|
|
10
|
+
const [MEDIA_ATTRIBUTE, PAGE_ATTRIBUTE, BOX_ATTRIBUTE, CROP_ATTRIBUTE] = PLACEMENT_ATTRIBUTES;
|
|
11
|
+
function classifyMediaAnswer(input) {
|
|
12
|
+
if ("error" in input) return {
|
|
13
|
+
status: "failed",
|
|
14
|
+
message: input.error instanceof Error ? input.error.message : String(input.error)
|
|
15
|
+
};
|
|
16
|
+
const answer = input.answer;
|
|
17
|
+
if (!answer) return { status: "missing" };
|
|
18
|
+
if (answer.kind !== "media" || !("src" in answer) || !("media" in answer)) return {
|
|
19
|
+
status: "invalid",
|
|
20
|
+
message: `resolved ${answer.kind}, not media`
|
|
21
|
+
};
|
|
22
|
+
const media = answer;
|
|
23
|
+
return safeImageSource(media.src) ? {
|
|
24
|
+
status: "ok",
|
|
25
|
+
value: media
|
|
26
|
+
} : { status: "refused-src" };
|
|
27
|
+
}
|
|
28
|
+
function placementCropEdges(crop) {
|
|
29
|
+
if (!crop) return void 0;
|
|
30
|
+
return `${crop.top ?? 0} ${crop.right ?? 0} ${crop.bottom ?? 0} ${crop.left ?? 0}`;
|
|
31
|
+
}
|
|
32
|
+
function positiveInteger(value) {
|
|
33
|
+
return value !== void 0 && /^[1-9][0-9]*$/.test(value) ? Number(value) : void 0;
|
|
34
|
+
}
|
|
35
|
+
function pageSelector(value) {
|
|
36
|
+
if (value === void 0) return void 0;
|
|
37
|
+
return /^[1-9][0-9]*$/.test(value) ? Number(value) : value;
|
|
38
|
+
}
|
|
39
|
+
function cropValue(value) {
|
|
40
|
+
if (value === void 0) return void 0;
|
|
41
|
+
const edges = value.trim().split(/\s+/);
|
|
42
|
+
if (edges.length < 1 || edges.length > 4) return { top: value };
|
|
43
|
+
const [top, second, third, fourth] = edges;
|
|
44
|
+
return {
|
|
45
|
+
top,
|
|
46
|
+
right: second ?? top,
|
|
47
|
+
bottom: third ?? top,
|
|
48
|
+
left: fourth ?? second ?? top
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function plannedElement(element, index) {
|
|
52
|
+
const address = element.attribs[MEDIA_ATTRIBUTE];
|
|
53
|
+
const page = pageSelector(element.attribs[PAGE_ATTRIBUTE]);
|
|
54
|
+
try {
|
|
55
|
+
return {
|
|
56
|
+
index,
|
|
57
|
+
placement: Placement.parse({
|
|
58
|
+
kind: "media",
|
|
59
|
+
address,
|
|
60
|
+
...page === void 0 ? {} : { page },
|
|
61
|
+
...element.attribs[BOX_ATTRIBUTE] === void 0 ? {} : { box: element.attribs[BOX_ATTRIBUTE] },
|
|
62
|
+
...element.attribs[CROP_ATTRIBUTE] === void 0 ? {} : { crop: cropValue(element.attribs[CROP_ATTRIBUTE]) }
|
|
63
|
+
}, "page"),
|
|
64
|
+
..."alt" in element.attribs ? { alt: element.attribs.alt } : {},
|
|
65
|
+
..."lang" in element.attribs ? { lang: element.attribs.lang } : {},
|
|
66
|
+
..."dir" in element.attribs ? { dir: element.attribs.dir } : {},
|
|
67
|
+
...positiveInteger(element.attribs.width) === void 0 ? {} : { width: positiveInteger(element.attribs.width) }
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
index,
|
|
72
|
+
address,
|
|
73
|
+
page,
|
|
74
|
+
message: error instanceof Error ? error.message : String(error)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function planElements(found) {
|
|
79
|
+
const planned = found.flatMap((element, index) => element.name.toLowerCase() === "img" && MEDIA_ATTRIBUTE in element.attribs ? [plannedElement(element, index)] : []);
|
|
80
|
+
return {
|
|
81
|
+
entries: planned.filter((entry) => "placement" in entry),
|
|
82
|
+
refusals: planned.filter((entry) => !("placement" in entry))
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function placementPlan(html) {
|
|
86
|
+
return planElements(elements(parse(html).children));
|
|
87
|
+
}
|
|
88
|
+
function placementPlaceholderSource(width = 400, height = 300) {
|
|
89
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="#eee"/><rect x="0.5" y="0.5" width="${width - 1}" height="${height - 1}" fill="none" stroke="#aaa"/></svg>`;
|
|
90
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
91
|
+
}
|
|
92
|
+
function placeholder(element) {
|
|
93
|
+
const authoredWidth = positiveInteger(element.attribs.width);
|
|
94
|
+
const authoredHeight = positiveInteger(element.attribs.height);
|
|
95
|
+
const width = authoredWidth ?? (authoredHeight === void 0 ? 400 : Math.round(authoredHeight * 4 / 3));
|
|
96
|
+
const height = authoredHeight ?? (authoredWidth === void 0 ? 300 : Math.round(authoredWidth * 3 / 4));
|
|
97
|
+
element.attribs.src = placementPlaceholderSource(width, height);
|
|
98
|
+
element.attribs["data-segnavia-placement"] = "missing";
|
|
99
|
+
if (!("alt" in element.attribs)) element.attribs.alt = "";
|
|
100
|
+
for (const attribute of PLACEMENT_ATTRIBUTES) delete element.attribs[attribute];
|
|
101
|
+
}
|
|
102
|
+
function applyResolved(element, entry, resolved) {
|
|
103
|
+
element.attribs.src = resolved.src;
|
|
104
|
+
if (entry.alt === void 0 && resolved.media.alt) {
|
|
105
|
+
element.attribs.alt = resolved.media.alt.text;
|
|
106
|
+
if (entry.lang === void 0 && entry.dir === void 0) {
|
|
107
|
+
if (resolved.media.alt.lang) element.attribs.lang = resolved.media.alt.lang;
|
|
108
|
+
element.attribs.dir = mediaTextDirection(resolved.media.alt);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const crop = placementCropEdges(entry.placement.crop);
|
|
112
|
+
if (crop) {
|
|
113
|
+
const declaration = `object-view-box: inset(${crop})`;
|
|
114
|
+
const authored = element.attribs.style?.trim();
|
|
115
|
+
element.attribs.style = authored ? `${authored.replace(/;?$/, ";")} ${declaration}` : declaration;
|
|
116
|
+
}
|
|
117
|
+
for (const attribute of PLACEMENT_ATTRIBUTES) delete element.attribs[attribute];
|
|
118
|
+
}
|
|
119
|
+
function abortable(work, signal) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
if (signal.aborted) {
|
|
122
|
+
reject(signal.reason);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const aborted = () => reject(signal.reason);
|
|
126
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
127
|
+
work().then((value) => {
|
|
128
|
+
signal.removeEventListener("abort", aborted);
|
|
129
|
+
resolve(value);
|
|
130
|
+
}, (error) => {
|
|
131
|
+
signal.removeEventListener("abort", aborted);
|
|
132
|
+
reject(error);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function resolvePlacements(html, options) {
|
|
137
|
+
const document = parse(html);
|
|
138
|
+
const found = elements(document.children);
|
|
139
|
+
const plan = planElements(found);
|
|
140
|
+
if (plan.entries.length === 0 && plan.refusals.length === 0) return html;
|
|
141
|
+
options.signal.throwIfAborted();
|
|
142
|
+
for (const refusal of plan.refusals) {
|
|
143
|
+
const element = found[refusal.index];
|
|
144
|
+
if (element) placeholder(element);
|
|
145
|
+
options.report?.({
|
|
146
|
+
address: refusal.address,
|
|
147
|
+
page: refusal.page,
|
|
148
|
+
reason: "invalid",
|
|
149
|
+
message: refusal.message
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
await Promise.all(plan.entries.map(async (entry) => {
|
|
153
|
+
const element = found[entry.index];
|
|
154
|
+
if (!element) return;
|
|
155
|
+
try {
|
|
156
|
+
const resolved = await abortable(() => options.resolvePlacement(entry.placement, {
|
|
157
|
+
purpose: options.purpose,
|
|
158
|
+
width: entry.width ?? options.width,
|
|
159
|
+
signal: options.signal
|
|
160
|
+
}), options.signal);
|
|
161
|
+
options.signal.throwIfAborted();
|
|
162
|
+
const classified = classifyMediaAnswer({ answer: resolved });
|
|
163
|
+
if (classified.status === "ok") applyResolved(element, entry, classified.value);
|
|
164
|
+
else {
|
|
165
|
+
placeholder(element);
|
|
166
|
+
options.report?.({
|
|
167
|
+
address: entry.placement.address,
|
|
168
|
+
page: entry.placement.page,
|
|
169
|
+
reason: classified.status,
|
|
170
|
+
...classified.message === void 0 ? {} : { message: classified.message }
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (options.signal.aborted) throw options.signal.reason;
|
|
175
|
+
const classified = classifyMediaAnswer({ error });
|
|
176
|
+
placeholder(element);
|
|
177
|
+
options.report?.({
|
|
178
|
+
address: entry.placement.address,
|
|
179
|
+
page: entry.placement.page,
|
|
180
|
+
reason: classified.status,
|
|
181
|
+
message: classified.message
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
options.signal.throwIfAborted();
|
|
186
|
+
return serialize(document);
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
export { PLACEMENT_ATTRIBUTES, classifyMediaAnswer, placementCropEdges, placementPlaceholderSource, placementPlan, resolvePlacements };
|
package/dist/sanitize.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface SanitizeOptions {
|
|
|
8
8
|
dataFonts?: boolean;
|
|
9
9
|
refuseElements?: Iterable<string>;
|
|
10
10
|
}
|
|
11
|
+
export declare function safeImageSource(value: string): boolean;
|
|
11
12
|
export declare function findActiveContent(html: string, options?: SanitizeOptions): string[];
|
|
12
13
|
export declare function sanitize(html: string, options?: SanitizeOptions): string;
|
|
13
14
|
//#endregion
|
package/dist/sanitize.js
CHANGED
|
@@ -80,6 +80,9 @@ function insideSvg(element) {
|
|
|
80
80
|
function normalizeUrl(value) {
|
|
81
81
|
return [...value].filter((c) => c.charCodeAt(0) > 32 && c.charCodeAt(0) !== 127).join("").toLowerCase();
|
|
82
82
|
}
|
|
83
|
+
function safeImageSource(value) {
|
|
84
|
+
return DATA_IMAGE.test(normalizeUrl(value));
|
|
85
|
+
}
|
|
83
86
|
function safeUrl(attribute, value, allowed) {
|
|
84
87
|
const url = normalizeUrl(value);
|
|
85
88
|
if (attribute === "href" || attribute === "xlink:href") {
|
|
@@ -92,7 +95,7 @@ function safeUrl(attribute, value, allowed) {
|
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
if (attribute === "srcset") return DATA_IMAGE_SRCSET.test(url);
|
|
95
|
-
if (attribute === "src" || attribute === "poster") return
|
|
98
|
+
if (attribute === "src" || attribute === "poster") return safeImageSource(value);
|
|
96
99
|
return false;
|
|
97
100
|
}
|
|
98
101
|
function refusedElement(element, refused, keepScripts) {
|
|
@@ -208,4 +211,4 @@ function sanitize(html, options = {}) {
|
|
|
208
211
|
return serialize(document);
|
|
209
212
|
}
|
|
210
213
|
//#endregion
|
|
211
|
-
export { REFUSED_ELEMENTS, elements, findActiveContent, parse, sanitize, serialize };
|
|
214
|
+
export { REFUSED_ELEMENTS, elements, findActiveContent, parse, safeImageSource, sanitize, serialize };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segnavia/html",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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.39.0",
|
|
30
30
|
"dom-serializer": "^2.0.0",
|
|
31
31
|
"domhandler": "^5.0.3",
|
|
32
32
|
"htmlparser2": "^10.0.0",
|