@segnavia/html 0.27.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/bindings.d.ts +1 -1
- package/dist/bindings.js +4 -1
- 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/css-tokens.js +189 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +7 -2
- 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
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { paginateColumns } from "./paginate-columns.js";
|
|
2
|
+
import { paginateMarks } from "./paginate-marks.js";
|
|
3
|
+
//#region src/book/paginate-sequence.ts
|
|
4
|
+
function paginateSequence(input, fillPage) {
|
|
5
|
+
function emptyMarks() {
|
|
6
|
+
return Object.create(null);
|
|
7
|
+
}
|
|
8
|
+
function columnRefusal(block) {
|
|
9
|
+
const ceiling = 12;
|
|
10
|
+
const count = block.columns;
|
|
11
|
+
if (count === void 0) return void 0;
|
|
12
|
+
if (Number.isInteger(count) && count >= 1 && count <= ceiling) return void 0;
|
|
13
|
+
const position = `chapter ${block.position.chapter} block ${block.position.block}`;
|
|
14
|
+
return {
|
|
15
|
+
tag: block.tag,
|
|
16
|
+
position: block.position,
|
|
17
|
+
message: `The ${block.tag} at ${position} could not establish a column count; use an integer from 1 to 12 to clear this refusal.`
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function pagesRefusal(flow) {
|
|
21
|
+
if (flow.kind !== "fixed") return void 0;
|
|
22
|
+
if (Number.isSafeInteger(flow.pages) && flow.pages >= 0) return void 0;
|
|
23
|
+
return {
|
|
24
|
+
flow: flow.id,
|
|
25
|
+
message: `The ${flow.id} flow could not establish a page count; use a safe integer of 0 or more to clear this refusal.`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function boundsRefusal() {
|
|
29
|
+
for (const flow of input.flows) {
|
|
30
|
+
const pages = pagesRefusal(flow);
|
|
31
|
+
if (pages) return pages;
|
|
32
|
+
if (flow.kind === "fixed") continue;
|
|
33
|
+
for (const block of flow.blocks) {
|
|
34
|
+
const columns = columnRefusal(block);
|
|
35
|
+
if (columns) return columns;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function isFront(flow) {
|
|
40
|
+
if (flow.kind === "fixed") return flow.sequence === "front";
|
|
41
|
+
return flow.kind === "front" || flow.kind === "contents";
|
|
42
|
+
}
|
|
43
|
+
function unitsOf(flow) {
|
|
44
|
+
if (flow.kind === "fixed") return [];
|
|
45
|
+
const units = [];
|
|
46
|
+
for (const block of flow.blocks) for (const [index, line] of block.lines.entries()) {
|
|
47
|
+
const previous = units[units.length - 1];
|
|
48
|
+
const refs = (block.footnoteRefs ?? []).filter((ref) => ref.line === index).map((ref) => ref.note);
|
|
49
|
+
const unit = {
|
|
50
|
+
flowId: flow.id,
|
|
51
|
+
block,
|
|
52
|
+
index,
|
|
53
|
+
top: line.top,
|
|
54
|
+
bottom: line.bottom,
|
|
55
|
+
refs
|
|
56
|
+
};
|
|
57
|
+
if (previous) {
|
|
58
|
+
unit.prevBottom = previous.bottom;
|
|
59
|
+
previous.nextTop = line.top;
|
|
60
|
+
}
|
|
61
|
+
units.push(unit);
|
|
62
|
+
}
|
|
63
|
+
return units;
|
|
64
|
+
}
|
|
65
|
+
function flowParents(flow) {
|
|
66
|
+
return flow.parents === void 0 ? {} : { parents: flow.parents };
|
|
67
|
+
}
|
|
68
|
+
function openingOf(flow) {
|
|
69
|
+
if (flow.opens === "recto" || flow.opens === "verso" || flow.opens === "any") return flow.opens;
|
|
70
|
+
return input.settings.rectoOpenings ? "recto" : "any";
|
|
71
|
+
}
|
|
72
|
+
function needsOpeningBlank(opens, count) {
|
|
73
|
+
const nextIsRecto = count % 2 === 0;
|
|
74
|
+
if (opens === "recto") return !nextIsRecto;
|
|
75
|
+
if (opens === "verso") return nextIsRecto;
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function makeBlank(number, sequence, flow) {
|
|
79
|
+
return {
|
|
80
|
+
kind: "blank",
|
|
81
|
+
number,
|
|
82
|
+
sequence,
|
|
83
|
+
flow: flow.id,
|
|
84
|
+
role: "blank",
|
|
85
|
+
windows: [],
|
|
86
|
+
notes: [],
|
|
87
|
+
blank: true,
|
|
88
|
+
sectionOpening: false,
|
|
89
|
+
...flowParents(flow)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function makeContent(part, number, flow, opener, placed, fragments, marks) {
|
|
93
|
+
const layout = paginateColumns(placed);
|
|
94
|
+
return {
|
|
95
|
+
kind: part,
|
|
96
|
+
number,
|
|
97
|
+
sequence: part,
|
|
98
|
+
flow: flow.id,
|
|
99
|
+
role: opener ? "opener" : "body",
|
|
100
|
+
windows: layout.windows,
|
|
101
|
+
notes: fragments,
|
|
102
|
+
...layout.headerRepeat ? { headerRepeat: layout.headerRepeat } : {},
|
|
103
|
+
blank: false,
|
|
104
|
+
sectionOpening: opener,
|
|
105
|
+
...opener || flow.runningHead === void 0 ? {} : { runningHead: flow.runningHead },
|
|
106
|
+
marks,
|
|
107
|
+
...flowParents(flow)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function makeFixed(part, number, flow, opener, marks) {
|
|
111
|
+
const templated = flow.kind === "fixed" && flow.role === "body";
|
|
112
|
+
return {
|
|
113
|
+
kind: part,
|
|
114
|
+
number,
|
|
115
|
+
sequence: part,
|
|
116
|
+
flow: flow.id,
|
|
117
|
+
role: templated ? "body" : "fixed",
|
|
118
|
+
windows: [],
|
|
119
|
+
notes: [],
|
|
120
|
+
blank: false,
|
|
121
|
+
sectionOpening: opener,
|
|
122
|
+
...opener && !templated || flow.runningHead === void 0 ? {} : { runningHead: flow.runningHead },
|
|
123
|
+
marks,
|
|
124
|
+
...flowParents(flow)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function emitMarked(part, flow, opener, placed, fragments, pages, carried) {
|
|
128
|
+
const marked = paginateMarks(placed, carried);
|
|
129
|
+
pages.push(makeContent(part, pages.length + 1, flow, opener, placed, fragments, marked.marks));
|
|
130
|
+
return marked.carried;
|
|
131
|
+
}
|
|
132
|
+
function appendFixed(flow, part, pages, continued, carried) {
|
|
133
|
+
const count = flow.kind === "fixed" ? flow.pages : 0;
|
|
134
|
+
let next = carried;
|
|
135
|
+
for (let index = 0; index < count; index += 1) {
|
|
136
|
+
const opener = index === 0;
|
|
137
|
+
const marked = paginateMarks([], next);
|
|
138
|
+
next = marked.carried;
|
|
139
|
+
pages.push(makeFixed(part, pages.length + 1, flow, opener, marked.marks));
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
pending: continued,
|
|
143
|
+
carried: next
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function paginateFlow(flow, part, pages, continued, carried) {
|
|
147
|
+
if (flow.kind === "fixed") return appendFixed(flow, part, pages, continued, carried);
|
|
148
|
+
const units = unitsOf(flow);
|
|
149
|
+
let index = 0;
|
|
150
|
+
let pending = continued;
|
|
151
|
+
let opener = true;
|
|
152
|
+
let nextCarried = carried;
|
|
153
|
+
while (index < units.length || pending.length > 0) {
|
|
154
|
+
const page = fillPage(units, index, pending);
|
|
155
|
+
if (page.refusal) return {
|
|
156
|
+
refusal: page.refusal,
|
|
157
|
+
pending,
|
|
158
|
+
carried: nextCarried
|
|
159
|
+
};
|
|
160
|
+
if (page.done || page.placed.length === 0 && page.fragments.length === 0) return {
|
|
161
|
+
pending,
|
|
162
|
+
carried: nextCarried
|
|
163
|
+
};
|
|
164
|
+
nextCarried = emitMarked(part, flow, opener && page.placed.length > 0, page.placed, page.fragments, pages, nextCarried);
|
|
165
|
+
opener = opener && page.placed.length === 0;
|
|
166
|
+
index = page.index;
|
|
167
|
+
pending = page.pending;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
pending,
|
|
171
|
+
carried: nextCarried
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function flushNotes(continued, flow, part, pages, carried) {
|
|
175
|
+
let pending = continued;
|
|
176
|
+
let nextCarried = carried;
|
|
177
|
+
while (pending.length > 0) {
|
|
178
|
+
const page = fillPage([], 0, pending);
|
|
179
|
+
if (page.refusal) return {
|
|
180
|
+
refusal: page.refusal,
|
|
181
|
+
carried: nextCarried
|
|
182
|
+
};
|
|
183
|
+
if (page.done || page.placed.length === 0 && page.fragments.length === 0) return { carried: nextCarried };
|
|
184
|
+
nextCarried = emitMarked(part, flow, false, page.placed, page.fragments, pages, nextCarried);
|
|
185
|
+
pending = page.pending;
|
|
186
|
+
}
|
|
187
|
+
return { carried: nextCarried };
|
|
188
|
+
}
|
|
189
|
+
function flushBeforeFixed(flow, previous, continued, carried, part, pages) {
|
|
190
|
+
if (flow.kind !== "fixed" || continued.length === 0 || !previous) return {
|
|
191
|
+
continued,
|
|
192
|
+
carried
|
|
193
|
+
};
|
|
194
|
+
const flushed = flushNotes(continued, previous, part, pages, carried);
|
|
195
|
+
if ("refusal" in flushed && flushed.refusal) return { refusal: flushed.refusal };
|
|
196
|
+
return {
|
|
197
|
+
continued: [],
|
|
198
|
+
carried: flushed.carried
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function emitFlow(flow, previous, continued, carried, part, pages, physicalOffset) {
|
|
202
|
+
const held = flushBeforeFixed(flow, previous, continued, carried, part, pages);
|
|
203
|
+
if ("refusal" in held && held.refusal) return { refusal: held.refusal };
|
|
204
|
+
if (needsOpeningBlank(openingOf(flow), physicalOffset + pages.length)) pages.push(makeBlank(pages.length + 1, part, flow));
|
|
205
|
+
const result = paginateFlow(flow, part, pages, held.continued, held.carried);
|
|
206
|
+
if ("refusal" in result && result.refusal) return { refusal: result.refusal };
|
|
207
|
+
return {
|
|
208
|
+
continued: result.pending,
|
|
209
|
+
carried: result.carried
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function paginatePart(flows, part, physicalOffset = 0) {
|
|
213
|
+
const pages = [];
|
|
214
|
+
let continued = [];
|
|
215
|
+
let carried = emptyMarks();
|
|
216
|
+
let previous;
|
|
217
|
+
for (const flow of flows) {
|
|
218
|
+
const result = emitFlow(flow, previous, continued, carried, part, pages, physicalOffset);
|
|
219
|
+
if ("refusal" in result && result.refusal) return {
|
|
220
|
+
pages,
|
|
221
|
+
refusals: [result.refusal]
|
|
222
|
+
};
|
|
223
|
+
continued = result.continued;
|
|
224
|
+
carried = result.carried;
|
|
225
|
+
previous = flow;
|
|
226
|
+
}
|
|
227
|
+
const last = flows[flows.length - 1];
|
|
228
|
+
if (last) {
|
|
229
|
+
const flushed = flushNotes(continued, last, part, pages, carried);
|
|
230
|
+
if ("refusal" in flushed && flushed.refusal) return {
|
|
231
|
+
pages,
|
|
232
|
+
refusals: [flushed.refusal]
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
pages,
|
|
237
|
+
refusals: []
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const bounded = boundsRefusal();
|
|
241
|
+
if (bounded) return {
|
|
242
|
+
pages: [],
|
|
243
|
+
refusals: [bounded]
|
|
244
|
+
};
|
|
245
|
+
const frontFlows = input.flows.filter(isFront);
|
|
246
|
+
const bodyFlows = input.flows.filter((flow) => !isFront(flow));
|
|
247
|
+
const front = paginatePart(frontFlows, "front");
|
|
248
|
+
const firstBody = bodyFlows[0];
|
|
249
|
+
const lastFront = frontFlows[frontFlows.length - 1];
|
|
250
|
+
const padFront = input.settings.rectoOpenings ? front.pages.length % 2 === 1 : firstBody !== void 0 && needsOpeningBlank(openingOf(firstBody), front.pages.length);
|
|
251
|
+
if (front.pages.length > 0 && lastFront && padFront) front.pages.push(makeBlank(front.pages.length + 1, "front", lastFront));
|
|
252
|
+
const body = paginatePart(bodyFlows, "body", front.pages.length);
|
|
253
|
+
if (body.refusals.length > 0) return {
|
|
254
|
+
pages: [...front.pages, ...body.pages],
|
|
255
|
+
refusals: body.refusals
|
|
256
|
+
};
|
|
257
|
+
if (front.refusals.length > 0) return {
|
|
258
|
+
pages: [...front.pages, ...body.pages],
|
|
259
|
+
refusals: front.refusals
|
|
260
|
+
};
|
|
261
|
+
return {
|
|
262
|
+
pages: [...front.pages, ...body.pages],
|
|
263
|
+
refusals: []
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
export { paginateSequence };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PaginateBookPage } from "./paginate-types.js";
|
|
2
|
+
//#region src/book/paginate-target.d.ts
|
|
3
|
+
export type PaginateTargetPage = Pick<PaginateBookPage, 'blank' | 'flow' | 'role' | 'sectionOpening' | 'windows'>;
|
|
4
|
+
export interface PaginateTarget {
|
|
5
|
+
flow: string;
|
|
6
|
+
heldPage?: number;
|
|
7
|
+
top?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function heldPageNumber(pages: PaginateTargetPage[], index: number): number;
|
|
10
|
+
export declare function windowPageIndex(pages: PaginateTargetPage[], flow: string, top: number): number | undefined;
|
|
11
|
+
export declare function openerPageIndex(pages: PaginateTargetPage[], flow: string): number | undefined;
|
|
12
|
+
export declare function pageIndexForTarget(pages: PaginateTargetPage[], target: PaginateTarget): number | undefined;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/book/paginate-target.ts
|
|
2
|
+
function heldPageNumber(pages, index) {
|
|
3
|
+
const page = pages[index];
|
|
4
|
+
if (!page) return 0;
|
|
5
|
+
let seen = 0;
|
|
6
|
+
for (let i = 0; i <= index; i++) {
|
|
7
|
+
const held = pages[i];
|
|
8
|
+
if (!held || held.flow !== page.flow || held.blank) continue;
|
|
9
|
+
if (held.role !== "fixed" && !(held.role === "body" && held.windows.length === 0)) continue;
|
|
10
|
+
seen += 1;
|
|
11
|
+
}
|
|
12
|
+
return seen;
|
|
13
|
+
}
|
|
14
|
+
function windowPageIndex(pages, flow, top) {
|
|
15
|
+
for (const [index, page] of pages.entries()) {
|
|
16
|
+
if (page.blank) continue;
|
|
17
|
+
for (const win of page.windows) if (win.flow === flow && top >= win.start && top < win.end) return index;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function openerPageIndex(pages, flow) {
|
|
21
|
+
for (const [index, page] of pages.entries()) if (!page.blank && page.flow === flow && page.sectionOpening) return index;
|
|
22
|
+
}
|
|
23
|
+
function pageIndexForTarget(pages, target) {
|
|
24
|
+
const held = target.heldPage;
|
|
25
|
+
if (held !== void 0 && Number.isSafeInteger(held) && held > 0) {
|
|
26
|
+
for (let index = 0; index < pages.length; index++) if (pages[index]?.flow === target.flow && heldPageNumber(pages, index) === held) return index;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (target.top !== void 0) {
|
|
30
|
+
const windowed = windowPageIndex(pages, target.flow, target.top);
|
|
31
|
+
if (windowed !== void 0) return windowed;
|
|
32
|
+
}
|
|
33
|
+
return openerPageIndex(pages, target.flow);
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { heldPageNumber, openerPageIndex, pageIndexForTarget, windowPageIndex };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { BookOpens, BookSectionParents } from "@segnavia/format";
|
|
2
|
+
//#region src/book/paginate-types.d.ts
|
|
3
|
+
export type PaginateSequence = 'front' | 'body';
|
|
4
|
+
export type PaginatePageRole = 'opener' | 'body' | 'blank' | 'fixed';
|
|
5
|
+
export interface PaginateLine {
|
|
6
|
+
top: number;
|
|
7
|
+
bottom: number;
|
|
8
|
+
}
|
|
9
|
+
export interface PaginateNoteRef {
|
|
10
|
+
note: string;
|
|
11
|
+
line: number;
|
|
12
|
+
}
|
|
13
|
+
export interface PaginateMark {
|
|
14
|
+
name: string;
|
|
15
|
+
value: string;
|
|
16
|
+
line: number;
|
|
17
|
+
}
|
|
18
|
+
export interface PaginateBlock {
|
|
19
|
+
kind: 'heading' | 'figure' | 'table' | 'text';
|
|
20
|
+
tag: string;
|
|
21
|
+
position: {
|
|
22
|
+
chapter: number;
|
|
23
|
+
block: number;
|
|
24
|
+
};
|
|
25
|
+
lines: PaginateLine[];
|
|
26
|
+
columns?: number;
|
|
27
|
+
orphans?: number;
|
|
28
|
+
widows?: number;
|
|
29
|
+
keepWithNext?: boolean;
|
|
30
|
+
keepTogether?: boolean;
|
|
31
|
+
headerHeight?: number;
|
|
32
|
+
footnoteRefs?: PaginateNoteRef[];
|
|
33
|
+
marks?: PaginateMark[];
|
|
34
|
+
}
|
|
35
|
+
export type PaginateFlowParents = BookSectionParents;
|
|
36
|
+
export type PaginateOpens = BookOpens;
|
|
37
|
+
interface PaginateFlowBase {
|
|
38
|
+
id: string;
|
|
39
|
+
runningHead?: string;
|
|
40
|
+
opens?: PaginateOpens;
|
|
41
|
+
parents?: PaginateFlowParents;
|
|
42
|
+
}
|
|
43
|
+
export type PaginateFlow = (PaginateFlowBase & {
|
|
44
|
+
kind: 'front' | 'contents' | 'chapter' | 'endnotes';
|
|
45
|
+
blocks: PaginateBlock[];
|
|
46
|
+
}) | (PaginateFlowBase & {
|
|
47
|
+
kind: 'fixed';
|
|
48
|
+
pages: number;
|
|
49
|
+
sequence?: PaginateSequence;
|
|
50
|
+
role?: 'fixed' | 'body';
|
|
51
|
+
});
|
|
52
|
+
export interface PaginateNote {
|
|
53
|
+
id: string;
|
|
54
|
+
lines: PaginateLine[];
|
|
55
|
+
}
|
|
56
|
+
export interface PaginateBookInput {
|
|
57
|
+
flows: PaginateFlow[];
|
|
58
|
+
notes: PaginateNote[];
|
|
59
|
+
geometry: {
|
|
60
|
+
textHeight: number;
|
|
61
|
+
notesGap: number;
|
|
62
|
+
};
|
|
63
|
+
settings: {
|
|
64
|
+
orphans?: number;
|
|
65
|
+
widows?: number;
|
|
66
|
+
rectoOpenings: boolean;
|
|
67
|
+
direction: 'ltr' | 'rtl';
|
|
68
|
+
endnotePlacement: 'chapter' | 'book';
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export interface PaginateWindow {
|
|
72
|
+
flow: string;
|
|
73
|
+
start: number;
|
|
74
|
+
end: number;
|
|
75
|
+
top: number;
|
|
76
|
+
column: number;
|
|
77
|
+
columns: number;
|
|
78
|
+
}
|
|
79
|
+
export interface PaginatePageNote {
|
|
80
|
+
id: string;
|
|
81
|
+
whole: boolean;
|
|
82
|
+
start: number;
|
|
83
|
+
end: number;
|
|
84
|
+
}
|
|
85
|
+
export type PaginatePageMarks = Record<string, {
|
|
86
|
+
first: string;
|
|
87
|
+
last: string;
|
|
88
|
+
}>;
|
|
89
|
+
export interface PaginateBookPage {
|
|
90
|
+
kind: 'front' | 'body' | 'blank';
|
|
91
|
+
number: number;
|
|
92
|
+
sequence: PaginateSequence;
|
|
93
|
+
flow: string;
|
|
94
|
+
role: PaginatePageRole;
|
|
95
|
+
windows: PaginateWindow[];
|
|
96
|
+
notes: PaginatePageNote[];
|
|
97
|
+
headerRepeat?: number;
|
|
98
|
+
blank: boolean;
|
|
99
|
+
sectionOpening: boolean;
|
|
100
|
+
runningHead?: string;
|
|
101
|
+
marks?: PaginatePageMarks;
|
|
102
|
+
parents?: PaginateFlowParents;
|
|
103
|
+
}
|
|
104
|
+
export type PaginateBookRefusal = {
|
|
105
|
+
tag: string;
|
|
106
|
+
position: {
|
|
107
|
+
chapter: number;
|
|
108
|
+
block: number;
|
|
109
|
+
};
|
|
110
|
+
message: string;
|
|
111
|
+
} | {
|
|
112
|
+
flow: string;
|
|
113
|
+
message: string;
|
|
114
|
+
} | {
|
|
115
|
+
note: string;
|
|
116
|
+
message: string;
|
|
117
|
+
};
|
|
118
|
+
export interface PaginateBookResult {
|
|
119
|
+
pages: PaginateBookPage[];
|
|
120
|
+
refusals: PaginateBookRefusal[];
|
|
121
|
+
}
|
|
122
|
+
export interface PaginateUnit {
|
|
123
|
+
flowId: string;
|
|
124
|
+
block: PaginateBlock;
|
|
125
|
+
index: number;
|
|
126
|
+
top: number;
|
|
127
|
+
bottom: number;
|
|
128
|
+
refs: string[];
|
|
129
|
+
prevBottom?: number;
|
|
130
|
+
nextTop?: number;
|
|
131
|
+
}
|
|
132
|
+
export interface PaginatePageFill {
|
|
133
|
+
placed: PaginateUnit[];
|
|
134
|
+
fragments: PaginatePageNote[];
|
|
135
|
+
index: number;
|
|
136
|
+
pending: PaginatePageNote[];
|
|
137
|
+
refusal?: PaginateBookRefusal;
|
|
138
|
+
done?: boolean;
|
|
139
|
+
}
|
|
140
|
+
export type PaginateNotesUsed = (placed: PaginateUnit[], fragments: PaginatePageNote[]) => number;
|
|
141
|
+
interface PaginateNotesBase {
|
|
142
|
+
notes: PaginateNote[];
|
|
143
|
+
geometry: {
|
|
144
|
+
textHeight: number;
|
|
145
|
+
notesGap: number;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export type PaginateNotesOp = (PaginateNotesBase & {
|
|
149
|
+
op: 'space';
|
|
150
|
+
fragments: PaginatePageNote[];
|
|
151
|
+
}) | (PaginateNotesBase & {
|
|
152
|
+
op: 'continue';
|
|
153
|
+
pending: PaginatePageNote[];
|
|
154
|
+
used: PaginateNotesUsed;
|
|
155
|
+
}) | (PaginateNotesBase & {
|
|
156
|
+
op: 'allocate';
|
|
157
|
+
placed: PaginateUnit[];
|
|
158
|
+
fragments: PaginatePageNote[];
|
|
159
|
+
unit: PaginateUnit;
|
|
160
|
+
used: PaginateNotesUsed;
|
|
161
|
+
}) | (PaginateNotesBase & {
|
|
162
|
+
op: 'recompute';
|
|
163
|
+
placed: PaginateUnit[];
|
|
164
|
+
seed: PaginatePageNote[];
|
|
165
|
+
used: PaginateNotesUsed;
|
|
166
|
+
}) | (PaginateNotesBase & {
|
|
167
|
+
op: 'leftover';
|
|
168
|
+
fragments: PaginatePageNote[];
|
|
169
|
+
});
|
|
170
|
+
//#endregion
|
package/dist/book/types.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface BookDocumentOptions {
|
|
|
8
8
|
}
|
|
9
9
|
export interface BookSectionInfo {
|
|
10
10
|
id: string;
|
|
11
|
-
kind: 'front' | 'contents' | 'chapter' | 'endnotes';
|
|
11
|
+
kind: 'front' | 'contents' | 'chapter' | 'endnotes' | 'designed' | 'templated';
|
|
12
12
|
title?: string;
|
|
13
13
|
runningHead?: string;
|
|
14
14
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { CssSanitizeResult } from "./sanitize-css.js";
|
|
2
|
+
//#region src/book-css.d.ts
|
|
3
|
+
export declare function sanitizeBookCss(css: string | undefined): CssSanitizeResult;
|
|
4
|
+
export declare function bookStylesheet(fontCss: string | undefined, bookCss: string | undefined, hostCss: string | undefined): string;
|
|
5
|
+
//#endregion
|
package/dist/book-css.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { sanitizeCss } from "./sanitize-css.js";
|
|
2
|
+
//#region src/book-css.ts
|
|
3
|
+
function sanitizeBookCss(css) {
|
|
4
|
+
const sanitized = sanitizeCss(css ?? "", false, false);
|
|
5
|
+
return {
|
|
6
|
+
css: sanitized.css.replaceAll("<", "\\3c "),
|
|
7
|
+
removed: sanitized.removed
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function bookStylesheet(fontCss, bookCss, hostCss) {
|
|
11
|
+
return `${fontCss ?? ""}${sanitizeBookCss(bookCss).css}${hostCss ?? ""}`;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { bookStylesheet, sanitizeBookCss };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
//#region src/css-tokens.ts
|
|
2
|
+
function newlineEnd(source, at) {
|
|
3
|
+
return source[at] === "\r" && source[at + 1] === "\n" ? at + 2 : at + 1;
|
|
4
|
+
}
|
|
5
|
+
function cssEscape(source, at) {
|
|
6
|
+
let end = at + 1;
|
|
7
|
+
if (end >= source.length) return ["", end];
|
|
8
|
+
if (/[0-9a-f]/i.test(source[end] ?? "")) {
|
|
9
|
+
const start = end;
|
|
10
|
+
while (end < source.length && end - start < 6 && /[0-9a-f]/i.test(source[end] ?? "")) end++;
|
|
11
|
+
const point = Number.parseInt(source.slice(start, end), 16);
|
|
12
|
+
if (source[end] === "\r" && source[end + 1] === "\n") end += 2;
|
|
13
|
+
else if (/\s/.test(source[end] ?? "")) end++;
|
|
14
|
+
return [point === 0 || point > 1114111 ? "�" : String.fromCodePoint(point), end];
|
|
15
|
+
}
|
|
16
|
+
if (source[end] === "\n" || source[end] === "\r" || source[end] === "\f") return ["", newlineEnd(source, end)];
|
|
17
|
+
return [source[end] ?? "", end + 1];
|
|
18
|
+
}
|
|
19
|
+
function name(source, at) {
|
|
20
|
+
let value = "";
|
|
21
|
+
let end = at;
|
|
22
|
+
while (end < source.length) {
|
|
23
|
+
const character = source[end] ?? "";
|
|
24
|
+
if (/[\w-]/.test(character) || character.charCodeAt(0) >= 128) {
|
|
25
|
+
value += character;
|
|
26
|
+
end++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (character !== "\\") break;
|
|
30
|
+
const escaped = cssEscape(source, end);
|
|
31
|
+
value += escaped[0];
|
|
32
|
+
end = escaped[1];
|
|
33
|
+
}
|
|
34
|
+
return [value.toLowerCase(), end];
|
|
35
|
+
}
|
|
36
|
+
function skipTrivia(source, at) {
|
|
37
|
+
if (/\s/.test(source[at] ?? "")) return at + 1;
|
|
38
|
+
if (!source.startsWith("/*", at)) return at;
|
|
39
|
+
const close = source.indexOf("*/", at + 2);
|
|
40
|
+
return close < 0 ? source.length : close + 2;
|
|
41
|
+
}
|
|
42
|
+
function stringToken(source, start) {
|
|
43
|
+
const quote = source[start] ?? "";
|
|
44
|
+
let at = start + 1;
|
|
45
|
+
let value = "";
|
|
46
|
+
while (at < source.length && source[at] !== quote) {
|
|
47
|
+
if (source[at] === "\n" || source[at] === "\r" || source[at] === "\f") break;
|
|
48
|
+
if (source[at] === "\\") {
|
|
49
|
+
const escaped = cssEscape(source, at);
|
|
50
|
+
value += escaped[0];
|
|
51
|
+
at = escaped[1];
|
|
52
|
+
} else value += source[at++] ?? "";
|
|
53
|
+
}
|
|
54
|
+
if (source[at] === quote) at++;
|
|
55
|
+
return {
|
|
56
|
+
end: at,
|
|
57
|
+
kind: "string",
|
|
58
|
+
start,
|
|
59
|
+
value
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function badUrlEnd(source, at) {
|
|
63
|
+
while (at < source.length) {
|
|
64
|
+
if (source[at] === ")") return at + 1;
|
|
65
|
+
if (source[at] === "\\") at = cssEscape(source, at)[1];
|
|
66
|
+
else at++;
|
|
67
|
+
}
|
|
68
|
+
return at;
|
|
69
|
+
}
|
|
70
|
+
function invalidUrlCharacter(character) {
|
|
71
|
+
const code = character.charCodeAt(0);
|
|
72
|
+
return character === "\"" || character === "'" || character === "(" || code <= 8 || code === 11 || code >= 14 && code <= 31 || code === 127;
|
|
73
|
+
}
|
|
74
|
+
function urlAfterWhitespace(source, start, at, value) {
|
|
75
|
+
let end = at;
|
|
76
|
+
while (/\s/.test(source[end] ?? "")) end++;
|
|
77
|
+
if (source[end] === ")") return {
|
|
78
|
+
end: end + 1,
|
|
79
|
+
kind: "url",
|
|
80
|
+
start,
|
|
81
|
+
value: value.trim().toLowerCase()
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
end: badUrlEnd(source, end),
|
|
85
|
+
kind: "bad-url",
|
|
86
|
+
start,
|
|
87
|
+
value
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function escapedUrlPart(source, start, at, value) {
|
|
91
|
+
const escaped = cssEscape(source, at);
|
|
92
|
+
if (escaped[1] === at + 1 || escaped[0] === "") return {
|
|
93
|
+
end: badUrlEnd(source, escaped[1]),
|
|
94
|
+
kind: "bad-url",
|
|
95
|
+
start,
|
|
96
|
+
value
|
|
97
|
+
};
|
|
98
|
+
return [escaped[0], escaped[1]];
|
|
99
|
+
}
|
|
100
|
+
function urlToken(source, start, content) {
|
|
101
|
+
let at = content;
|
|
102
|
+
let value = "";
|
|
103
|
+
while (/\s/.test(source[at] ?? "")) at++;
|
|
104
|
+
if (source[at] === "\"" || source[at] === "'") return {
|
|
105
|
+
end: content,
|
|
106
|
+
kind: "function",
|
|
107
|
+
start,
|
|
108
|
+
value: "url"
|
|
109
|
+
};
|
|
110
|
+
while (at < source.length) {
|
|
111
|
+
const character = source[at] ?? "";
|
|
112
|
+
if (character === ")") return {
|
|
113
|
+
end: at + 1,
|
|
114
|
+
kind: "url",
|
|
115
|
+
start,
|
|
116
|
+
value: value.trim().toLowerCase()
|
|
117
|
+
};
|
|
118
|
+
if (/\s/.test(character)) return urlAfterWhitespace(source, start, at, value);
|
|
119
|
+
if (invalidUrlCharacter(character)) return {
|
|
120
|
+
end: badUrlEnd(source, at + 1),
|
|
121
|
+
kind: "bad-url",
|
|
122
|
+
start,
|
|
123
|
+
value
|
|
124
|
+
};
|
|
125
|
+
if (character === "\\") {
|
|
126
|
+
const escaped = escapedUrlPart(source, start, at, value);
|
|
127
|
+
if (!Array.isArray(escaped)) return escaped;
|
|
128
|
+
value += escaped[0];
|
|
129
|
+
at = escaped[1];
|
|
130
|
+
} else {
|
|
131
|
+
value += character;
|
|
132
|
+
at++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
end: at,
|
|
137
|
+
kind: "url",
|
|
138
|
+
start,
|
|
139
|
+
value: value.trim().toLowerCase()
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function tokenAt(source, at) {
|
|
143
|
+
const character = source[at] ?? "";
|
|
144
|
+
if (character === "\"" || character === "'") return stringToken(source, at);
|
|
145
|
+
const offset = character === "@" ? 1 : 0;
|
|
146
|
+
const parsed = name(source, at + offset);
|
|
147
|
+
if (parsed[0]) {
|
|
148
|
+
const isFunction = offset === 0 && source[parsed[1]] === "(";
|
|
149
|
+
if (isFunction && parsed[0] === "url") return urlToken(source, at, parsed[1] + 1);
|
|
150
|
+
const kind = offset === 1 ? "at" : isFunction ? "function" : "word";
|
|
151
|
+
return {
|
|
152
|
+
end: parsed[1] + (isFunction ? 1 : 0),
|
|
153
|
+
kind,
|
|
154
|
+
start: at,
|
|
155
|
+
value: parsed[0]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
end: at + 1,
|
|
160
|
+
kind: {
|
|
161
|
+
"{": "open",
|
|
162
|
+
"}": "close",
|
|
163
|
+
";": "semicolon",
|
|
164
|
+
"(": "open",
|
|
165
|
+
"[": "open",
|
|
166
|
+
")": "close",
|
|
167
|
+
"]": "close"
|
|
168
|
+
}[character] ?? "word",
|
|
169
|
+
start: at,
|
|
170
|
+
value: character
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function cssTokens(source) {
|
|
174
|
+
const found = [];
|
|
175
|
+
let at = 0;
|
|
176
|
+
while (at < source.length) {
|
|
177
|
+
const significant = skipTrivia(source, at);
|
|
178
|
+
if (significant !== at) {
|
|
179
|
+
at = significant;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const token = tokenAt(source, at);
|
|
183
|
+
found.push(token);
|
|
184
|
+
at = token.end;
|
|
185
|
+
}
|
|
186
|
+
return found;
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
export { cssEscape, cssTokens };
|