postext-epub 0.1.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/LICENSE +21 -0
- package/README.md +194 -0
- package/dist/__tests__/sampleBook.d.ts +17 -0
- package/dist/__tests__/sampleBook.d.ts.map +1 -0
- package/dist/__tests__/sampleBook.js +103 -0
- package/dist/__tests__/sampleBook.js.map +1 -0
- package/dist/epubcheck.test.d.ts +2 -0
- package/dist/epubcheck.test.d.ts.map +1 -0
- package/dist/epubcheck.test.js +69 -0
- package/dist/epubcheck.test.js.map +1 -0
- package/dist/fixed/fixed.test.d.ts +2 -0
- package/dist/fixed/fixed.test.d.ts.map +1 -0
- package/dist/fixed/fixed.test.js +225 -0
- package/dist/fixed/fixed.test.js.map +1 -0
- package/dist/fixed/fontUse.d.ts +21 -0
- package/dist/fixed/fontUse.d.ts.map +1 -0
- package/dist/fixed/fontUse.js +82 -0
- package/dist/fixed/fontUse.js.map +1 -0
- package/dist/fixed/index.d.ts +3 -0
- package/dist/fixed/index.d.ts.map +1 -0
- package/dist/fixed/index.js +254 -0
- package/dist/fixed/index.js.map +1 -0
- package/dist/fixed/outline.d.ts +40 -0
- package/dist/fixed/outline.d.ts.map +1 -0
- package/dist/fixed/outline.js +88 -0
- package/dist/fixed/outline.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/package/pack.d.ts +25 -0
- package/dist/package/pack.d.ts.map +1 -0
- package/dist/package/pack.js +253 -0
- package/dist/package/pack.js.map +1 -0
- package/dist/package/pack.test.d.ts +2 -0
- package/dist/package/pack.test.d.ts.map +1 -0
- package/dist/package/pack.test.js +164 -0
- package/dist/package/pack.test.js.map +1 -0
- package/dist/package/read.d.ts +15 -0
- package/dist/package/read.d.ts.map +1 -0
- package/dist/package/read.js +198 -0
- package/dist/package/read.js.map +1 -0
- package/dist/package/strings.d.ts +17 -0
- package/dist/package/strings.d.ts.map +1 -0
- package/dist/package/strings.js +24 -0
- package/dist/package/strings.js.map +1 -0
- package/dist/reflowable/__tests__/vdt.d.ts +13 -0
- package/dist/reflowable/__tests__/vdt.d.ts.map +1 -0
- package/dist/reflowable/__tests__/vdt.js +55 -0
- package/dist/reflowable/__tests__/vdt.js.map +1 -0
- package/dist/reflowable/__tests__/xml.d.ts +8 -0
- package/dist/reflowable/__tests__/xml.d.ts.map +1 -0
- package/dist/reflowable/__tests__/xml.js +107 -0
- package/dist/reflowable/__tests__/xml.js.map +1 -0
- package/dist/reflowable/css.d.ts +45 -0
- package/dist/reflowable/css.d.ts.map +1 -0
- package/dist/reflowable/css.js +491 -0
- package/dist/reflowable/css.js.map +1 -0
- package/dist/reflowable/index.d.ts +4 -0
- package/dist/reflowable/index.d.ts.map +1 -0
- package/dist/reflowable/index.js +257 -0
- package/dist/reflowable/index.js.map +1 -0
- package/dist/reflowable/inline.d.ts +56 -0
- package/dist/reflowable/inline.d.ts.map +1 -0
- package/dist/reflowable/inline.js +334 -0
- package/dist/reflowable/inline.js.map +1 -0
- package/dist/reflowable/model.d.ts +261 -0
- package/dist/reflowable/model.d.ts.map +1 -0
- package/dist/reflowable/model.js +6 -0
- package/dist/reflowable/model.js.map +1 -0
- package/dist/reflowable/reflowable.test.d.ts +2 -0
- package/dist/reflowable/reflowable.test.d.ts.map +1 -0
- package/dist/reflowable/reflowable.test.js +469 -0
- package/dist/reflowable/reflowable.test.js.map +1 -0
- package/dist/reflowable/rtl.test.d.ts +2 -0
- package/dist/reflowable/rtl.test.d.ts.map +1 -0
- package/dist/reflowable/rtl.test.js +217 -0
- package/dist/reflowable/rtl.test.js.map +1 -0
- package/dist/reflowable/walk.d.ts +42 -0
- package/dist/reflowable/walk.d.ts.map +1 -0
- package/dist/reflowable/walk.js +1096 -0
- package/dist/reflowable/walk.js.map +1 -0
- package/dist/reflowable/xhtml.d.ts +33 -0
- package/dist/reflowable/xhtml.d.ts.map +1 -0
- package/dist/reflowable/xhtml.js +472 -0
- package/dist/reflowable/xhtml.js.map +1 -0
- package/dist/shared/accessibility.d.ts +20 -0
- package/dist/shared/accessibility.d.ts.map +1 -0
- package/dist/shared/accessibility.js +30 -0
- package/dist/shared/accessibility.js.map +1 -0
- package/dist/shared/assets.d.ts +46 -0
- package/dist/shared/assets.d.ts.map +1 -0
- package/dist/shared/assets.js +143 -0
- package/dist/shared/assets.js.map +1 -0
- package/dist/shared/assets.test.d.ts +2 -0
- package/dist/shared/assets.test.d.ts.map +1 -0
- package/dist/shared/assets.test.js +106 -0
- package/dist/shared/assets.test.js.map +1 -0
- package/dist/shared/media.d.ts +11 -0
- package/dist/shared/media.d.ts.map +1 -0
- package/dist/shared/media.js +52 -0
- package/dist/shared/media.js.map +1 -0
- package/dist/shared/uuid.d.ts +7 -0
- package/dist/shared/uuid.d.ts.map +1 -0
- package/dist/shared/uuid.js +63 -0
- package/dist/shared/uuid.js.map +1 -0
- package/dist/shared/xml.d.ts +54 -0
- package/dist/shared/xml.d.ts.map +1 -0
- package/dist/shared/xml.js +214 -0
- package/dist/shared/xml.js.map +1 -0
- package/dist/shared/xml.test.d.ts +2 -0
- package/dist/shared/xml.test.d.ts.map +1 -0
- package/dist/shared/xml.test.js +71 -0
- package/dist/shared/xml.test.js.map +1 -0
- package/dist/shared/xmlRead.d.ts +19 -0
- package/dist/shared/xmlRead.d.ts.map +1 -0
- package/dist/shared/xmlRead.js +85 -0
- package/dist/shared/xmlRead.js.map +1 -0
- package/dist/types.d.ts +205 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/worker/client.d.ts +39 -0
- package/dist/worker/client.d.ts.map +1 -0
- package/dist/worker/client.js +184 -0
- package/dist/worker/client.js.map +1 -0
- package/dist/worker/epub.worker.d.ts +2 -0
- package/dist/worker/epub.worker.d.ts.map +1 -0
- package/dist/worker/epub.worker.js +7 -0
- package/dist/worker/epub.worker.js.map +1 -0
- package/dist/worker/protocol.d.ts +44 -0
- package/dist/worker/protocol.d.ts.map +1 -0
- package/dist/worker/protocol.js +6 -0
- package/dist/worker/protocol.js.map +1 -0
- package/dist/worker/serve.d.ts +11 -0
- package/dist/worker/serve.d.ts.map +1 -0
- package/dist/worker/serve.js +53 -0
- package/dist/worker/serve.js.map +1 -0
- package/dist/worker/worker.test.d.ts +2 -0
- package/dist/worker/worker.test.d.ts.map +1 -0
- package/dist/worker/worker.test.js +168 -0
- package/dist/worker/worker.test.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
// The resolved print documents back to a semantic book, in reading order.
|
|
2
|
+
//
|
|
3
|
+
// Pages are read as the tagged PDF reads them (postext-pdf
|
|
4
|
+
// `structureFlow.ts`): column by column, then the page's floats, each moved
|
|
5
|
+
// to where it is read — after the text block with the highest content
|
|
6
|
+
// index up to its own. On the way:
|
|
7
|
+
// - fragments of a block split across columns or pages (continuations
|
|
8
|
+
// carry the head's id plus `-cont-N`) are joined into one paragraph;
|
|
9
|
+
// - consecutive list items become nested lists by `listDepth`;
|
|
10
|
+
// - blocks sharing a callout's `containerId` go into one box, nested
|
|
11
|
+
// boxes by `calloutPath`; other containers (`:::paragraphs`) only lend
|
|
12
|
+
// their paragraphs a style class;
|
|
13
|
+
// - headings take levels that never skip one;
|
|
14
|
+
// - footnote paragraphs (`footnoteNote`) leave the flow and become the
|
|
15
|
+
// notes of the content document that cites them;
|
|
16
|
+
// - each printed page leaves a page-break marker where its text starts;
|
|
17
|
+
// - a part (a divider page, or `partMarks` without one) opens a
|
|
18
|
+
// document of its own and the chapter goes on in a new one.
|
|
19
|
+
import { canonicalLocaleTag, dimensionToPx, primaryFontFamily } from 'postext';
|
|
20
|
+
import { appendLine, appendLines, fontPx, idOf, mathSvg, plainText } from './inline';
|
|
21
|
+
const fragmentKey = (block) => block.id.replace(/-cont-\d+$/, '');
|
|
22
|
+
const isTocRow = (block) => block.tocEntry !== undefined || block.tocPart !== undefined;
|
|
23
|
+
/** The source range of a block, as a key. */
|
|
24
|
+
const rangeKey = (block) => `${block.sourceStart ?? ''}:${block.sourceEnd ?? ''}`;
|
|
25
|
+
/** Whether a block links page numbers: an entry of the index. */
|
|
26
|
+
const hasPageLink = (block) => block.lines.some((l) => l.segments?.some((g) => g.pageLink !== undefined));
|
|
27
|
+
const lineKey = (page, x, y) => `${page}|${x.toFixed(2)}|${y.toFixed(2)}`;
|
|
28
|
+
/** The language a chapter document is written in. */
|
|
29
|
+
export function docLanguage(config, fallback) {
|
|
30
|
+
const h = config.bodyText.hyphenation;
|
|
31
|
+
return canonicalLocaleTag(config.locale) ?? canonicalLocaleTag(h.tag) ?? canonicalLocaleTag(fallback) ?? 'en';
|
|
32
|
+
}
|
|
33
|
+
export function walkBook(docs, options) {
|
|
34
|
+
const book = {
|
|
35
|
+
files: [],
|
|
36
|
+
headings: [],
|
|
37
|
+
anchors: new Map(),
|
|
38
|
+
resources: new Map(),
|
|
39
|
+
pages: new Map(),
|
|
40
|
+
notes: new Map(),
|
|
41
|
+
images: new Set(),
|
|
42
|
+
altText: false,
|
|
43
|
+
missingAlt: false,
|
|
44
|
+
maths: false,
|
|
45
|
+
verse: false,
|
|
46
|
+
};
|
|
47
|
+
const counters = { heading: 0, block: 0 };
|
|
48
|
+
docs.forEach((doc, i) => new DocWalker(book, doc, i, options, counters).walk());
|
|
49
|
+
return book;
|
|
50
|
+
}
|
|
51
|
+
class DocWalker {
|
|
52
|
+
book;
|
|
53
|
+
doc;
|
|
54
|
+
index;
|
|
55
|
+
counters;
|
|
56
|
+
offset;
|
|
57
|
+
lang;
|
|
58
|
+
dir;
|
|
59
|
+
config;
|
|
60
|
+
bodyPx;
|
|
61
|
+
/** Ids of the boxes (`:::callout`) of the document. */
|
|
62
|
+
calloutIds = new Set();
|
|
63
|
+
/** Inline anchors by the line they sit on. */
|
|
64
|
+
lineAnchors = new Map();
|
|
65
|
+
/** Source ranges of the `:::index` directives: every entry block a
|
|
66
|
+
* directive expands into carries its range, the entries with only a
|
|
67
|
+
* cross-reference (`See …`) too, which have no page link. */
|
|
68
|
+
indexRanges = new Set();
|
|
69
|
+
partMarks;
|
|
70
|
+
/** Whether the engine named the paragraph styles of the blocks
|
|
71
|
+
* (`VDTBlock.paragraphStyleId`, `VDTBlock.indexLevel`): an older
|
|
72
|
+
* document's are told from their faces (`paragraphStyleOf`). */
|
|
73
|
+
styleHints;
|
|
74
|
+
file;
|
|
75
|
+
root;
|
|
76
|
+
lastLevel = new Map();
|
|
77
|
+
lastContainer;
|
|
78
|
+
callouts = new Map();
|
|
79
|
+
sinks = new Map();
|
|
80
|
+
verses = new Map();
|
|
81
|
+
tables = new Map();
|
|
82
|
+
figures = new Set();
|
|
83
|
+
/** Text blocks read so far, with their content index: where floats go. */
|
|
84
|
+
flow = [];
|
|
85
|
+
placedAfter = new Map();
|
|
86
|
+
notes = new Map();
|
|
87
|
+
seenNotes = new Set();
|
|
88
|
+
pendingPages = [];
|
|
89
|
+
floating = false;
|
|
90
|
+
partKey;
|
|
91
|
+
/** The palette overrides of the part in force. */
|
|
92
|
+
palette;
|
|
93
|
+
fileCount = 0;
|
|
94
|
+
constructor(book, doc, index, options, counters) {
|
|
95
|
+
this.book = book;
|
|
96
|
+
this.doc = doc;
|
|
97
|
+
this.index = index;
|
|
98
|
+
this.counters = counters;
|
|
99
|
+
this.offset = doc.pageIndexOffset ?? 0;
|
|
100
|
+
this.config = doc.config;
|
|
101
|
+
this.lang = docLanguage(doc.config, options.language);
|
|
102
|
+
if (doc.config.direction === 'rtl')
|
|
103
|
+
this.dir = 'rtl';
|
|
104
|
+
this.bodyPx = dimensionToPx(doc.config.bodyText.fontSize, doc.config.page.dpi);
|
|
105
|
+
this.partMarks = [...(doc.partMarks ?? [])].sort((a, b) => a.afterContentIndex - b.afterContentIndex);
|
|
106
|
+
this.palette = doc.partStart?.palette;
|
|
107
|
+
const frames = [...doc.blocks, ...doc.pages.flatMap((p) => p.floats ?? [])].filter((b) => b.type === 'callout');
|
|
108
|
+
for (const f of frames) {
|
|
109
|
+
if (f.containerId !== undefined)
|
|
110
|
+
this.calloutIds.add(f.containerId);
|
|
111
|
+
for (const id of f.calloutPath ?? [])
|
|
112
|
+
this.calloutIds.add(id);
|
|
113
|
+
}
|
|
114
|
+
for (const b of doc.blocks) {
|
|
115
|
+
if (b.sourceStart !== undefined && hasPageLink(b))
|
|
116
|
+
this.indexRanges.add(rangeKey(b));
|
|
117
|
+
}
|
|
118
|
+
this.styleHints = [...doc.blocks, ...doc.pages.flatMap((p) => p.floats ?? [])].some((b) => b.paragraphStyleId !== undefined || b.indexLevel !== undefined);
|
|
119
|
+
for (const a of doc.anchors ?? []) {
|
|
120
|
+
if (a.kind !== 'anchor')
|
|
121
|
+
continue;
|
|
122
|
+
const key = lineKey(a.pageIndex, a.x, a.y);
|
|
123
|
+
const ids = this.lineAnchors.get(key) ?? [];
|
|
124
|
+
ids.push(a.id);
|
|
125
|
+
this.lineAnchors.set(key, ids);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
walk() {
|
|
129
|
+
const floats = [];
|
|
130
|
+
for (const page of this.doc.pages) {
|
|
131
|
+
const bookIndex = this.offset + page.index;
|
|
132
|
+
if (page.partInfo) {
|
|
133
|
+
const key = `${page.partInfo.number}\u0000${page.partInfo.title}`;
|
|
134
|
+
if (key !== this.partKey)
|
|
135
|
+
this.openPart(page.partInfo.number, page.partInfo.title, bookIndex, page.partInfo.palette);
|
|
136
|
+
}
|
|
137
|
+
else if (this.file?.kind === 'part') {
|
|
138
|
+
this.newFile('chapter');
|
|
139
|
+
}
|
|
140
|
+
if (!this.file)
|
|
141
|
+
this.newFile('chapter');
|
|
142
|
+
this.book.pages.set(bookIndex, { loc: { file: this.file, id: '' }, label: page.pageLabel || String(page.pageNumberValue) });
|
|
143
|
+
this.pendingPages.push(bookIndex);
|
|
144
|
+
for (const col of page.columns) {
|
|
145
|
+
for (const block of col.blocks)
|
|
146
|
+
this.place(block, this.root);
|
|
147
|
+
}
|
|
148
|
+
// Floats: footnotes are collected now; the rest is read later, in
|
|
149
|
+
// groups (a floated box with its content).
|
|
150
|
+
let group = [];
|
|
151
|
+
const flush = () => {
|
|
152
|
+
if (group.length > 0)
|
|
153
|
+
floats.push(group);
|
|
154
|
+
group = [];
|
|
155
|
+
};
|
|
156
|
+
for (const f of [...(page.floats ?? []), ...page.marginNotes]) {
|
|
157
|
+
if (f.footnoteNote !== undefined) {
|
|
158
|
+
this.note(f);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (group.length > 0 && (f.containerId === undefined || f.containerId !== group[0].containerId))
|
|
162
|
+
flush();
|
|
163
|
+
group.push(f);
|
|
164
|
+
if (f.containerId === undefined)
|
|
165
|
+
flush();
|
|
166
|
+
}
|
|
167
|
+
flush();
|
|
168
|
+
}
|
|
169
|
+
for (const g of floats)
|
|
170
|
+
this.readFloat(g);
|
|
171
|
+
// Page starts with no text after them: at the end of the last file.
|
|
172
|
+
this.flushPages();
|
|
173
|
+
this.finishNotes();
|
|
174
|
+
this.markPullQuotes();
|
|
175
|
+
// Anchors no line holds: at the start of their page.
|
|
176
|
+
for (const a of this.doc.anchors ?? []) {
|
|
177
|
+
if (this.book.anchors.has(a.id))
|
|
178
|
+
continue;
|
|
179
|
+
const page = this.book.pages.get(this.offset + a.pageIndex);
|
|
180
|
+
if (page)
|
|
181
|
+
this.book.anchors.set(a.id, page.loc);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// --- files ---------------------------------------------------------------
|
|
185
|
+
newFile(kind) {
|
|
186
|
+
const file = {
|
|
187
|
+
href: '',
|
|
188
|
+
itemId: '',
|
|
189
|
+
kind,
|
|
190
|
+
doc: this.index,
|
|
191
|
+
nodes: [],
|
|
192
|
+
notes: [],
|
|
193
|
+
lang: this.lang,
|
|
194
|
+
...(this.dir ? { dir: this.dir } : {}),
|
|
195
|
+
...(this.palette && Object.keys(this.palette).length > 0 ? { palette: this.palette } : {}),
|
|
196
|
+
};
|
|
197
|
+
this.fileCount++;
|
|
198
|
+
this.book.files.push(file);
|
|
199
|
+
this.file = file;
|
|
200
|
+
this.root = { nodes: file.nodes, lists: [] };
|
|
201
|
+
this.lastContainer = undefined;
|
|
202
|
+
this.callouts = new Map();
|
|
203
|
+
return file;
|
|
204
|
+
}
|
|
205
|
+
/** A part opener: a document of its own with the part's title. A file
|
|
206
|
+
* holding nothing but page starts becomes the opener. */
|
|
207
|
+
openPart(number, title, bookIndex, palette) {
|
|
208
|
+
this.partKey = `${number}\u0000${title}`;
|
|
209
|
+
this.palette = palette;
|
|
210
|
+
const reuse = this.file && this.file.nodes.every((n) => n.k === 'marker');
|
|
211
|
+
const file = reuse ? this.file : this.newFile('part');
|
|
212
|
+
file.kind = 'part';
|
|
213
|
+
if (palette && Object.keys(palette).length > 0)
|
|
214
|
+
file.palette = palette;
|
|
215
|
+
else
|
|
216
|
+
delete file.palette;
|
|
217
|
+
const id = `part-${++this.counters.heading}`;
|
|
218
|
+
const label = [number, title].filter((s) => s.trim()).join(' ');
|
|
219
|
+
const inl = [...this.takePages()];
|
|
220
|
+
if (number.trim())
|
|
221
|
+
inl.push({ t: 'raw', xhtml: `<span class="pt-part-number">${escapeText(number)}</span>` }, { t: 'text', text: ' ', fmt: {} });
|
|
222
|
+
inl.push({ t: 'text', text: title, fmt: {} });
|
|
223
|
+
const node = { k: 'h', level: 1, id, inl, cls: ['pt-part-title'] };
|
|
224
|
+
file.nodes.push(node);
|
|
225
|
+
file.title ??= label;
|
|
226
|
+
this.lastLevel.set(file, 1);
|
|
227
|
+
this.book.headings.push({ file, id, level: 0, label, page: bookIndex, listed: true });
|
|
228
|
+
}
|
|
229
|
+
// --- page starts ---------------------------------------------------------
|
|
230
|
+
/** The page starts waiting for text, as items (none while a float is
|
|
231
|
+
* read: a float is no page's start). Registers where they landed. */
|
|
232
|
+
takePages() {
|
|
233
|
+
if (this.floating || this.pendingPages.length === 0)
|
|
234
|
+
return [];
|
|
235
|
+
const out = [];
|
|
236
|
+
for (const bookIndex of this.pendingPages) {
|
|
237
|
+
const page = this.book.pages.get(bookIndex);
|
|
238
|
+
page.loc = { file: this.file, id: `page-${bookIndex + 1}` };
|
|
239
|
+
out.push({ t: 'page', bookIndex });
|
|
240
|
+
}
|
|
241
|
+
this.pendingPages = [];
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
flushPages() {
|
|
245
|
+
const items = this.takePages();
|
|
246
|
+
if (items.length > 0)
|
|
247
|
+
this.file.nodes.push({ k: 'marker', inl: items });
|
|
248
|
+
}
|
|
249
|
+
// --- containers ----------------------------------------------------------
|
|
250
|
+
calloutState(cid, path, root) {
|
|
251
|
+
let entry = this.callouts.get(cid);
|
|
252
|
+
if (!entry) {
|
|
253
|
+
const node = { k: 'callout', children: [] };
|
|
254
|
+
root.nodes.push(node);
|
|
255
|
+
entry = { node, state: { nodes: node.children, lists: [], top: root.top ?? node } };
|
|
256
|
+
this.callouts.set(cid, entry);
|
|
257
|
+
}
|
|
258
|
+
for (const id of path) {
|
|
259
|
+
let inner = this.callouts.get(id);
|
|
260
|
+
if (!inner) {
|
|
261
|
+
const node = { k: 'callout', children: [] };
|
|
262
|
+
entry.state.nodes.push(node);
|
|
263
|
+
inner = { node, state: { nodes: node.children, lists: [], top: entry.state.top } };
|
|
264
|
+
this.callouts.set(id, inner);
|
|
265
|
+
}
|
|
266
|
+
entry = inner;
|
|
267
|
+
}
|
|
268
|
+
return entry.state;
|
|
269
|
+
}
|
|
270
|
+
/** The container a block goes into, its grouping state brought in step
|
|
271
|
+
* (lists, quotation and contents close on any other block). */
|
|
272
|
+
enter(block, root) {
|
|
273
|
+
const cid = block.containerId;
|
|
274
|
+
const state = cid !== undefined && this.calloutIds.has(cid) ? this.calloutState(cid, block.calloutPath ?? [], root) : root;
|
|
275
|
+
if (state !== this.lastContainer || block.type === 'callout') {
|
|
276
|
+
state.lists = [];
|
|
277
|
+
state.quote = undefined;
|
|
278
|
+
state.toc = undefined;
|
|
279
|
+
}
|
|
280
|
+
this.lastContainer = state;
|
|
281
|
+
const toc = isTocRow(block);
|
|
282
|
+
if (block.type !== 'listItem' || toc)
|
|
283
|
+
state.lists = [];
|
|
284
|
+
if (block.type !== 'blockquote')
|
|
285
|
+
state.quote = undefined;
|
|
286
|
+
if (!toc)
|
|
287
|
+
state.toc = undefined;
|
|
288
|
+
return state;
|
|
289
|
+
}
|
|
290
|
+
topOf(state, node) {
|
|
291
|
+
return state.top ?? node;
|
|
292
|
+
}
|
|
293
|
+
record(block, top) {
|
|
294
|
+
if (this.floating || block.contentIndex === undefined)
|
|
295
|
+
return;
|
|
296
|
+
this.flow.push({ index: block.contentIndex, top, file: this.file });
|
|
297
|
+
}
|
|
298
|
+
// --- blocks --------------------------------------------------------------
|
|
299
|
+
ctx(block) {
|
|
300
|
+
return {
|
|
301
|
+
doc: this.index,
|
|
302
|
+
lang: this.lang,
|
|
303
|
+
basePx: fontPx(block.fontString),
|
|
304
|
+
noteRef: (id) => {
|
|
305
|
+
const key = `${this.index}:${id}`;
|
|
306
|
+
if (!this.book.notes.has(key))
|
|
307
|
+
this.book.notes.set(key, { file: this.file, id: idOf('fn-', id) });
|
|
308
|
+
if (this.seenNotes.has(id))
|
|
309
|
+
return false;
|
|
310
|
+
this.seenNotes.add(id);
|
|
311
|
+
return true;
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** Items to set before a line: the anchors the line holds. */
|
|
316
|
+
anchorsAt(block, line) {
|
|
317
|
+
const ids = this.lineAnchors.get(lineKey(block.pageIndex, line.bbox.x, line.bbox.y));
|
|
318
|
+
if (!ids)
|
|
319
|
+
return [];
|
|
320
|
+
const out = [];
|
|
321
|
+
for (const id of ids) {
|
|
322
|
+
if (this.book.anchors.has(id))
|
|
323
|
+
continue;
|
|
324
|
+
const elemId = idOf('a-', id);
|
|
325
|
+
this.book.anchors.set(id, { file: this.file, id: elemId });
|
|
326
|
+
out.push({ t: 'anchor', id: elemId });
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
appendBlockLines(sink, block, lines = block.lines, before = []) {
|
|
331
|
+
const ctx = this.ctx(block);
|
|
332
|
+
appendLines(sink, lines, ctx, (line, i) => [...(i === 0 ? before : []), ...this.anchorsAt(block, line)]);
|
|
333
|
+
}
|
|
334
|
+
place(block, root) {
|
|
335
|
+
if (block.footnoteNote !== undefined) {
|
|
336
|
+
this.note(block);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// A part set without a divider page opens with the first block after
|
|
340
|
+
// its fence.
|
|
341
|
+
if (!this.floating && block.contentIndex !== undefined) {
|
|
342
|
+
while (this.partMarks.length > 0 && block.contentIndex > this.partMarks[0].afterContentIndex) {
|
|
343
|
+
const mark = this.partMarks.shift();
|
|
344
|
+
this.openPart(mark.number, mark.title, this.offset + block.pageIndex, mark.palette);
|
|
345
|
+
this.newFile('chapter');
|
|
346
|
+
root = this.root;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (block.type === 'callout') {
|
|
350
|
+
this.calloutFrame(block, root);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const key = this.textKey(block);
|
|
354
|
+
const verse = this.verses.get(key);
|
|
355
|
+
if (verse) {
|
|
356
|
+
this.enter(block, root);
|
|
357
|
+
this.verseLines(verse, block);
|
|
358
|
+
this.record(block, verse.top);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const sink = this.sinks.get(key);
|
|
362
|
+
if (sink && block.indexLevel !== undefined && block.lines.some((l) => l.indexLevel !== undefined)) {
|
|
363
|
+
// An index block's fragment that starts entries of its own.
|
|
364
|
+
this.indexEntries(block, this.enter(block, root), key, sink);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (sink) {
|
|
368
|
+
this.enter(block, root);
|
|
369
|
+
const pages = this.takePages();
|
|
370
|
+
this.appendBlockLines(sink, block, block.lines, pages);
|
|
371
|
+
this.record(block, sink.top);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (block.type === 'resource') {
|
|
375
|
+
this.resource(block, root);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (block.type === 'mathDisplay') {
|
|
379
|
+
this.math(block, root);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (isTocRow(block)) {
|
|
383
|
+
this.tocRow(block, root, key);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const hasText = block.lines.some((l) => l.text.length > 0 || (l.segments?.length ?? 0) > 0);
|
|
387
|
+
if (!hasText && !(block.type === 'heading' && block.designOverlay))
|
|
388
|
+
return;
|
|
389
|
+
switch (block.type) {
|
|
390
|
+
case 'heading':
|
|
391
|
+
this.heading(block, root, key);
|
|
392
|
+
break;
|
|
393
|
+
case 'listItem':
|
|
394
|
+
this.listItem(block, root, key);
|
|
395
|
+
break;
|
|
396
|
+
case 'blockquote':
|
|
397
|
+
this.quote(block, root, key);
|
|
398
|
+
break;
|
|
399
|
+
default:
|
|
400
|
+
this.paragraph(block, root, key);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/** The key the fragments of one text block share: the head's id (a
|
|
404
|
+
* continuation adds `-cont-N`); inside a box split across pages, where
|
|
405
|
+
* each fragment numbers its children afresh, the box and the content
|
|
406
|
+
* index. */
|
|
407
|
+
textKey(block) {
|
|
408
|
+
const cid = block.containerId;
|
|
409
|
+
if (cid !== undefined && this.calloutIds.has(cid) && block.contentIndex !== undefined && block.type !== 'heading' && block.type !== 'resource') {
|
|
410
|
+
return `box:${cid}:${(block.calloutPath ?? []).join('.')}:${block.contentIndex}`;
|
|
411
|
+
}
|
|
412
|
+
return fragmentKey(block);
|
|
413
|
+
}
|
|
414
|
+
newSink(key, inl, top) {
|
|
415
|
+
const sink = { inl, top, file: this.file };
|
|
416
|
+
this.sinks.set(key, sink);
|
|
417
|
+
return sink;
|
|
418
|
+
}
|
|
419
|
+
calloutFrame(block, root) {
|
|
420
|
+
const cid = block.containerId;
|
|
421
|
+
if (cid === undefined)
|
|
422
|
+
return;
|
|
423
|
+
this.enter(block, root);
|
|
424
|
+
const path = block.calloutPath ?? [];
|
|
425
|
+
this.calloutState(cid, path, root);
|
|
426
|
+
const own = this.callouts.get(path.length > 0 ? path[path.length - 1] : cid).node;
|
|
427
|
+
if (block.callout?.styleId)
|
|
428
|
+
own.styleId = block.callout.styleId;
|
|
429
|
+
if ((block.callout?.part ?? 0) === 0 && own.title === undefined) {
|
|
430
|
+
const title = (block.designOverlay?.blocks ?? [])
|
|
431
|
+
.flatMap((b) => (b.kind === 'text' && !b.artifact ? [b.lines.map((l) => l.text).join(' ')] : []))
|
|
432
|
+
.join(' ')
|
|
433
|
+
.replace(/\s+/g, ' ')
|
|
434
|
+
.trim();
|
|
435
|
+
if (title)
|
|
436
|
+
own.title = title;
|
|
437
|
+
}
|
|
438
|
+
// The box's first content block starts its lists afresh.
|
|
439
|
+
this.lastContainer = undefined;
|
|
440
|
+
}
|
|
441
|
+
heading(block, root, key) {
|
|
442
|
+
const state = this.enter(block, root);
|
|
443
|
+
const file = this.file;
|
|
444
|
+
const last = this.lastLevel.get(file) ?? 0;
|
|
445
|
+
const level = Math.max(1, Math.min(6, block.headingLevel ?? 1, last + 1));
|
|
446
|
+
this.lastLevel.set(file, level);
|
|
447
|
+
const anchor = block.attrs?.id;
|
|
448
|
+
let id;
|
|
449
|
+
if (anchor && !this.book.anchors.has(anchor)) {
|
|
450
|
+
id = idOf('a-', anchor);
|
|
451
|
+
this.book.anchors.set(anchor, { file, id });
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
id = `h-${++this.counters.heading}`;
|
|
455
|
+
}
|
|
456
|
+
const cls = [];
|
|
457
|
+
if (block.headingStyleId)
|
|
458
|
+
cls.push(idOf('hs-', block.headingStyleId));
|
|
459
|
+
// A structural heading (its level hidden) prints nothing but is part
|
|
460
|
+
// of the outline: kept for navigation, out of sight.
|
|
461
|
+
if (block.hidden && block.lines.every((l) => l.bbox.height <= 0))
|
|
462
|
+
cls.push('pt-hidden');
|
|
463
|
+
const node = { k: 'h', level, id, inl: [], ...(cls.length ? { cls } : {}), ...(block.direction ? { dir: block.direction } : {}) };
|
|
464
|
+
state.nodes.push(node);
|
|
465
|
+
const top = this.topOf(state, node);
|
|
466
|
+
const sink = this.newSink(key, node.inl, top);
|
|
467
|
+
const pages = this.takePages();
|
|
468
|
+
if (block.sourceTitle !== undefined) {
|
|
469
|
+
// The title as written (the lines print it transformed, upper case):
|
|
470
|
+
// the stylesheet transforms it again.
|
|
471
|
+
const prefix = block.numberPrefix ? `${block.numberPrefix}${block.numberSeparator ?? ' '}` : '';
|
|
472
|
+
node.inl.push(...pages, { t: 'text', text: prefix + block.sourceTitle, fmt: {} });
|
|
473
|
+
sink.prev = block.lines[block.lines.length - 1];
|
|
474
|
+
}
|
|
475
|
+
else if (block.lines.length > 0) {
|
|
476
|
+
this.appendBlockLines(sink, block, block.lines, pages);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
const text = (block.designOverlay?.blocks ?? [])
|
|
480
|
+
.flatMap((b) => (b.kind === 'text' && !b.artifact ? [b.lines.map((l) => l.text).join(' ')] : []))
|
|
481
|
+
.join(' ');
|
|
482
|
+
node.inl.push(...pages, { t: 'text', text, fmt: {} });
|
|
483
|
+
}
|
|
484
|
+
this.record(block, top);
|
|
485
|
+
const label = plainText(node.inl);
|
|
486
|
+
file.title ??= label;
|
|
487
|
+
const style = block.headingStyleId ? this.config.headingStyles.find((s) => s.id === block.headingStyleId) : undefined;
|
|
488
|
+
const navDepth = Math.max(3, ...this.config.toc.levels.map((l) => l.level));
|
|
489
|
+
this.book.headings.push({
|
|
490
|
+
file,
|
|
491
|
+
id,
|
|
492
|
+
level: block.headingLevel ?? 1,
|
|
493
|
+
label,
|
|
494
|
+
page: this.offset + block.pageIndex,
|
|
495
|
+
listed: style?.toc !== false && (block.headingLevel ?? 1) <= navDepth,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
paragraph(block, root, key) {
|
|
499
|
+
const state = this.enter(block, root);
|
|
500
|
+
if (block.indexLevel !== undefined || hasPageLink(block) || (block.sourceStart !== undefined && this.indexRanges.has(rangeKey(block)))) {
|
|
501
|
+
this.indexEntries(block, state, key);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (block.lines.some((l) => l.verse !== undefined)) {
|
|
505
|
+
this.verse(block, state, key);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const node = { k: 'p', inl: [], ...(block.direction ? { dir: block.direction } : {}) };
|
|
509
|
+
const cls = [];
|
|
510
|
+
if (block.bibEntry !== undefined) {
|
|
511
|
+
cls.push('pt-bib');
|
|
512
|
+
const anchor = `ref-${block.bibEntry}`;
|
|
513
|
+
if (!this.book.anchors.has(anchor)) {
|
|
514
|
+
node.id = idOf('a-', anchor);
|
|
515
|
+
this.book.anchors.set(anchor, { file: this.file, id: node.id });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
const style = this.paragraphStyle(block);
|
|
520
|
+
if (style)
|
|
521
|
+
cls.push(idOf('ps-', style));
|
|
522
|
+
}
|
|
523
|
+
if (cls.length)
|
|
524
|
+
node.cls = cls;
|
|
525
|
+
state.nodes.push(node);
|
|
526
|
+
const top = this.topOf(state, node);
|
|
527
|
+
const sink = this.newSink(key, node.inl, top);
|
|
528
|
+
this.appendBlockLines(sink, block, block.lines, this.takePages());
|
|
529
|
+
this.record(block, top);
|
|
530
|
+
if (!this.floating && block.bibEntry !== undefined && !this.book.bibliography && node.id)
|
|
531
|
+
this.book.bibliography = { file: this.file, id: node.id };
|
|
532
|
+
}
|
|
533
|
+
/** The paragraph style a block is set in: the one the engine names, or
|
|
534
|
+
* (an older document) the one its face tells, outside boxes. */
|
|
535
|
+
paragraphStyle(block) {
|
|
536
|
+
if (this.styleHints)
|
|
537
|
+
return block.paragraphStyleId;
|
|
538
|
+
if (block.containerId !== undefined && this.calloutIds.has(block.containerId))
|
|
539
|
+
return undefined;
|
|
540
|
+
return paragraphStyleOf(block, this.config, this.bodyPx);
|
|
541
|
+
}
|
|
542
|
+
/** A `:::verse` poem (#378): its lines back to bayts. */
|
|
543
|
+
verse(block, state, key) {
|
|
544
|
+
const style = this.styleHints ? block.paragraphStyleId : undefined;
|
|
545
|
+
const node = {
|
|
546
|
+
k: 'verse',
|
|
547
|
+
pre: this.takePages(),
|
|
548
|
+
bayts: [],
|
|
549
|
+
...(style ? { cls: [idOf('ps-', style)] } : {}),
|
|
550
|
+
...(block.direction ? { dir: block.direction } : {}),
|
|
551
|
+
};
|
|
552
|
+
state.nodes.push(node);
|
|
553
|
+
this.book.verse = true;
|
|
554
|
+
const open = { node, top: this.topOf(state, node) };
|
|
555
|
+
this.verses.set(key, open);
|
|
556
|
+
this.verseLines(open, block);
|
|
557
|
+
this.record(block, open.top);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* The lines of a poem's fragment, bayt by bayt (`VDTLine.verse`). A
|
|
561
|
+
* whole bayt's line is cut at its gap (the `labelTab` space; with an
|
|
562
|
+
* ornament, the inserted mark and an empty space follow it); a
|
|
563
|
+
* staggered bayt comes as a ṣadr line and an ʿajuz line; a hemistich
|
|
564
|
+
* broken over lines is joined again. Page starts waiting for text open
|
|
565
|
+
* the next bayt.
|
|
566
|
+
*/
|
|
567
|
+
verseLines(open, block) {
|
|
568
|
+
const ctx = this.ctx(block);
|
|
569
|
+
const bayts = open.node.bayts;
|
|
570
|
+
for (const line of block.lines) {
|
|
571
|
+
const before = [...this.takePages(), ...this.anchorsAt(block, line)];
|
|
572
|
+
const v = line.verse ?? { bayt: -1, part: 'single' };
|
|
573
|
+
const segs = line.segments ?? [];
|
|
574
|
+
if (v.part === 'bayt') {
|
|
575
|
+
const gap = segs.findIndex((g) => g.kind === 'space' && g.labelTab);
|
|
576
|
+
const sadr = gap >= 0 ? segs.slice(0, gap) : segs;
|
|
577
|
+
const rest = gap >= 0 ? segs.slice(gap + 1) : [];
|
|
578
|
+
const ornament = rest.find((g) => g.kind === 'text' && g.inserted)?.text;
|
|
579
|
+
const ajuz = rest.filter((g) => !g.inserted && !(g.kind === 'space' && g.labelTab));
|
|
580
|
+
while (ajuz[0]?.kind === 'space')
|
|
581
|
+
ajuz.shift();
|
|
582
|
+
const bayt = { sadr: [], ajuz: [], ...(ornament ? { ornament } : {}) };
|
|
583
|
+
bayts.push(bayt);
|
|
584
|
+
appendLine({ inl: bayt.sadr }, line, ctx, before, sadr);
|
|
585
|
+
open.sink = { inl: bayt.ajuz };
|
|
586
|
+
appendLine(open.sink, line, ctx, [], ajuz);
|
|
587
|
+
}
|
|
588
|
+
else if (open.sink && v.bayt === open.bayt && v.part === open.part) {
|
|
589
|
+
appendLine(open.sink, line, ctx, before, segs);
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
let bayt = bayts[bayts.length - 1];
|
|
593
|
+
if (!(v.part === 'ajuz' && bayt && v.bayt === open.bayt && open.part === 'sadr')) {
|
|
594
|
+
bayt = { sadr: [], ajuz: [], ...(v.part === 'single' ? { single: true } : {}) };
|
|
595
|
+
bayts.push(bayt);
|
|
596
|
+
}
|
|
597
|
+
open.sink = { inl: v.part === 'ajuz' ? bayt.ajuz : bayt.sadr };
|
|
598
|
+
appendLine(open.sink, line, ctx, before, segs);
|
|
599
|
+
}
|
|
600
|
+
open.bayt = v.bayt;
|
|
601
|
+
open.part = v.part;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* A block of the back-of-book index: its group letter, then each entry it
|
|
606
|
+
* holds (the entries with no page of their own that head a sub-entry, and
|
|
607
|
+
* the sub-entry) as a paragraph of its own, classed by depth
|
|
608
|
+
* (`pt-index-l1`, `pt-index-l2`…). The engine marks the first line of
|
|
609
|
+
* each entry with its level (`VDTLine.indexLevel`); in an older document
|
|
610
|
+
* an entry starts on a line set at its level's indent, its turnover lines
|
|
611
|
+
* hanging `turnoverIndent` further in (`indexDirective.ts`). `cont` is
|
|
612
|
+
* the sink of the entry a later fragment's first lines go on.
|
|
613
|
+
*/
|
|
614
|
+
indexEntries(block, state, key, cont) {
|
|
615
|
+
let lines = block.lines;
|
|
616
|
+
const pages = this.takePages();
|
|
617
|
+
const hinted = block.indexLevel !== undefined;
|
|
618
|
+
const entries = [];
|
|
619
|
+
if (hinted) {
|
|
620
|
+
// Lines before the first entry: the run-on of the entry `cont` holds,
|
|
621
|
+
// or the group's letter.
|
|
622
|
+
const first = lines.findIndex((l) => l.indexLevel !== undefined);
|
|
623
|
+
const lead = first < 0 ? lines : lines.slice(0, first);
|
|
624
|
+
if (lead.length > 0 && cont) {
|
|
625
|
+
this.appendBlockLines(cont, block, lead, pages.splice(0));
|
|
626
|
+
}
|
|
627
|
+
else if (lead.length > 0) {
|
|
628
|
+
const group = { k: 'p', inl: [], cls: ['pt-index-group'] };
|
|
629
|
+
state.nodes.push(group);
|
|
630
|
+
this.appendBlockLines({ inl: group.inl }, block, lead, pages.splice(0));
|
|
631
|
+
}
|
|
632
|
+
for (const line of first < 0 ? [] : lines.slice(first)) {
|
|
633
|
+
if (line.indexLevel !== undefined)
|
|
634
|
+
entries.push({ level: line.indexLevel, lines: [line] });
|
|
635
|
+
else
|
|
636
|
+
entries[entries.length - 1].lines.push(line);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
// A group letter of the index, set as the first line of its first
|
|
641
|
+
// entry in a face of its own: a paragraph of its own.
|
|
642
|
+
const head = lines[0].segments ?? [];
|
|
643
|
+
const letter = lines.length > 1 && head.some((g) => g.kind === 'text') && head.every((g) => g.kind === 'space' || (g.fontString !== undefined && g.pageLink === undefined));
|
|
644
|
+
if (letter) {
|
|
645
|
+
const group = { k: 'p', inl: [], cls: ['pt-index-group'] };
|
|
646
|
+
state.nodes.push(group);
|
|
647
|
+
this.appendBlockLines({ inl: group.inl }, block, lines.slice(0, 1), pages.splice(0));
|
|
648
|
+
lines = lines.slice(1);
|
|
649
|
+
}
|
|
650
|
+
const cfg = this.config.index;
|
|
651
|
+
const dpi = this.config.page.dpi;
|
|
652
|
+
const sizePx = dimensionToPx(cfg.fontSize, dpi, this.bodyPx);
|
|
653
|
+
const indentPx = dimensionToPx(cfg.indent, dpi, sizePx);
|
|
654
|
+
const turnoverPx = dimensionToPx(cfg.turnoverIndent, dpi, sizePx);
|
|
655
|
+
let lastX = 0;
|
|
656
|
+
for (const line of lines) {
|
|
657
|
+
const x = line.bbox.x - block.bbox.x;
|
|
658
|
+
const last = entries[entries.length - 1];
|
|
659
|
+
if (last && Math.abs(x - (lastX + turnoverPx)) < 1) {
|
|
660
|
+
last.lines.push(line);
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
entries.push({ level: indentPx > 0 ? Math.max(0, Math.round(x / indentPx)) : 0, lines: [line] });
|
|
664
|
+
lastX = x;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
let top;
|
|
669
|
+
for (const [i, entry] of entries.entries()) {
|
|
670
|
+
const node = {
|
|
671
|
+
k: 'p',
|
|
672
|
+
inl: [],
|
|
673
|
+
cls: ['pt-index-entry', ...(entry.level > 0 ? [`pt-index-l${Math.min(entry.level, 3)}`] : [])],
|
|
674
|
+
...(block.direction ? { dir: block.direction } : {}),
|
|
675
|
+
};
|
|
676
|
+
state.nodes.push(node);
|
|
677
|
+
top = this.topOf(state, node);
|
|
678
|
+
// The last entry takes the lines of the block's next fragment.
|
|
679
|
+
const sink = i === entries.length - 1 ? this.newSink(key, node.inl, top) : { inl: node.inl };
|
|
680
|
+
this.appendBlockLines(sink, block, entry.lines, i === 0 ? pages : []);
|
|
681
|
+
if (!this.floating && !this.book.index)
|
|
682
|
+
this.book.index = { file: this.file, id: (node.id ??= `b-${++this.counters.block}`) };
|
|
683
|
+
}
|
|
684
|
+
if (top)
|
|
685
|
+
this.record(block, top);
|
|
686
|
+
}
|
|
687
|
+
quote(block, root, key) {
|
|
688
|
+
const state = this.enter(block, root);
|
|
689
|
+
if (!state.quote) {
|
|
690
|
+
state.quote = { k: 'quote', children: [] };
|
|
691
|
+
state.nodes.push(state.quote);
|
|
692
|
+
}
|
|
693
|
+
const node = { k: 'p', inl: [], ...(block.direction ? { dir: block.direction } : {}) };
|
|
694
|
+
state.quote.children.push(node);
|
|
695
|
+
const top = this.topOf(state, state.quote);
|
|
696
|
+
const sink = this.newSink(key, node.inl, top);
|
|
697
|
+
this.appendBlockLines(sink, block, block.lines, this.takePages());
|
|
698
|
+
this.record(block, top);
|
|
699
|
+
}
|
|
700
|
+
listItem(block, root, key) {
|
|
701
|
+
const state = this.enter(block, root);
|
|
702
|
+
const depth = block.listDepth ?? 1;
|
|
703
|
+
const lists = state.lists;
|
|
704
|
+
while (lists.length > 0 && lists[lists.length - 1].depth > depth)
|
|
705
|
+
lists.pop();
|
|
706
|
+
let top = lists[lists.length - 1];
|
|
707
|
+
if (top && top.depth === depth && top.kind !== block.listKind) {
|
|
708
|
+
lists.pop();
|
|
709
|
+
top = lists[lists.length - 1];
|
|
710
|
+
}
|
|
711
|
+
if (!top || top.depth < depth) {
|
|
712
|
+
const node = { k: 'list', ordered: block.listKind === 'ordered', task: block.listKind === 'task', items: [] };
|
|
713
|
+
(top?.lastItem?.children ?? state.nodes).push(node);
|
|
714
|
+
top = { depth, kind: block.listKind, node };
|
|
715
|
+
lists.push(top);
|
|
716
|
+
}
|
|
717
|
+
const marker = [block.prefixText, block.bulletText, block.separatorText].filter(Boolean).join('');
|
|
718
|
+
const item = {
|
|
719
|
+
inl: [],
|
|
720
|
+
children: [],
|
|
721
|
+
...(marker ? { marker } : {}),
|
|
722
|
+
...(block.strikethroughText ? { done: true } : {}),
|
|
723
|
+
...(block.direction ? { dir: block.direction } : {}),
|
|
724
|
+
};
|
|
725
|
+
top.node.items.push(item);
|
|
726
|
+
top.lastItem = item;
|
|
727
|
+
const topNode = this.topOf(state, lists[0].node);
|
|
728
|
+
const sink = this.newSink(key, item.inl, topNode);
|
|
729
|
+
this.appendBlockLines(sink, block, block.lines, this.takePages());
|
|
730
|
+
this.record(block, topNode);
|
|
731
|
+
}
|
|
732
|
+
/** A row of a `:::toc`: its number, its title (the leader and page
|
|
733
|
+
* number dropped: the row links to the heading instead) and the
|
|
734
|
+
* subtitle lines under it. */
|
|
735
|
+
tocRow(block, root, key) {
|
|
736
|
+
const state = this.enter(block, root);
|
|
737
|
+
if (!state.toc) {
|
|
738
|
+
state.toc = { k: 'toc', pre: this.takePages(), rows: [] };
|
|
739
|
+
state.nodes.push(state.toc);
|
|
740
|
+
if (!this.floating && !this.book.contents)
|
|
741
|
+
this.book.contents = { file: this.file, id: `toc-${++this.counters.block}` };
|
|
742
|
+
}
|
|
743
|
+
const top = this.topOf(state, state.toc);
|
|
744
|
+
if (block.tocPart) {
|
|
745
|
+
const p = block.tocPart;
|
|
746
|
+
const row = {
|
|
747
|
+
...(p.number ? { number: p.number } : {}),
|
|
748
|
+
inl: [{ t: 'text', text: p.title, fmt: {} }],
|
|
749
|
+
subtitle: [],
|
|
750
|
+
...(p.pageIndex !== undefined ? { page: p.pageIndex } : {}),
|
|
751
|
+
part: true,
|
|
752
|
+
title: p.title,
|
|
753
|
+
};
|
|
754
|
+
state.toc.rows.push(row);
|
|
755
|
+
this.record(block, top);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
const lines = block.lines;
|
|
759
|
+
const isLabel = (s) => s.kind === 'space' || (s.kind === 'text' && s.fontString !== undefined);
|
|
760
|
+
let labelLine = -1;
|
|
761
|
+
let cut = 0;
|
|
762
|
+
for (let i = 0; i < lines.length && labelLine < 0; i++) {
|
|
763
|
+
const segs = lines[i].segments ?? [];
|
|
764
|
+
const lastText = [...segs].reverse().find((s) => s.kind !== 'space');
|
|
765
|
+
if (lastText?.kind === 'text' && lastText.fontString !== undefined) {
|
|
766
|
+
labelLine = i;
|
|
767
|
+
cut = segs.length;
|
|
768
|
+
while (cut > 0 && isLabel(segs[cut - 1]))
|
|
769
|
+
cut--;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const ctx = this.ctx(block);
|
|
773
|
+
const title = { inl: [] };
|
|
774
|
+
const subtitle = { inl: [] };
|
|
775
|
+
lines.forEach((line, i) => {
|
|
776
|
+
if (labelLine >= 0 && i > labelLine)
|
|
777
|
+
appendLine(subtitle, line, ctx);
|
|
778
|
+
else
|
|
779
|
+
appendLine(title, line, ctx, [], i === labelLine ? (line.segments ?? []).slice(0, cut) : undefined);
|
|
780
|
+
});
|
|
781
|
+
const row = {
|
|
782
|
+
...(block.bulletText ? { number: block.bulletText } : {}),
|
|
783
|
+
inl: title.inl,
|
|
784
|
+
subtitle: subtitle.inl,
|
|
785
|
+
...(block.tocEntry?.pageIndex !== undefined ? { page: block.tocEntry.pageIndex } : {}),
|
|
786
|
+
title: plainText(title.inl),
|
|
787
|
+
};
|
|
788
|
+
state.toc.rows.push(row);
|
|
789
|
+
this.newSink(key, row.inl, top).prev = lines[labelLine >= 0 ? labelLine : lines.length - 1];
|
|
790
|
+
this.record(block, top);
|
|
791
|
+
}
|
|
792
|
+
math(block, root) {
|
|
793
|
+
const state = this.enter(block, root);
|
|
794
|
+
const render = block.mathRender;
|
|
795
|
+
const node = {
|
|
796
|
+
k: 'math',
|
|
797
|
+
pre: this.takePages(),
|
|
798
|
+
tex: render?.tex ?? block.tex ?? '',
|
|
799
|
+
svg: '',
|
|
800
|
+
};
|
|
801
|
+
if (render && !render.error) {
|
|
802
|
+
node.svg = mathSvg(render, fontPx(block.fontString), false);
|
|
803
|
+
this.book.maths = true;
|
|
804
|
+
}
|
|
805
|
+
state.nodes.push(node);
|
|
806
|
+
this.record(block, this.topOf(state, node));
|
|
807
|
+
}
|
|
808
|
+
resource(block, root) {
|
|
809
|
+
const rb = block.resourceBlock;
|
|
810
|
+
if (!rb)
|
|
811
|
+
return;
|
|
812
|
+
const res = rb.resource;
|
|
813
|
+
const state = this.enter(block, root);
|
|
814
|
+
const ctx = this.ctx({ fontString: rb.captionFontString });
|
|
815
|
+
const lines = (ls) => {
|
|
816
|
+
const sink = { inl: [] };
|
|
817
|
+
for (const l of ls)
|
|
818
|
+
appendLine(sink, l, ctx);
|
|
819
|
+
return sink.inl;
|
|
820
|
+
};
|
|
821
|
+
const captionAbove = this.config.captionStyle.position === 'above';
|
|
822
|
+
if (rb.kind === 'table' && rb.table) {
|
|
823
|
+
let node = this.tables.get(res.id);
|
|
824
|
+
if (!node) {
|
|
825
|
+
node = {
|
|
826
|
+
k: 'table',
|
|
827
|
+
id: idOf('res-', res.id),
|
|
828
|
+
pre: this.takePages(),
|
|
829
|
+
caption: lines(rb.captionLines),
|
|
830
|
+
captionAbove,
|
|
831
|
+
note: [],
|
|
832
|
+
headerRows: res.table?.model.headerRowCount ?? 0,
|
|
833
|
+
...(res.table?.model.columnWidths ? { columnWidths: res.table.model.columnWidths } : {}),
|
|
834
|
+
cells: new Map(),
|
|
835
|
+
};
|
|
836
|
+
this.tables.set(res.id, node);
|
|
837
|
+
state.nodes.push(node);
|
|
838
|
+
if (!this.book.resources.has(res.id))
|
|
839
|
+
this.book.resources.set(res.id, { file: this.file, id: node.id });
|
|
840
|
+
this.record(block, this.topOf(state, node));
|
|
841
|
+
}
|
|
842
|
+
if (rb.noteLines.length > 0 && node.note.length === 0)
|
|
843
|
+
node.note = lines(rb.noteLines);
|
|
844
|
+
const cellCtx = this.ctx({ fontString: rb.table.fontString });
|
|
845
|
+
for (const cell of rb.table.cells) {
|
|
846
|
+
const k = `${cell.row}:${cell.col}`;
|
|
847
|
+
if (node.cells.has(k))
|
|
848
|
+
continue;
|
|
849
|
+
const sink = { inl: [] };
|
|
850
|
+
for (const l of cell.lines)
|
|
851
|
+
appendLine(sink, l, cellCtx);
|
|
852
|
+
const c = {
|
|
853
|
+
row: cell.row,
|
|
854
|
+
col: cell.col,
|
|
855
|
+
colSpan: cell.colSpan,
|
|
856
|
+
rowSpan: cell.rowSpan,
|
|
857
|
+
header: cell.isHeader,
|
|
858
|
+
align: cell.align,
|
|
859
|
+
verticalAlign: cell.verticalAlign,
|
|
860
|
+
inl: sink.inl,
|
|
861
|
+
...(cell.background ? { background: cell.background } : {}),
|
|
862
|
+
...(cell.alternate ? { alternate: true } : {}),
|
|
863
|
+
};
|
|
864
|
+
if (cell.image) {
|
|
865
|
+
const alt = cell.image.altText ?? '';
|
|
866
|
+
c.image = {
|
|
867
|
+
fileId: cell.image.fileId,
|
|
868
|
+
alt,
|
|
869
|
+
...(cell.rect.width > 0 ? { width: Math.min(1, cell.image.rect.width / cell.rect.width) } : {}),
|
|
870
|
+
};
|
|
871
|
+
this.book.images.add(cell.image.fileId);
|
|
872
|
+
if (alt)
|
|
873
|
+
this.book.altText = true;
|
|
874
|
+
else
|
|
875
|
+
this.book.missingAlt = true;
|
|
876
|
+
}
|
|
877
|
+
node.cells.set(k, c);
|
|
878
|
+
}
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (this.figures.has(res.id))
|
|
882
|
+
return;
|
|
883
|
+
this.figures.add(res.id);
|
|
884
|
+
const caption = lines(rb.captionLines);
|
|
885
|
+
const alt = (res.altText?.trim() || plainCaption(res.caption) || '').trim();
|
|
886
|
+
const node = {
|
|
887
|
+
k: 'figure',
|
|
888
|
+
id: idOf('res-', res.id),
|
|
889
|
+
pre: this.takePages(),
|
|
890
|
+
...(rb.fileId ? { fileId: rb.fileId } : {}),
|
|
891
|
+
alt,
|
|
892
|
+
caption,
|
|
893
|
+
captionAbove,
|
|
894
|
+
note: lines(rb.noteLines),
|
|
895
|
+
};
|
|
896
|
+
if (rb.fileId)
|
|
897
|
+
this.book.images.add(rb.fileId);
|
|
898
|
+
if (alt)
|
|
899
|
+
this.book.altText = true;
|
|
900
|
+
else
|
|
901
|
+
this.book.missingAlt = true;
|
|
902
|
+
state.nodes.push(node);
|
|
903
|
+
if (!this.book.resources.has(res.id))
|
|
904
|
+
this.book.resources.set(res.id, { file: this.file, id: node.id });
|
|
905
|
+
this.record(block, this.topOf(state, node));
|
|
906
|
+
}
|
|
907
|
+
// --- floats --------------------------------------------------------------
|
|
908
|
+
/** Read a float group where it belongs: after the text block with the
|
|
909
|
+
* highest content index up to its own, after the floats already read
|
|
910
|
+
* there; ahead of the document's first block when none comes before. */
|
|
911
|
+
readFloat(group) {
|
|
912
|
+
const index = group[0].contentIndex;
|
|
913
|
+
let anchor;
|
|
914
|
+
if (index !== undefined) {
|
|
915
|
+
for (let i = this.flow.length - 1; i >= 0; i--) {
|
|
916
|
+
if (this.flow[i].index <= index) {
|
|
917
|
+
anchor = this.flow[i];
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// The float is read in the document its anchor is in: the ids it
|
|
923
|
+
// registers (a figure, an anchor in a floated box) belong there.
|
|
924
|
+
const first = this.flow[0];
|
|
925
|
+
const file = anchor?.file ?? first?.file ?? this.file;
|
|
926
|
+
const temp = { nodes: [], lists: [] };
|
|
927
|
+
const saved = { container: this.lastContainer, file: this.file };
|
|
928
|
+
this.floating = true;
|
|
929
|
+
this.lastContainer = undefined;
|
|
930
|
+
this.file = file;
|
|
931
|
+
try {
|
|
932
|
+
for (const block of group)
|
|
933
|
+
this.place(block, temp);
|
|
934
|
+
}
|
|
935
|
+
finally {
|
|
936
|
+
this.floating = false;
|
|
937
|
+
this.lastContainer = saved.container;
|
|
938
|
+
this.file = saved.file;
|
|
939
|
+
}
|
|
940
|
+
if (temp.nodes.length === 0)
|
|
941
|
+
return;
|
|
942
|
+
if (!anchor) {
|
|
943
|
+
const at = first ? Math.max(0, file.nodes.indexOf(first.top)) : file.nodes.length;
|
|
944
|
+
file.nodes.splice(at, 0, ...temp.nodes);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const nodes = file.nodes;
|
|
948
|
+
const after = nodes.indexOf(this.placedAfter.get(anchor.top) ?? anchor.top);
|
|
949
|
+
nodes.splice(after >= 0 ? after + 1 : nodes.length, 0, ...temp.nodes);
|
|
950
|
+
this.placedAfter.set(anchor.top, temp.nodes[temp.nodes.length - 1]);
|
|
951
|
+
}
|
|
952
|
+
// --- pull quotes ---------------------------------------------------------
|
|
953
|
+
/**
|
|
954
|
+
* Pull quotes: a box of text alone (untitled, or titled with marks
|
|
955
|
+
* alone) whose words (twenty letters or more)
|
|
956
|
+
* the chapter's running text also reads (letters and digits compared,
|
|
957
|
+
* case aside; an ellipsis in the box may stand for words left out) is
|
|
958
|
+
* marked `'echo'`, and the
|
|
959
|
+
* serializer hides it from assistive technology, so its words are read
|
|
960
|
+
* once, where the text has them. A box whose style id names it a pull
|
|
961
|
+
* quote (`pullquote`, `pull-quote`…) but whose words are its own stays
|
|
962
|
+
* readable (`'own'`).
|
|
963
|
+
*/
|
|
964
|
+
markPullQuotes() {
|
|
965
|
+
const files = this.book.files.filter((f) => f.doc === this.index);
|
|
966
|
+
const text = [];
|
|
967
|
+
const boxes = [];
|
|
968
|
+
const collect = (nodes) => {
|
|
969
|
+
for (const n of nodes) {
|
|
970
|
+
switch (n.k) {
|
|
971
|
+
case 'callout':
|
|
972
|
+
boxes.push(n);
|
|
973
|
+
break;
|
|
974
|
+
case 'p':
|
|
975
|
+
case 'h':
|
|
976
|
+
text.push(plainText(n.inl));
|
|
977
|
+
break;
|
|
978
|
+
case 'quote':
|
|
979
|
+
collect(n.children);
|
|
980
|
+
break;
|
|
981
|
+
case 'list': {
|
|
982
|
+
const items = (list) => {
|
|
983
|
+
for (const item of list.items) {
|
|
984
|
+
text.push(plainText(item.inl));
|
|
985
|
+
collect(item.children);
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
items(n);
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
case 'verse':
|
|
992
|
+
for (const b of n.bayts)
|
|
993
|
+
text.push(plainText(b.sadr), plainText(b.ajuz));
|
|
994
|
+
break;
|
|
995
|
+
default:
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
for (const f of files)
|
|
1001
|
+
collect(f.nodes);
|
|
1002
|
+
if (boxes.length === 0)
|
|
1003
|
+
return;
|
|
1004
|
+
const body = letters(text.join(' '));
|
|
1005
|
+
for (const box of boxes) {
|
|
1006
|
+
const named = /pull[\s_-]?quote/i.test(box.styleId ?? '');
|
|
1007
|
+
// A title of marks alone (a hanging quotation mark) is decoration.
|
|
1008
|
+
const echoed = letters(box.title ?? '') === '' && box.children.length > 0 && box.children.every((c) => c.k === 'p' || c.k === 'quote') && echoes(box, body);
|
|
1009
|
+
if (echoed)
|
|
1010
|
+
box.pullQuote = 'echo';
|
|
1011
|
+
else if (named)
|
|
1012
|
+
box.pullQuote = 'own';
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// --- notes ---------------------------------------------------------------
|
|
1016
|
+
note(block) {
|
|
1017
|
+
const id = block.footnoteNote;
|
|
1018
|
+
let note = this.notes.get(id);
|
|
1019
|
+
if (!note) {
|
|
1020
|
+
note = { doc: this.index, id, inl: [], sink: { inl: [] }, ...(block.direction ? { dir: block.direction } : {}) };
|
|
1021
|
+
note.inl = note.sink.inl;
|
|
1022
|
+
this.notes.set(id, note);
|
|
1023
|
+
}
|
|
1024
|
+
const saved = this.floating;
|
|
1025
|
+
this.floating = true;
|
|
1026
|
+
try {
|
|
1027
|
+
appendLines(note.sink, block.lines, this.ctx(block));
|
|
1028
|
+
}
|
|
1029
|
+
finally {
|
|
1030
|
+
this.floating = saved;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
/** Each note goes to the document of its first marker (the last
|
|
1034
|
+
* document when no marker was read). */
|
|
1035
|
+
finishNotes() {
|
|
1036
|
+
for (const note of this.notes.values()) {
|
|
1037
|
+
const loc = this.book.notes.get(`${this.index}:${note.id}`);
|
|
1038
|
+
const file = loc?.file ?? this.file;
|
|
1039
|
+
if (!loc)
|
|
1040
|
+
this.book.notes.set(`${this.index}:${note.id}`, { file, id: idOf('fn-', note.id) });
|
|
1041
|
+
file.notes.push({ doc: note.doc, id: note.id, inl: note.inl, ...(note.dir ? { dir: note.dir } : {}) });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/** The letters and digits of a text, lower case: what two settings of
|
|
1046
|
+
* the same words share whatever their quotation marks, dashes and
|
|
1047
|
+
* spacing. */
|
|
1048
|
+
function letters(text) {
|
|
1049
|
+
return text.normalize('NFKC').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, '');
|
|
1050
|
+
}
|
|
1051
|
+
/** Whether the words of a box are read in `body` (see `letters`): each run
|
|
1052
|
+
* between the ellipses that mark words left out, in order. */
|
|
1053
|
+
function echoes(box, body) {
|
|
1054
|
+
const words = box.children
|
|
1055
|
+
.flatMap((c) => (c.k === 'p' ? [c] : c.k === 'quote' ? c.children : []))
|
|
1056
|
+
.map((p) => plainText(p.inl))
|
|
1057
|
+
.join(' ');
|
|
1058
|
+
const runs = words.split(/\[?(?:…|\.\.\.)\]?/).map(letters).filter((r) => r.length > 0);
|
|
1059
|
+
// A few words can repeat the text by chance: a pull quote is a sentence.
|
|
1060
|
+
if (runs.reduce((n, r) => n + r.length, 0) < 20)
|
|
1061
|
+
return false;
|
|
1062
|
+
let at = 0;
|
|
1063
|
+
for (const run of runs) {
|
|
1064
|
+
const found = body.indexOf(run, at);
|
|
1065
|
+
if (found < 0)
|
|
1066
|
+
return false;
|
|
1067
|
+
at = found + run.length;
|
|
1068
|
+
}
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
function escapeText(text) {
|
|
1072
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1073
|
+
}
|
|
1074
|
+
/** A resource caption as plain text (its Markdown marks dropped). */
|
|
1075
|
+
function plainCaption(caption) {
|
|
1076
|
+
return (caption ?? '').replace(/[*_`~^]/g, '').replace(/:\w+\[([^\]]*)\]\{[^}]*\}/g, '$1').replace(/:\w+\{[^}]*\}/g, '').trim();
|
|
1077
|
+
}
|
|
1078
|
+
/** The paragraph style a block of a `:::paragraphs` container was set in,
|
|
1079
|
+
* told by its type (the block keeps no style id): a block set otherwise
|
|
1080
|
+
* than the body text whose family, size, slant and alignment match one
|
|
1081
|
+
* style alone. */
|
|
1082
|
+
function paragraphStyleOf(block, config, bodyPx) {
|
|
1083
|
+
const px = fontPx(block.fontString);
|
|
1084
|
+
const italic = /\bitalic\b/.test(block.fontString);
|
|
1085
|
+
const familyOf = (f) => primaryFontFamily(f).replace(/^['"]|['"]$/g, '');
|
|
1086
|
+
const body = config.bodyText;
|
|
1087
|
+
const asBody = Math.abs(px - bodyPx) < 0.01 && !italic && block.textAlign === body.textAlign && block.fontString.includes(familyOf(body.fontFamily));
|
|
1088
|
+
if (asBody)
|
|
1089
|
+
return undefined;
|
|
1090
|
+
const matches = config.paragraphStyles.filter((s) => {
|
|
1091
|
+
const size = dimensionToPx(s.fontSize, config.page.dpi, bodyPx);
|
|
1092
|
+
return Math.abs(size - px) < 0.01 && s.italic === italic && s.textAlign === block.textAlign && block.fontString.includes(familyOf(s.fontFamily));
|
|
1093
|
+
});
|
|
1094
|
+
return matches.length === 1 ? matches[0].id : undefined;
|
|
1095
|
+
}
|
|
1096
|
+
//# sourceMappingURL=walk.js.map
|