@segnavia/html 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base64.d.ts +4 -0
- package/dist/base64.js +51 -0
- package/dist/bindings.d.ts +4 -1
- package/dist/bindings.js +19 -2
- package/dist/book/book-document.js +146 -65
- package/dist/book/book-held.js +182 -0
- package/dist/book/book-view.d.ts +5 -0
- package/dist/book/book-view.js +26 -0
- package/dist/book/furniture.d.ts +14 -0
- package/dist/book/furniture.js +36 -0
- package/dist/book/notes.js +18 -9
- package/dist/book/paginate-book.d.ts +11 -0
- package/dist/book/paginate-book.js +195 -0
- package/dist/book/paginate-columns.d.ts +8 -0
- package/dist/book/paginate-columns.js +119 -0
- package/dist/book/paginate-keeps.d.ts +4 -0
- package/dist/book/paginate-keeps.js +80 -0
- package/dist/book/paginate-marks.d.ts +7 -0
- package/dist/book/paginate-marks.js +34 -0
- package/dist/book/paginate-notes.d.ts +21 -0
- package/dist/book/paginate-notes.js +105 -0
- package/dist/book/paginate-sequence.d.ts +4 -0
- package/dist/book/paginate-sequence.js +267 -0
- package/dist/book/paginate-target.d.ts +13 -0
- package/dist/book/paginate-target.js +36 -0
- package/dist/book/paginate-types.d.ts +170 -0
- package/dist/book/types.d.ts +1 -1
- package/dist/book-css.d.ts +5 -0
- package/dist/book-css.js +14 -0
- package/dist/control-char.js +10 -0
- package/dist/css-tokens.js +189 -0
- package/dist/font-css.d.ts +11 -0
- package/dist/font-css.js +75 -0
- package/dist/font-format.d.ts +4 -0
- package/dist/font-format.js +11 -0
- package/dist/font-references.d.ts +4 -0
- package/dist/font-references.js +192 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.js +14 -2
- package/dist/motifs.d.ts +32 -0
- package/dist/motifs.js +245 -0
- package/dist/printed-pages.d.ts +42 -0
- package/dist/printed-pages.js +97 -0
- package/dist/sanitize-css.d.ts +6 -0
- package/dist/sanitize-css.js +308 -0
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +37 -15
- package/dist/unicode-range.d.ts +11 -0
- package/dist/unicode-range.js +86 -0
- package/package.json +2 -2
|
@@ -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 };
|
package/dist/book/notes.js
CHANGED
|
@@ -37,9 +37,13 @@ function noteState() {
|
|
|
37
37
|
globalFootnote: 0,
|
|
38
38
|
chapterFootnotes: /* @__PURE__ */ new Map(),
|
|
39
39
|
endnoteCount: 0,
|
|
40
|
-
|
|
40
|
+
chapterEndnotes: /* @__PURE__ */ new Map(),
|
|
41
|
+
footnoteParagraphs: []
|
|
41
42
|
};
|
|
42
43
|
}
|
|
44
|
+
function endnotesInOrder(state) {
|
|
45
|
+
return [...state.chapterEndnotes.values()].flat();
|
|
46
|
+
}
|
|
43
47
|
function nearestBlock(note) {
|
|
44
48
|
for (let parent = note.parent; parent; parent = parent.parent) if (isTag(parent) && BLOCKS.has(parent.name)) return parent;
|
|
45
49
|
}
|
|
@@ -79,7 +83,7 @@ function addFootnoteBlock(block, paragraph, blocks) {
|
|
|
79
83
|
if (INSIDE_BLOCKS.has(block.name)) append(block, notes);
|
|
80
84
|
else insertAfter(block, notes);
|
|
81
85
|
}
|
|
82
|
-
function transformFootnote(note, book, chapter, state, blocks) {
|
|
86
|
+
function transformFootnote(note, book, chapter, state, blocks, galley) {
|
|
83
87
|
const block = nearestBlock(note);
|
|
84
88
|
if (!block) return;
|
|
85
89
|
const number = footnoteNumber(book, chapter, state);
|
|
@@ -88,10 +92,12 @@ function transformFootnote(note, book, chapter, state, blocks) {
|
|
|
88
92
|
const marker = noteMarker(number, book.footnotes.marker);
|
|
89
93
|
const content = [...note.children];
|
|
90
94
|
replace(note, element("sup", { class: "segnavia-note-ref" }, [markerLink(refId, noteId, marker)]));
|
|
91
|
-
|
|
95
|
+
const paragraph = noteParagraph("segnavia-footnote", noteId, refId, marker, content);
|
|
96
|
+
if (galley) state.footnoteParagraphs.push(paragraph);
|
|
97
|
+
else addFootnoteBlock(block, paragraph, blocks);
|
|
92
98
|
state.footnotes = true;
|
|
93
99
|
}
|
|
94
|
-
function transformEndnote(note, book, state) {
|
|
100
|
+
function transformEndnote(note, book, state, owner) {
|
|
95
101
|
state.endnoteCount += 1;
|
|
96
102
|
const number = state.endnoteCount;
|
|
97
103
|
const noteId = `segnavia-en-${number}`;
|
|
@@ -99,14 +105,17 @@ function transformEndnote(note, book, state) {
|
|
|
99
105
|
const marker = noteMarker(number, book.endnotes.marker);
|
|
100
106
|
const content = [...note.children];
|
|
101
107
|
replace(note, element("sup", { class: "segnavia-note-ref" }, [markerLink(refId, noteId, marker)]));
|
|
102
|
-
|
|
108
|
+
const paragraph = noteParagraph("segnavia-endnote", noteId, refId, marker, content);
|
|
109
|
+
const held = state.chapterEndnotes.get(owner) ?? [];
|
|
110
|
+
held.push(paragraph);
|
|
111
|
+
state.chapterEndnotes.set(owner, held);
|
|
103
112
|
state.endnotes = true;
|
|
104
113
|
}
|
|
105
|
-
function transformNotes(root, book, chapter, state) {
|
|
114
|
+
function transformNotes(root, book, chapter, state, galley = false, notesOwner = chapter) {
|
|
106
115
|
const blocks = /* @__PURE__ */ new Map();
|
|
107
116
|
const notes = elements(root.children).filter((node) => node.name === "span" && (node.attribs["data-note"] === "footnote" || node.attribs["data-note"] === "endnote"));
|
|
108
|
-
for (const note of notes) if (note.attribs["data-note"] === "footnote") transformFootnote(note, book, chapter, state, blocks);
|
|
109
|
-
else transformEndnote(note, book, state);
|
|
117
|
+
for (const note of notes) if (note.attribs["data-note"] === "footnote") transformFootnote(note, book, chapter, state, blocks, galley);
|
|
118
|
+
else transformEndnote(note, book, state, notesOwner);
|
|
110
119
|
}
|
|
111
120
|
//#endregion
|
|
112
|
-
export { noteState, transformNotes };
|
|
121
|
+
export { endnotesInOrder, noteState, transformNotes };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PaginateBookInput, PaginateBookResult } from "./paginate-types.js";
|
|
2
|
+
import { paginateColumns } from "./paginate-columns.js";
|
|
3
|
+
import { paginateKeeps } from "./paginate-keeps.js";
|
|
4
|
+
import { paginateMarks } from "./paginate-marks.js";
|
|
5
|
+
import { paginateNotes } from "./paginate-notes.js";
|
|
6
|
+
import { paginateSequence } from "./paginate-sequence.js";
|
|
7
|
+
//#region src/book/paginate-book.d.ts
|
|
8
|
+
export declare function paginateBook(input: PaginateBookInput): PaginateBookResult;
|
|
9
|
+
export declare const PAGINATE_FUNCTIONS: (typeof paginateColumns | typeof paginateNotes | typeof paginateKeeps | typeof paginateMarks | typeof paginateSequence)[];
|
|
10
|
+
export declare function paginateScript(): string;
|
|
11
|
+
//#endregion
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { paginateColumns } from "./paginate-columns.js";
|
|
2
|
+
import { paginateKeeps } from "./paginate-keeps.js";
|
|
3
|
+
import { paginateMarks } from "./paginate-marks.js";
|
|
4
|
+
import { paginateNotes } from "./paginate-notes.js";
|
|
5
|
+
import { paginateSequence } from "./paginate-sequence.js";
|
|
6
|
+
//#region src/book/paginate-book.ts
|
|
7
|
+
function paginateBook(input) {
|
|
8
|
+
const textHeight = input.geometry.textHeight;
|
|
9
|
+
const orphans = input.settings.orphans ?? 2;
|
|
10
|
+
const widows = input.settings.widows ?? 2;
|
|
11
|
+
function used(placed, fragments) {
|
|
12
|
+
return paginateColumns(placed).height + paginateNotes({
|
|
13
|
+
op: "space",
|
|
14
|
+
notes: input.notes,
|
|
15
|
+
geometry: input.geometry,
|
|
16
|
+
fragments
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function withNotes(extra) {
|
|
20
|
+
return {
|
|
21
|
+
notes: input.notes,
|
|
22
|
+
geometry: input.geometry,
|
|
23
|
+
used,
|
|
24
|
+
...extra
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function refusal(block) {
|
|
28
|
+
const position = `chapter ${block.position.chapter} block ${block.position.block}`;
|
|
29
|
+
return {
|
|
30
|
+
tag: block.tag,
|
|
31
|
+
position: block.position,
|
|
32
|
+
message: `The ${block.tag} at ${position} could not establish a fit in the text area; shorten it or increase the text-area height to clear this refusal.`
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function blockForNote(id) {
|
|
36
|
+
for (const flow of input.flows) {
|
|
37
|
+
if (flow.kind === "fixed") continue;
|
|
38
|
+
for (const candidate of flow.blocks) if (candidate.footnoteRefs?.some((ref) => ref.note === id)) return candidate;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function noteRefusal(id) {
|
|
42
|
+
const clears = "use a taller text area or a shorter note line to clear this refusal.";
|
|
43
|
+
const found = blockForNote(id);
|
|
44
|
+
if (!found) return {
|
|
45
|
+
note: id,
|
|
46
|
+
message: `Note ${id} could not establish a fit for a note line in the text area; ${clears}`
|
|
47
|
+
};
|
|
48
|
+
const where = `chapter ${found.position.chapter} block ${found.position.block}`;
|
|
49
|
+
return {
|
|
50
|
+
tag: found.tag,
|
|
51
|
+
position: found.position,
|
|
52
|
+
message: `The ${found.tag} at ${where} could not establish a fit for a note line in the text area; ${clears}`
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function lineTooTall(unit) {
|
|
56
|
+
return unit.bottom - unit.top > textHeight;
|
|
57
|
+
}
|
|
58
|
+
function greedyFill(placed, fragments, units, index) {
|
|
59
|
+
let next = index;
|
|
60
|
+
let notes = fragments;
|
|
61
|
+
while (next < units.length) {
|
|
62
|
+
const unit = units[next];
|
|
63
|
+
if (!unit || lineTooTall(unit)) break;
|
|
64
|
+
const allocated = paginateNotes(withNotes({
|
|
65
|
+
op: "allocate",
|
|
66
|
+
placed,
|
|
67
|
+
fragments: notes,
|
|
68
|
+
unit
|
|
69
|
+
}));
|
|
70
|
+
if (!allocated) break;
|
|
71
|
+
placed.push(unit);
|
|
72
|
+
notes = allocated;
|
|
73
|
+
next += 1;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
placed,
|
|
77
|
+
fragments: notes,
|
|
78
|
+
index: next
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function forcePlace(unit) {
|
|
82
|
+
if (lineTooTall(unit)) return {
|
|
83
|
+
placed: [],
|
|
84
|
+
fragments: [],
|
|
85
|
+
index: 0,
|
|
86
|
+
pending: [],
|
|
87
|
+
refusal: refusal(unit.block)
|
|
88
|
+
};
|
|
89
|
+
const allocated = paginateNotes(withNotes({
|
|
90
|
+
op: "allocate",
|
|
91
|
+
placed: [],
|
|
92
|
+
fragments: [],
|
|
93
|
+
unit
|
|
94
|
+
}));
|
|
95
|
+
if (!allocated) return {
|
|
96
|
+
placed: [],
|
|
97
|
+
fragments: [],
|
|
98
|
+
index: 0,
|
|
99
|
+
pending: [],
|
|
100
|
+
refusal: refusal(unit.block)
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
placed: [unit],
|
|
104
|
+
fragments: allocated,
|
|
105
|
+
index: 0,
|
|
106
|
+
pending: paginateNotes(withNotes({
|
|
107
|
+
op: "leftover",
|
|
108
|
+
fragments: allocated
|
|
109
|
+
}))
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function fillPage(units, index, pending) {
|
|
113
|
+
const held = paginateNotes(withNotes({
|
|
114
|
+
op: "continue",
|
|
115
|
+
pending
|
|
116
|
+
}));
|
|
117
|
+
const incoming = units[index];
|
|
118
|
+
if (held.leftover.length === 0 && incoming && lineTooTall(incoming)) return {
|
|
119
|
+
placed: [],
|
|
120
|
+
fragments: [],
|
|
121
|
+
index,
|
|
122
|
+
pending,
|
|
123
|
+
refusal: refusal(incoming.block)
|
|
124
|
+
};
|
|
125
|
+
const stuck = held.fragments.length === 0 ? held.leftover[0] : void 0;
|
|
126
|
+
if (stuck) return {
|
|
127
|
+
placed: [],
|
|
128
|
+
fragments: [],
|
|
129
|
+
index,
|
|
130
|
+
pending: held.leftover,
|
|
131
|
+
refusal: noteRefusal(stuck.id)
|
|
132
|
+
};
|
|
133
|
+
if (held.leftover.length > 0) return {
|
|
134
|
+
placed: [],
|
|
135
|
+
fragments: held.fragments,
|
|
136
|
+
index,
|
|
137
|
+
pending: held.leftover
|
|
138
|
+
};
|
|
139
|
+
const filled = greedyFill([], held.fragments, units, index);
|
|
140
|
+
const placed = paginateKeeps(filled.placed, units[filled.index], textHeight, orphans, widows, held.fragments.length > 0);
|
|
141
|
+
const nextIndex = filled.index - (filled.placed.length - placed.length);
|
|
142
|
+
const fragments = paginateNotes(withNotes({
|
|
143
|
+
op: "recompute",
|
|
144
|
+
placed,
|
|
145
|
+
seed: held.fragments
|
|
146
|
+
}));
|
|
147
|
+
if (placed.length > 0 || fragments.length > 0) return {
|
|
148
|
+
placed,
|
|
149
|
+
fragments,
|
|
150
|
+
index: nextIndex,
|
|
151
|
+
pending: paginateNotes(withNotes({
|
|
152
|
+
op: "leftover",
|
|
153
|
+
fragments
|
|
154
|
+
}))
|
|
155
|
+
};
|
|
156
|
+
const unit = units[nextIndex];
|
|
157
|
+
const leftoverNote = pending[0];
|
|
158
|
+
if (!unit && leftoverNote) return {
|
|
159
|
+
placed: [],
|
|
160
|
+
fragments: [],
|
|
161
|
+
index: nextIndex,
|
|
162
|
+
pending,
|
|
163
|
+
refusal: noteRefusal(leftoverNote.id)
|
|
164
|
+
};
|
|
165
|
+
if (!unit) return {
|
|
166
|
+
placed: [],
|
|
167
|
+
fragments: [],
|
|
168
|
+
index: nextIndex,
|
|
169
|
+
pending,
|
|
170
|
+
done: true
|
|
171
|
+
};
|
|
172
|
+
const forced = forcePlace(unit);
|
|
173
|
+
if (forced.refusal) return forced;
|
|
174
|
+
return {
|
|
175
|
+
placed: forced.placed,
|
|
176
|
+
fragments: forced.fragments,
|
|
177
|
+
index: nextIndex + 1,
|
|
178
|
+
pending: forced.pending
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return paginateSequence(input, fillPage);
|
|
182
|
+
}
|
|
183
|
+
const PAGINATE_FUNCTIONS = [
|
|
184
|
+
paginateColumns,
|
|
185
|
+
paginateNotes,
|
|
186
|
+
paginateKeeps,
|
|
187
|
+
paginateMarks,
|
|
188
|
+
paginateSequence,
|
|
189
|
+
paginateBook
|
|
190
|
+
];
|
|
191
|
+
function paginateScript() {
|
|
192
|
+
return `${PAGINATE_FUNCTIONS.map((fn) => fn.toString()).join("\n")}\nvar paginateBook=${paginateBook.name};`;
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
export { PAGINATE_FUNCTIONS, paginateBook, paginateScript };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PaginateUnit, PaginateWindow } from "./paginate-types.js";
|
|
2
|
+
//#region src/book/paginate-columns.d.ts
|
|
3
|
+
export declare function paginateColumns(placed: PaginateUnit[]): {
|
|
4
|
+
windows: PaginateWindow[];
|
|
5
|
+
height: number;
|
|
6
|
+
headerRepeat: number;
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//#region src/book/paginate-columns.ts
|
|
2
|
+
function paginateColumns(placed) {
|
|
3
|
+
function columnCount(block) {
|
|
4
|
+
return block.columns ?? 1;
|
|
5
|
+
}
|
|
6
|
+
function headerFor() {
|
|
7
|
+
const first = placed[0];
|
|
8
|
+
if (first?.block.kind !== "table" || first.index === 0) return 0;
|
|
9
|
+
return first.block.headerHeight ?? 0;
|
|
10
|
+
}
|
|
11
|
+
function sizes(count, columns) {
|
|
12
|
+
const base = Math.floor(count / columns);
|
|
13
|
+
const extra = count % columns;
|
|
14
|
+
return Array.from({ length: columns }, (_, index) => base + (index < extra ? 1 : 0));
|
|
15
|
+
}
|
|
16
|
+
function lineHeight(unit) {
|
|
17
|
+
return unit.bottom - unit.top;
|
|
18
|
+
}
|
|
19
|
+
function edgeStart(unit) {
|
|
20
|
+
return unit.prevBottom === void 0 ? unit.top : (unit.top + unit.prevBottom) / 2;
|
|
21
|
+
}
|
|
22
|
+
function edgeEnd(unit) {
|
|
23
|
+
return unit.nextTop === void 0 ? unit.bottom : (unit.bottom + unit.nextTop) / 2;
|
|
24
|
+
}
|
|
25
|
+
const headerRepeat = headerFor();
|
|
26
|
+
const windows = [];
|
|
27
|
+
let cursorBottom;
|
|
28
|
+
let cursorGalley;
|
|
29
|
+
let lowest = 0;
|
|
30
|
+
function beginY(index) {
|
|
31
|
+
const unit = placed[index];
|
|
32
|
+
if (!unit || cursorBottom === void 0 || cursorGalley === void 0) return 0;
|
|
33
|
+
return cursorBottom + (unit.top - cursorGalley);
|
|
34
|
+
}
|
|
35
|
+
function stack(from, to, startY) {
|
|
36
|
+
let y = startY;
|
|
37
|
+
let tallest = startY;
|
|
38
|
+
let lastBottom = startY;
|
|
39
|
+
for (let index = from; index < to; index += 1) {
|
|
40
|
+
const unit = placed[index];
|
|
41
|
+
const previous = index > from ? placed[index - 1] : void 0;
|
|
42
|
+
if (!unit) break;
|
|
43
|
+
if (previous) y += lineHeight(previous) + (unit.top - previous.bottom);
|
|
44
|
+
lastBottom = y + lineHeight(unit);
|
|
45
|
+
if (lastBottom > tallest) tallest = lastBottom;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
tallest,
|
|
49
|
+
lastBottom
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function emit(from, to, startY, column, columns) {
|
|
53
|
+
const first = placed[from];
|
|
54
|
+
const last = placed[to - 1];
|
|
55
|
+
if (!first || !last) return;
|
|
56
|
+
const start = edgeStart(first);
|
|
57
|
+
windows.push({
|
|
58
|
+
flow: first.flowId,
|
|
59
|
+
start,
|
|
60
|
+
end: edgeEnd(last),
|
|
61
|
+
top: startY - (first.top - start) + headerRepeat,
|
|
62
|
+
column,
|
|
63
|
+
columns
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function remember(bottom, last) {
|
|
67
|
+
cursorBottom = bottom;
|
|
68
|
+
if (last) cursorGalley = last.bottom;
|
|
69
|
+
}
|
|
70
|
+
function raise(height) {
|
|
71
|
+
if (height > lowest) lowest = height;
|
|
72
|
+
}
|
|
73
|
+
function layoutRun(from, to, columns) {
|
|
74
|
+
const startY = beginY(from);
|
|
75
|
+
if (columns <= 1) {
|
|
76
|
+
const laid = stack(from, to, startY);
|
|
77
|
+
emit(from, to, startY, 0, 1);
|
|
78
|
+
remember(laid.lastBottom, placed[to - 1]);
|
|
79
|
+
raise(laid.tallest);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const parts = sizes(to - from, columns);
|
|
83
|
+
let offset = 0;
|
|
84
|
+
let tallest = startY;
|
|
85
|
+
for (let column = 0; column < columns; column += 1) {
|
|
86
|
+
const size = parts[column] ?? 0;
|
|
87
|
+
const start = from + offset;
|
|
88
|
+
const end = start + size;
|
|
89
|
+
offset += size;
|
|
90
|
+
if (size === 0) continue;
|
|
91
|
+
const laid = stack(start, end, startY);
|
|
92
|
+
emit(start, end, startY, column, columns);
|
|
93
|
+
if (laid.tallest > tallest) tallest = laid.tallest;
|
|
94
|
+
}
|
|
95
|
+
remember(tallest, placed[to - 1]);
|
|
96
|
+
raise(tallest);
|
|
97
|
+
}
|
|
98
|
+
let index = 0;
|
|
99
|
+
while (index < placed.length) {
|
|
100
|
+
const unit = placed[index];
|
|
101
|
+
if (!unit) break;
|
|
102
|
+
const columns = columnCount(unit.block);
|
|
103
|
+
let end = index + 1;
|
|
104
|
+
while (end < placed.length) {
|
|
105
|
+
const next = placed[end];
|
|
106
|
+
if (!next || columnCount(next.block) !== columns) break;
|
|
107
|
+
end += 1;
|
|
108
|
+
}
|
|
109
|
+
layoutRun(index, end, columns);
|
|
110
|
+
index = end;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
windows,
|
|
114
|
+
height: headerRepeat + lowest,
|
|
115
|
+
headerRepeat
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { paginateColumns };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { PaginateUnit } from "./paginate-types.js";
|
|
2
|
+
//#region src/book/paginate-keeps.d.ts
|
|
3
|
+
export declare function paginateKeeps(placed: PaginateUnit[], next: PaginateUnit | undefined, textHeight: number, orphans: number, widows: number, allowEmpty?: boolean): PaginateUnit[];
|
|
4
|
+
//#endregion
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { paginateColumns } from "./paginate-columns.js";
|
|
2
|
+
//#region src/book/paginate-keeps.ts
|
|
3
|
+
function paginateKeeps(placed, next, textHeight, orphans, widows, allowEmpty) {
|
|
4
|
+
function sameBlock(left, right) {
|
|
5
|
+
return left.position.chapter === right.position.chapter && left.position.block === right.position.block;
|
|
6
|
+
}
|
|
7
|
+
function countOf(held, block) {
|
|
8
|
+
let count = 0;
|
|
9
|
+
for (const unit of held) if (sameBlock(unit.block, block)) count += 1;
|
|
10
|
+
return count;
|
|
11
|
+
}
|
|
12
|
+
function dropBlock(held, block) {
|
|
13
|
+
return held.filter((unit) => !sameBlock(unit.block, block));
|
|
14
|
+
}
|
|
15
|
+
function kept(dropped) {
|
|
16
|
+
if (dropped.length > 0) return dropped;
|
|
17
|
+
return allowEmpty ? dropped : void 0;
|
|
18
|
+
}
|
|
19
|
+
function layoutHeight(block) {
|
|
20
|
+
const units = [];
|
|
21
|
+
for (const [index, line] of block.lines.entries()) units.push({
|
|
22
|
+
flowId: "",
|
|
23
|
+
block,
|
|
24
|
+
index,
|
|
25
|
+
top: line.top,
|
|
26
|
+
bottom: line.bottom,
|
|
27
|
+
refs: []
|
|
28
|
+
});
|
|
29
|
+
return paginateColumns(units).height;
|
|
30
|
+
}
|
|
31
|
+
function keepWithNext(block) {
|
|
32
|
+
return block.kind === "heading" || Boolean(block.keepWithNext);
|
|
33
|
+
}
|
|
34
|
+
function keepTogether(block) {
|
|
35
|
+
if (!(block.kind === "figure" || block.kind === "table" || Boolean(block.keepTogether))) return false;
|
|
36
|
+
return layoutHeight(block) <= textHeight;
|
|
37
|
+
}
|
|
38
|
+
function orphansOf(block) {
|
|
39
|
+
return block.orphans ?? orphans;
|
|
40
|
+
}
|
|
41
|
+
function widowsOf(block) {
|
|
42
|
+
return block.widows ?? widows;
|
|
43
|
+
}
|
|
44
|
+
function rewindTogether(held, last) {
|
|
45
|
+
if (!keepTogether(last.block) || countOf(held, last.block) >= last.block.lines.length) return void 0;
|
|
46
|
+
return kept(dropBlock(held, last.block));
|
|
47
|
+
}
|
|
48
|
+
function rewindOrphans(held, last) {
|
|
49
|
+
const block = last.block;
|
|
50
|
+
if (block.kind === "table") return void 0;
|
|
51
|
+
const heldCount = countOf(held, block);
|
|
52
|
+
if (!held.some((unit) => sameBlock(unit.block, block) && unit.index === 0) || heldCount >= block.lines.length || heldCount >= orphansOf(block)) return void 0;
|
|
53
|
+
return kept(dropBlock(held, block));
|
|
54
|
+
}
|
|
55
|
+
function rewindWidows(held, last) {
|
|
56
|
+
const block = last.block;
|
|
57
|
+
if (block.kind === "table") return void 0;
|
|
58
|
+
const remaining = block.lines.length - last.index - 1;
|
|
59
|
+
const needWidows = widowsOf(block);
|
|
60
|
+
if (remaining <= 0 || remaining >= needWidows) return void 0;
|
|
61
|
+
const need = needWidows - remaining;
|
|
62
|
+
return kept(countOf(held, block) - need <= 0 ? dropBlock(held, block) : held.slice(0, held.length - need));
|
|
63
|
+
}
|
|
64
|
+
function rewindHeading(held, last, following) {
|
|
65
|
+
const other = !following || !sameBlock(following.block, last.block);
|
|
66
|
+
if (!keepWithNext(last.block) || countOf(held, last.block) !== last.block.lines.length || !other) return void 0;
|
|
67
|
+
return kept(dropBlock(held, last.block));
|
|
68
|
+
}
|
|
69
|
+
let result = placed;
|
|
70
|
+
for (let pass = 0; pass < 8; pass += 1) {
|
|
71
|
+
const last = result[result.length - 1];
|
|
72
|
+
if (!last) return result;
|
|
73
|
+
const nextResult = rewindTogether(result, last) ?? rewindOrphans(result, last) ?? rewindWidows(result, last) ?? rewindHeading(result, last, next);
|
|
74
|
+
if (!nextResult) return result;
|
|
75
|
+
result = nextResult;
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { paginateKeeps };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PaginatePageMarks, PaginateUnit } from "./paginate-types.js";
|
|
2
|
+
//#region src/book/paginate-marks.d.ts
|
|
3
|
+
export declare function paginateMarks(placed: PaginateUnit[], carried: PaginatePageMarks): {
|
|
4
|
+
marks: PaginatePageMarks;
|
|
5
|
+
carried: PaginatePageMarks;
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/book/paginate-marks.ts
|
|
2
|
+
function paginateMarks(placed, carried) {
|
|
3
|
+
const onPage = Object.create(null);
|
|
4
|
+
for (const unit of placed) for (const mark of unit.block.marks ?? []) {
|
|
5
|
+
if (mark.line !== unit.index) continue;
|
|
6
|
+
const current = onPage[mark.name];
|
|
7
|
+
if (!current) onPage[mark.name] = {
|
|
8
|
+
first: mark.value,
|
|
9
|
+
last: mark.value
|
|
10
|
+
};
|
|
11
|
+
else current.last = mark.value;
|
|
12
|
+
}
|
|
13
|
+
const marks = Object.assign(Object.create(null), onPage);
|
|
14
|
+
for (const name of Object.keys(carried)) {
|
|
15
|
+
if (marks[name] !== void 0) continue;
|
|
16
|
+
const last = carried[name]?.last;
|
|
17
|
+
if (last === void 0) continue;
|
|
18
|
+
marks[name] = {
|
|
19
|
+
first: last,
|
|
20
|
+
last
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const next = Object.assign(Object.create(null), carried);
|
|
24
|
+
for (const name of Object.keys(onPage)) {
|
|
25
|
+
const value = onPage[name];
|
|
26
|
+
if (value) next[name] = value;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
marks,
|
|
30
|
+
carried: next
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { paginateMarks };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PaginateNotesOp, PaginatePageNote } from "./paginate-types.js";
|
|
2
|
+
//#region src/book/paginate-notes.d.ts
|
|
3
|
+
export declare function paginateNotes(payload: Extract<PaginateNotesOp, {
|
|
4
|
+
op: 'space';
|
|
5
|
+
}>): number;
|
|
6
|
+
export declare function paginateNotes(payload: Extract<PaginateNotesOp, {
|
|
7
|
+
op: 'continue';
|
|
8
|
+
}>): {
|
|
9
|
+
fragments: PaginatePageNote[];
|
|
10
|
+
leftover: PaginatePageNote[];
|
|
11
|
+
};
|
|
12
|
+
export declare function paginateNotes(payload: Extract<PaginateNotesOp, {
|
|
13
|
+
op: 'allocate';
|
|
14
|
+
}>): PaginatePageNote[] | undefined;
|
|
15
|
+
export declare function paginateNotes(payload: Extract<PaginateNotesOp, {
|
|
16
|
+
op: 'recompute';
|
|
17
|
+
}>): PaginatePageNote[];
|
|
18
|
+
export declare function paginateNotes(payload: Extract<PaginateNotesOp, {
|
|
19
|
+
op: 'leftover';
|
|
20
|
+
}>): PaginatePageNote[];
|
|
21
|
+
//#endregion
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//#region src/book/paginate-notes.ts
|
|
2
|
+
function paginateNotes(payload) {
|
|
3
|
+
const notesById = new Map(payload.notes.map((note) => [note.id, note]));
|
|
4
|
+
const notesGap = payload.geometry.notesGap;
|
|
5
|
+
const textHeight = payload.geometry.textHeight;
|
|
6
|
+
function noteEnd(id) {
|
|
7
|
+
return notesById.get(id)?.lines.length ?? 0;
|
|
8
|
+
}
|
|
9
|
+
function fragmentHeight(fragment) {
|
|
10
|
+
const note = notesById.get(fragment.id);
|
|
11
|
+
const start = note?.lines[fragment.start];
|
|
12
|
+
const end = note?.lines[fragment.end - 1];
|
|
13
|
+
return start && end ? end.bottom - start.top : 0;
|
|
14
|
+
}
|
|
15
|
+
function notesSpace(fragments) {
|
|
16
|
+
if (fragments.length === 0) return 0;
|
|
17
|
+
let height = 0;
|
|
18
|
+
for (const fragment of fragments) height += fragmentHeight(fragment);
|
|
19
|
+
return notesGap + height;
|
|
20
|
+
}
|
|
21
|
+
function measure(placed, fragments) {
|
|
22
|
+
if (payload.op === "continue" || payload.op === "allocate" || payload.op === "recompute") return payload.used(placed, fragments);
|
|
23
|
+
return notesSpace(fragments);
|
|
24
|
+
}
|
|
25
|
+
function makeFragment(id, start, end) {
|
|
26
|
+
return {
|
|
27
|
+
id,
|
|
28
|
+
whole: start === 0 && end === noteEnd(id),
|
|
29
|
+
start,
|
|
30
|
+
end
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function leftoverFrom(fragments) {
|
|
34
|
+
return fragments.flatMap((fragment) => fragment.end < noteEnd(fragment.id) ? [makeFragment(fragment.id, fragment.end, noteEnd(fragment.id))] : []);
|
|
35
|
+
}
|
|
36
|
+
function takeContinued(continued) {
|
|
37
|
+
if (continued.length === 0) return {
|
|
38
|
+
fragments: [],
|
|
39
|
+
leftover: []
|
|
40
|
+
};
|
|
41
|
+
const fragments = [];
|
|
42
|
+
const leftover = [];
|
|
43
|
+
let pending = true;
|
|
44
|
+
for (const item of continued) {
|
|
45
|
+
const total = noteEnd(item.id);
|
|
46
|
+
if (!pending) {
|
|
47
|
+
leftover.push(item);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const whole = makeFragment(item.id, item.start, total);
|
|
51
|
+
if (measure([], [...fragments, whole]) <= textHeight) {
|
|
52
|
+
fragments.push(whole);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
let end = item.start;
|
|
56
|
+
while (end < total && measure([], [...fragments, makeFragment(item.id, item.start, end + 1)]) <= textHeight) end += 1;
|
|
57
|
+
if (end > item.start) fragments.push(makeFragment(item.id, item.start, end));
|
|
58
|
+
if (end < total) leftover.push(makeFragment(item.id, end, total));
|
|
59
|
+
pending = false;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
fragments,
|
|
63
|
+
leftover
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function fitNote(trial, fragments, id) {
|
|
67
|
+
const total = noteEnd(id);
|
|
68
|
+
if (total === 0) return void 0;
|
|
69
|
+
const whole = makeFragment(id, 0, total);
|
|
70
|
+
if (measure(trial, [...fragments, whole]) <= textHeight) return whole;
|
|
71
|
+
const minimum = Math.min(2, total);
|
|
72
|
+
if (measure(trial, [...fragments, makeFragment(id, 0, minimum)]) > textHeight) return void 0;
|
|
73
|
+
let end = minimum;
|
|
74
|
+
while (end < total && measure(trial, [...fragments, makeFragment(id, 0, end + 1)]) <= textHeight) end += 1;
|
|
75
|
+
return makeFragment(id, 0, end);
|
|
76
|
+
}
|
|
77
|
+
function allocateNotes(placed, existing, unit) {
|
|
78
|
+
const trial = [...placed, unit];
|
|
79
|
+
if (measure(trial, existing) > textHeight) return void 0;
|
|
80
|
+
const fragments = existing.slice();
|
|
81
|
+
for (const id of unit.refs) {
|
|
82
|
+
if (fragments.some((fragment) => fragment.id === id)) continue;
|
|
83
|
+
const fragment = fitNote(trial, fragments, id);
|
|
84
|
+
if (!fragment) return void 0;
|
|
85
|
+
fragments.push(fragment);
|
|
86
|
+
}
|
|
87
|
+
return fragments;
|
|
88
|
+
}
|
|
89
|
+
function notesFor(placed, seed) {
|
|
90
|
+
let fragments = seed;
|
|
91
|
+
for (const [offset, unit] of placed.entries()) {
|
|
92
|
+
const allocated = allocateNotes(placed.slice(0, offset), fragments, unit);
|
|
93
|
+
if (!allocated) break;
|
|
94
|
+
fragments = allocated;
|
|
95
|
+
}
|
|
96
|
+
return fragments;
|
|
97
|
+
}
|
|
98
|
+
if (payload.op === "space") return notesSpace(payload.fragments);
|
|
99
|
+
if (payload.op === "continue") return takeContinued(payload.pending);
|
|
100
|
+
if (payload.op === "allocate") return allocateNotes(payload.placed, payload.fragments, payload.unit);
|
|
101
|
+
if (payload.op === "recompute") return notesFor(payload.placed, payload.seed);
|
|
102
|
+
return leftoverFrom(payload.fragments);
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
export { paginateNotes };
|