wolfy-reader 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 +277 -0
- package/dist/core/book.d.ts +75 -0
- package/dist/core/book.d.ts.map +1 -0
- package/dist/core/book.js +2 -0
- package/dist/core/book.js.map +1 -0
- package/dist/core/errors.d.ts +17 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +29 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/format.d.ts +13 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/format.js +2 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/graphemes.d.ts +36 -0
- package/dist/core/graphemes.d.ts.map +1 -0
- package/dist/core/graphemes.js +62 -0
- package/dist/core/graphemes.js.map +1 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/lookup.d.ts +10 -0
- package/dist/core/lookup.d.ts.map +1 -0
- package/dist/core/lookup.js +16 -0
- package/dist/core/lookup.js.map +1 -0
- package/dist/core/open.d.ts +12 -0
- package/dist/core/open.d.ts.map +1 -0
- package/dist/core/open.js +23 -0
- package/dist/core/open.js.map +1 -0
- package/dist/core/position.d.ts +121 -0
- package/dist/core/position.d.ts.map +1 -0
- package/dist/core/position.js +327 -0
- package/dist/core/position.js.map +1 -0
- package/dist/core/reading-text.d.ts +77 -0
- package/dist/core/reading-text.d.ts.map +1 -0
- package/dist/core/reading-text.js +144 -0
- package/dist/core/reading-text.js.map +1 -0
- package/dist/core/source.d.ts +17 -0
- package/dist/core/source.d.ts.map +1 -0
- package/dist/core/source.js +39 -0
- package/dist/core/source.js.map +1 -0
- package/dist/core/storage.d.ts +7 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +2 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/text.d.ts +18 -0
- package/dist/core/text.d.ts.map +1 -0
- package/dist/core/text.js +32 -0
- package/dist/core/text.js.map +1 -0
- package/dist/formats/epub/href.d.ts +14 -0
- package/dist/formats/epub/href.d.ts.map +1 -0
- package/dist/formats/epub/href.js +52 -0
- package/dist/formats/epub/href.js.map +1 -0
- package/dist/formats/epub/index.d.ts +3 -0
- package/dist/formats/epub/index.d.ts.map +1 -0
- package/dist/formats/epub/index.js +223 -0
- package/dist/formats/epub/index.js.map +1 -0
- package/dist/formats/epub/opf.d.ts +27 -0
- package/dist/formats/epub/opf.d.ts.map +1 -0
- package/dist/formats/epub/opf.js +110 -0
- package/dist/formats/epub/opf.js.map +1 -0
- package/dist/formats/epub/toc.d.ts +6 -0
- package/dist/formats/epub/toc.d.ts.map +1 -0
- package/dist/formats/epub/toc.js +88 -0
- package/dist/formats/epub/toc.js.map +1 -0
- package/dist/formats/fb2/index.d.ts +13 -0
- package/dist/formats/fb2/index.d.ts.map +1 -0
- package/dist/formats/fb2/index.js +317 -0
- package/dist/formats/fb2/index.js.map +1 -0
- package/dist/formats/index.d.ts +4 -0
- package/dist/formats/index.d.ts.map +1 -0
- package/dist/formats/index.js +4 -0
- package/dist/formats/index.js.map +1 -0
- package/dist/formats/text/index.d.ts +11 -0
- package/dist/formats/text/index.d.ts.map +1 -0
- package/dist/formats/text/index.js +201 -0
- package/dist/formats/text/index.js.map +1 -0
- package/dist/formats/xml.d.ts +33 -0
- package/dist/formats/xml.d.ts.map +1 -0
- package/dist/formats/xml.js +321 -0
- package/dist/formats/xml.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/chunk.d.ts +51 -0
- package/dist/layout/chunk.d.ts.map +1 -0
- package/dist/layout/chunk.js +131 -0
- package/dist/layout/chunk.js.map +1 -0
- package/dist/layout/index.d.ts +238 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +476 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/reader/index.d.ts +231 -0
- package/dist/reader/index.d.ts.map +1 -0
- package/dist/reader/index.js +958 -0
- package/dist/reader/index.js.map +1 -0
- package/dist/reader/input.d.ts +69 -0
- package/dist/reader/input.d.ts.map +1 -0
- package/dist/reader/input.js +75 -0
- package/dist/reader/input.js.map +1 -0
- package/dist/search/extract.d.ts +51 -0
- package/dist/search/extract.d.ts.map +1 -0
- package/dist/search/extract.js +401 -0
- package/dist/search/extract.js.map +1 -0
- package/dist/search/index.d.ts +11 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +11 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/matcher.d.ts +56 -0
- package/dist/search/matcher.d.ts.map +1 -0
- package/dist/search/matcher.js +128 -0
- package/dist/search/matcher.js.map +1 -0
- package/dist/search/normalize.d.ts +48 -0
- package/dist/search/normalize.d.ts.map +1 -0
- package/dist/search/normalize.js +103 -0
- package/dist/search/normalize.js.map +1 -0
- package/dist/view/allowlist.d.ts +59 -0
- package/dist/view/allowlist.d.ts.map +1 -0
- package/dist/view/allowlist.js +198 -0
- package/dist/view/allowlist.js.map +1 -0
- package/dist/view/appearance.d.ts +137 -0
- package/dist/view/appearance.d.ts.map +1 -0
- package/dist/view/appearance.js +274 -0
- package/dist/view/appearance.js.map +1 -0
- package/dist/view/css.d.ts +24 -0
- package/dist/view/css.d.ts.map +1 -0
- package/dist/view/css.js +153 -0
- package/dist/view/css.js.map +1 -0
- package/dist/view/frame.d.ts +54 -0
- package/dist/view/frame.d.ts.map +1 -0
- package/dist/view/frame.js +980 -0
- package/dist/view/frame.js.map +1 -0
- package/dist/view/host.d.ts +160 -0
- package/dist/view/host.d.ts.map +1 -0
- package/dist/view/host.js +385 -0
- package/dist/view/host.js.map +1 -0
- package/dist/view/index.d.ts +12 -0
- package/dist/view/index.d.ts.map +1 -0
- package/dist/view/index.js +7 -0
- package/dist/view/index.js.map +1 -0
- package/dist/view/protocol.d.ts +220 -0
- package/dist/view/protocol.d.ts.map +1 -0
- package/dist/view/protocol.js +246 -0
- package/dist/view/protocol.js.map +1 -0
- package/dist/view/reference.d.ts +18 -0
- package/dist/view/reference.d.ts.map +1 -0
- package/dist/view/reference.js +25 -0
- package/dist/view/reference.js.map +1 -0
- package/dist/view/resources.d.ts +51 -0
- package/dist/view/resources.d.ts.map +1 -0
- package/dist/view/resources.js +242 -0
- package/dist/view/resources.js.map +1 -0
- package/dist/view/sanitize.d.ts +32 -0
- package/dist/view/sanitize.d.ts.map +1 -0
- package/dist/view/sanitize.js +226 -0
- package/dist/view/sanitize.js.map +1 -0
- package/dist/zip/central.d.ts +12 -0
- package/dist/zip/central.d.ts.map +1 -0
- package/dist/zip/central.js +72 -0
- package/dist/zip/central.js.map +1 -0
- package/dist/zip/crc32.d.ts +3 -0
- package/dist/zip/crc32.d.ts.map +1 -0
- package/dist/zip/crc32.js +28 -0
- package/dist/zip/crc32.js.map +1 -0
- package/dist/zip/eocd.d.ts +9 -0
- package/dist/zip/eocd.d.ts.map +1 -0
- package/dist/zip/eocd.js +84 -0
- package/dist/zip/eocd.js.map +1 -0
- package/dist/zip/errors.d.ts +19 -0
- package/dist/zip/errors.d.ts.map +1 -0
- package/dist/zip/errors.js +37 -0
- package/dist/zip/errors.js.map +1 -0
- package/dist/zip/index.d.ts +18 -0
- package/dist/zip/index.d.ts.map +1 -0
- package/dist/zip/index.js +78 -0
- package/dist/zip/index.js.map +1 -0
- package/dist/zip/inflate.d.ts +2 -0
- package/dist/zip/inflate.d.ts.map +1 -0
- package/dist/zip/inflate.js +43 -0
- package/dist/zip/inflate.js.map +1 -0
- package/dist/zip/source.d.ts +15 -0
- package/dist/zip/source.d.ts.map +1 -0
- package/dist/zip/source.js +34 -0
- package/dist/zip/source.js.map +1 -0
- package/package.json +75 -0
- package/src/core/book.ts +80 -0
- package/src/core/errors.ts +31 -0
- package/src/core/format.ts +14 -0
- package/src/core/graphemes.ts +73 -0
- package/src/core/index.ts +27 -0
- package/src/core/lookup.ts +16 -0
- package/src/core/open.ts +31 -0
- package/src/core/position.ts +485 -0
- package/src/core/reading-text.ts +200 -0
- package/src/core/source.ts +61 -0
- package/src/core/storage.ts +6 -0
- package/src/core/text.ts +35 -0
- package/src/formats/epub/href.ts +54 -0
- package/src/formats/epub/index.ts +235 -0
- package/src/formats/epub/opf.ts +143 -0
- package/src/formats/epub/toc.ts +120 -0
- package/src/formats/fb2/index.ts +348 -0
- package/src/formats/index.ts +3 -0
- package/src/formats/text/index.ts +208 -0
- package/src/formats/xml.ts +326 -0
- package/src/index.ts +26 -0
- package/src/layout/chunk.ts +169 -0
- package/src/layout/index.ts +573 -0
- package/src/reader/index.ts +1206 -0
- package/src/reader/input.ts +122 -0
- package/src/search/extract.ts +403 -0
- package/src/search/index.ts +10 -0
- package/src/search/matcher.ts +187 -0
- package/src/search/normalize.ts +122 -0
- package/src/view/allowlist.ts +273 -0
- package/src/view/appearance.ts +380 -0
- package/src/view/css.ts +172 -0
- package/src/view/frame.ts +1018 -0
- package/src/view/host.ts +527 -0
- package/src/view/index.ts +24 -0
- package/src/view/protocol.ts +412 -0
- package/src/view/reference.ts +30 -0
- package/src/view/resources.ts +265 -0
- package/src/view/sanitize.ts +266 -0
- package/src/zip/central.ts +85 -0
- package/src/zip/crc32.ts +30 -0
- package/src/zip/eocd.ts +97 -0
- package/src/zip/errors.ts +41 -0
- package/src/zip/index.ts +114 -0
- package/src/zip/inflate.ts +41 -0
- package/src/zip/source.ts +51 -0
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
import { escapeXmlAttribute } from '../core/text.js';
|
|
2
|
+
export const CONTENT_ROOT_ID = 'wolfy-reader-content';
|
|
3
|
+
/** Class marking each per-chunk container the host emits into the frame body. */
|
|
4
|
+
export const CHUNK_CLASS = 'wolfy-reader-chunk';
|
|
5
|
+
/** Cumulative character offset of a chunk's first glyph into the section text. */
|
|
6
|
+
export const CHUNK_START_ATTR = 'data-chunk-start';
|
|
7
|
+
/** Cumulative character offset just past a chunk's last glyph. */
|
|
8
|
+
export const CHUNK_END_ATTR = 'data-chunk-end';
|
|
9
|
+
/** Zero-based index of a chunk in section order. */
|
|
10
|
+
export const CHUNK_INDEX_ATTR = 'data-chunk-index';
|
|
11
|
+
/** Class every draw-only decoration overlay box carries, alongside the caller's class. */
|
|
12
|
+
export const DECORATION_CLASS = 'wolfy-reader-decoration';
|
|
13
|
+
/**
|
|
14
|
+
* A minimal reset and nothing else. Column geometry belongs to the paginator
|
|
15
|
+
* and typography to the appearance controls; the host's job is to stay out of
|
|
16
|
+
* the way of both, so it must not put the body in a column context.
|
|
17
|
+
*/
|
|
18
|
+
const RESET_CSS = [
|
|
19
|
+
'html{box-sizing:border-box}',
|
|
20
|
+
'*,*::before,*::after{box-sizing:inherit}',
|
|
21
|
+
'body{margin:0}',
|
|
22
|
+
'img,svg{max-width:100%;height:auto}',
|
|
23
|
+
// A draw-only decoration overlay: pointer-transparent and given a visible default
|
|
24
|
+
// so a bare `decorate` shows without host CSS. It carries the caller's class too, so
|
|
25
|
+
// an appearance stylesheet can restyle it; a host that wants a different look targets
|
|
26
|
+
// its own class. The default is deliberately mild (translucent), below the text.
|
|
27
|
+
`.${DECORATION_CLASS}{pointer-events:none;background:rgba(255,214,0,0.4);border-radius:2px;mix-blend-mode:multiply}`,
|
|
28
|
+
].join('');
|
|
29
|
+
/**
|
|
30
|
+
* Delivered as a meta element because a library with no server cannot set a
|
|
31
|
+
* header. That costs the three header-only directives — `frame-ancestors`,
|
|
32
|
+
* `sandbox` and `report-uri` — and means the element must be the first thing in
|
|
33
|
+
* the document, since nothing before it is covered.
|
|
34
|
+
*
|
|
35
|
+
* `script-src` names a per-render nonce rather than allowing inline script, so
|
|
36
|
+
* a script that somehow survived sanitization still cannot run: the nonce is
|
|
37
|
+
* unguessable and unreadable by anything that is not already executing. That
|
|
38
|
+
* also blocks `javascript:` URLs, which need `'unsafe-inline'` to run.
|
|
39
|
+
*
|
|
40
|
+
* `data:` is the only source of content because the frame's opaque origin is
|
|
41
|
+
* refused the host's `blob:` URLs. It grants no reach — a data URL carries its
|
|
42
|
+
* own bytes and fetches nothing — so egress stays closed.
|
|
43
|
+
*/
|
|
44
|
+
function contentSecurityPolicy(nonce) {
|
|
45
|
+
return [
|
|
46
|
+
"default-src 'none'",
|
|
47
|
+
`script-src 'nonce-${nonce}'`,
|
|
48
|
+
"style-src 'unsafe-inline' data:",
|
|
49
|
+
'img-src data:',
|
|
50
|
+
'font-src data:',
|
|
51
|
+
"connect-src 'none'",
|
|
52
|
+
"form-action 'none'",
|
|
53
|
+
"base-uri 'none'",
|
|
54
|
+
].join('; ');
|
|
55
|
+
}
|
|
56
|
+
export function createNonce() {
|
|
57
|
+
const bytes = new Uint8Array(16);
|
|
58
|
+
crypto.getRandomValues(bytes);
|
|
59
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
60
|
+
}
|
|
61
|
+
export function coordinationScript(hostOrigin, keyboardNav = true) {
|
|
62
|
+
return `(function(){
|
|
63
|
+
'use strict';
|
|
64
|
+
var VERSION = 9;
|
|
65
|
+
var host = window.parent;
|
|
66
|
+
var target = ${JSON.stringify(hostOrigin)};
|
|
67
|
+
// Whether the host acts on forwarded nav keys. Baked in at document assembly
|
|
68
|
+
// (like the theme stylesheet), never sent over the wire, so it is not a
|
|
69
|
+
// protocol field and needs no version bump. It gates only preventDefault —
|
|
70
|
+
// forwarding stays unconditional and the host ignores what it has disabled.
|
|
71
|
+
var KEYBOARD_NAV = ${JSON.stringify(keyboardNav)};
|
|
72
|
+
var ROOT_ID = ${JSON.stringify(CONTENT_ROOT_ID)};
|
|
73
|
+
var CHUNK_CLASS = ${JSON.stringify(CHUNK_CLASS)};
|
|
74
|
+
var START_ATTR = ${JSON.stringify(CHUNK_START_ATTR)};
|
|
75
|
+
var END_ATTR = ${JSON.stringify(CHUNK_END_ATTR)};
|
|
76
|
+
function send(message){ try { host.postMessage(message, target); } catch (error) {} }
|
|
77
|
+
|
|
78
|
+
// Hand-maintained copy of asHostMessage from protocol.ts. Kept in step by hand
|
|
79
|
+
// because this script is a string template and cannot import. See protocol.ts.
|
|
80
|
+
function validateHost(data){
|
|
81
|
+
if (data === null || typeof data !== 'object' || data.v !== VERSION) return null;
|
|
82
|
+
if (typeof data.id !== 'number') return null;
|
|
83
|
+
var t = data.type;
|
|
84
|
+
if (t === 'ping' || t === 'measure' || t === 'relayout' || t === 'sectionText' || t === 'diagnostics') return data;
|
|
85
|
+
if (t === 'paginate') {
|
|
86
|
+
var o = data.options;
|
|
87
|
+
if (o === null || typeof o !== 'object') return null;
|
|
88
|
+
if (o.mode !== 'paginated' && o.mode !== 'scrolled') return null;
|
|
89
|
+
if (typeof o.pageWidth !== 'number' || typeof o.pageHeight !== 'number') return null;
|
|
90
|
+
if (typeof o.columnGap !== 'number' || typeof o.chunkChars !== 'number') return null;
|
|
91
|
+
if (typeof o.windowChunks !== 'number' || typeof o.columnCount !== 'number') return null;
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
if (t === 'goToPage' || t === 'offsetOfPage') return typeof data.page === 'number' ? data : null;
|
|
95
|
+
if (t === 'pageOfOffset') return typeof data.offset === 'number' ? data : null;
|
|
96
|
+
if (t === 'offsetOfElementId') return typeof data.elementId === 'string' ? data : null;
|
|
97
|
+
if (t === 'decorate') {
|
|
98
|
+
if (typeof data.decorationId !== 'string' || typeof data.className !== 'string') return null;
|
|
99
|
+
if (typeof data.start !== 'number' || typeof data.end !== 'number') return null;
|
|
100
|
+
return data;
|
|
101
|
+
}
|
|
102
|
+
if (t === 'undecorate') return typeof data.decorationId === 'string' ? data : null;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Live pagination state, rebuilt on paginate and re-measured on relayout.
|
|
107
|
+
var layout = {
|
|
108
|
+
mode: 'paginated',
|
|
109
|
+
pageWidth: 0,
|
|
110
|
+
pageHeight: 0,
|
|
111
|
+
columnGap: 0,
|
|
112
|
+
columnCount: 1,
|
|
113
|
+
windowChunks: 2,
|
|
114
|
+
chunks: [], // { el, index, start, end, top, height, pages, firstPage }
|
|
115
|
+
pageCount: 0,
|
|
116
|
+
contentWidth: 0,
|
|
117
|
+
contentHeight: 0,
|
|
118
|
+
firm: false
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function chunkContainers(){
|
|
122
|
+
var root = document.getElementById(ROOT_ID) || document.body;
|
|
123
|
+
var found = root.getElementsByClassName(CHUNK_CLASS);
|
|
124
|
+
var list = [];
|
|
125
|
+
for (var i = 0; i < found.length; i++) list.push(found[i]);
|
|
126
|
+
return list;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function numAttr(el, name){
|
|
130
|
+
var v = parseInt(el.getAttribute(name) || '', 10);
|
|
131
|
+
return isNaN(v) ? 0 : v;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Measure one chunk's laid-out content extent. Each chunk is its own
|
|
135
|
+
// multi-column formatting context, so its horizontal content width (scrollWidth)
|
|
136
|
+
// divided by the page stride yields its page count. Range rects length-checked.
|
|
137
|
+
function measureChunkWidth(el){
|
|
138
|
+
// scrollWidth of a multi-column box grows with the number of columns, so it is
|
|
139
|
+
// the total painted width. Guard against a zero-width container.
|
|
140
|
+
var w = el.scrollWidth;
|
|
141
|
+
if (w > 0) return w;
|
|
142
|
+
var range = document.createRange();
|
|
143
|
+
try {
|
|
144
|
+
range.selectNodeContents(el);
|
|
145
|
+
var rects = range.getClientRects();
|
|
146
|
+
if (rects.length === 0) return 0;
|
|
147
|
+
var min = Infinity, max = -Infinity;
|
|
148
|
+
for (var i = 0; i < rects.length; i++){
|
|
149
|
+
if (rects[i].left < min) min = rects[i].left;
|
|
150
|
+
if (rects[i].right > max) max = rects[i].right;
|
|
151
|
+
}
|
|
152
|
+
return max > min ? (max - min) : 0;
|
|
153
|
+
} finally {
|
|
154
|
+
range.detach && range.detach();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The width of a single CSS column. columnGap is the reader's one spacing unit:
|
|
159
|
+
// it is the page-edge margin AND the gutter between columns, so a page reads
|
|
160
|
+
// margin, col, margin, col, ..., margin. The text area is the page minus its two
|
|
161
|
+
// edge margins; N columns and (N-1) inter-column gaps then share that inner width.
|
|
162
|
+
function colWidth(){
|
|
163
|
+
var n = layout.columnCount > 0 ? layout.columnCount : 1;
|
|
164
|
+
var inner = layout.pageWidth - 2 * layout.columnGap;
|
|
165
|
+
if (n <= 1) return inner;
|
|
166
|
+
return (inner - (n - 1) * layout.columnGap) / n;
|
|
167
|
+
}
|
|
168
|
+
// The horizontal distance one page-turn translates the active chunk: N column
|
|
169
|
+
// strides. Each column stride is its width plus the gap that follows it, so N
|
|
170
|
+
// columns advance by N * (colWidth + columnGap). With the edge-margin geometry this
|
|
171
|
+
// equals pageWidth - columnGap, which lands every page's columns inside the same
|
|
172
|
+
// left/right margins — the mapping math (offsetOfPage/pageOfOffset) reads live box
|
|
173
|
+
// positions and this stride, so it needs no edge-margin term of its own.
|
|
174
|
+
function stride(){
|
|
175
|
+
var n = layout.columnCount > 0 ? layout.columnCount : 1;
|
|
176
|
+
return n * (colWidth() + layout.columnGap);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// The chunk (and its local page) that paints a given global page, or null. Shared
|
|
180
|
+
// by every page→chunk seam so the [firstPage, firstPage+pages) test lives once.
|
|
181
|
+
function chunkAtPage(page){
|
|
182
|
+
for (var i = 0; i < layout.chunks.length; i++){
|
|
183
|
+
var ch = layout.chunks[i];
|
|
184
|
+
if (page >= ch.firstPage && page < ch.firstPage + ch.pages) return ch;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The chunk covering a section-text character offset, or null.
|
|
190
|
+
function chunkAtOffset(offset){
|
|
191
|
+
for (var i = 0; i < layout.chunks.length; i++){
|
|
192
|
+
var ch = layout.chunks[i];
|
|
193
|
+
if (offset >= ch.start && offset < ch.end) return ch;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Realize a chunk so a Range can measure its real text even if it was evicted
|
|
199
|
+
// (content-visibility:auto skips layout for off-screen chunks). Every measuring
|
|
200
|
+
// seam calls this rather than poking the style ad hoc.
|
|
201
|
+
function realize(el){ el.style.contentVisibility = 'visible'; }
|
|
202
|
+
|
|
203
|
+
// (Re)apply geometry to every chunk container and recompute the page map. In
|
|
204
|
+
// paginated mode each chunk is an absolutely-positioned multi-column context and
|
|
205
|
+
// contributes ceil(contentWidth / pageStride) pages; boundaries are forced page
|
|
206
|
+
// breaks so per-chunk page counts simply sum. In scrolled mode chunks stack
|
|
207
|
+
// vertically in normal flow with no paging.
|
|
208
|
+
function relayout(){
|
|
209
|
+
var els = chunkContainers();
|
|
210
|
+
layout.chunks = [];
|
|
211
|
+
var root = document.getElementById(ROOT_ID) || document.body;
|
|
212
|
+
if (layout.mode === 'scrolled'){
|
|
213
|
+
// The document scrolls vertically to read, so restore that; but clip the
|
|
214
|
+
// horizontal axis so an over-wide element (a broad table, a long pre) cannot add
|
|
215
|
+
// a stray sideways scrollbar to what is a vertical reading surface.
|
|
216
|
+
document.documentElement.style.overflow = '';
|
|
217
|
+
document.body.style.overflow = '';
|
|
218
|
+
document.documentElement.style.overflowX = 'hidden';
|
|
219
|
+
document.body.style.overflowX = 'hidden';
|
|
220
|
+
root.style.position = '';
|
|
221
|
+
root.style.width = layout.pageWidth ? (layout.pageWidth + 'px') : '';
|
|
222
|
+
root.style.height = '';
|
|
223
|
+
var top = 0;
|
|
224
|
+
for (var i = 0; i < els.length; i++){
|
|
225
|
+
var el = els[i];
|
|
226
|
+
el.style.position = 'static';
|
|
227
|
+
el.style.columnWidth = 'auto';
|
|
228
|
+
el.style.width = layout.pageWidth ? (layout.pageWidth + 'px') : 'auto';
|
|
229
|
+
// Scrolled text gets the same left/right edge margins as a page (box-sizing is
|
|
230
|
+
// border-box, so the padding eats into pageWidth rather than overflowing it).
|
|
231
|
+
el.style.paddingLeft = layout.columnGap + 'px';
|
|
232
|
+
el.style.paddingRight = layout.columnGap + 'px';
|
|
233
|
+
el.style.height = 'auto';
|
|
234
|
+
el.style.contentVisibility = '';
|
|
235
|
+
var h = el.offsetHeight;
|
|
236
|
+
layout.chunks.push({
|
|
237
|
+
el: el, index: numAttr(el, ${JSON.stringify(CHUNK_INDEX_ATTR)}),
|
|
238
|
+
start: numAttr(el, START_ATTR), end: numAttr(el, END_ATTR),
|
|
239
|
+
top: top, height: h, pages: 0, firstPage: 0
|
|
240
|
+
});
|
|
241
|
+
top += h;
|
|
242
|
+
}
|
|
243
|
+
layout.pageCount = 0;
|
|
244
|
+
layout.contentWidth = layout.pageWidth;
|
|
245
|
+
layout.contentHeight = top;
|
|
246
|
+
layout.firm = true;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Paginated. The root box is the page viewport (overflow:hidden). The document
|
|
250
|
+
// must never scroll: each chunk is a multi-column context whose later columns lay
|
|
251
|
+
// out far to the right and a page turn translates them into view, so without this
|
|
252
|
+
// the off-page columns give html/body a wide scrollWidth and a stray scrollbar.
|
|
253
|
+
document.documentElement.style.overflow = 'hidden';
|
|
254
|
+
document.body.style.overflow = 'hidden';
|
|
255
|
+
root.style.position = 'relative';
|
|
256
|
+
root.style.width = layout.pageWidth + 'px';
|
|
257
|
+
root.style.height = layout.pageHeight + 'px';
|
|
258
|
+
root.style.overflow = 'hidden';
|
|
259
|
+
var pageStride = stride();
|
|
260
|
+
var pageAcc = 0;
|
|
261
|
+
var maxWidth = 0;
|
|
262
|
+
for (var j = 0; j < els.length; j++){
|
|
263
|
+
var c = els[j];
|
|
264
|
+
c.style.position = 'absolute';
|
|
265
|
+
c.style.top = '0';
|
|
266
|
+
// Inset the chunk by one margin so every page shows a left edge margin; the
|
|
267
|
+
// width is the page minus both edge margins, so a right margin falls out too.
|
|
268
|
+
// The edge margin here is positional (left/width), not padding — a chunk that
|
|
269
|
+
// was scrolled carries padding, which must be cleared so it is not inset twice.
|
|
270
|
+
c.style.left = layout.columnGap + 'px';
|
|
271
|
+
c.style.paddingLeft = '0';
|
|
272
|
+
c.style.paddingRight = '0';
|
|
273
|
+
c.style.height = layout.pageHeight + 'px';
|
|
274
|
+
// Each chunk is its own multi-column context. The CSS column width is one
|
|
275
|
+
// reader column (a fraction of the inner width for a 2-column layout); the page
|
|
276
|
+
// turn translates by stride() (N columns), so a page paints N columns at once.
|
|
277
|
+
c.style.columnWidth = colWidth() + 'px';
|
|
278
|
+
c.style.columnGap = layout.columnGap + 'px';
|
|
279
|
+
c.style.columnFill = 'auto';
|
|
280
|
+
c.style.width = (layout.pageWidth - 2 * layout.columnGap) + 'px';
|
|
281
|
+
// The chunk must NOT clip: its multi-column overflow (columns 2, 3, …) lays
|
|
282
|
+
// out to the right, and a page turn reveals a later column by translating the
|
|
283
|
+
// whole chunk left. A clip here would travel with the box and paint only the
|
|
284
|
+
// first column — every page after the first would be blank. The root box owns
|
|
285
|
+
// the viewport clip; the chunk stays visible so its columns can scroll in.
|
|
286
|
+
c.style.overflow = 'visible';
|
|
287
|
+
c.style.contentVisibility = 'visible';
|
|
288
|
+
var cw = measureChunkWidth(c);
|
|
289
|
+
var pages = cw <= 0 ? 1 : Math.max(1, Math.ceil((cw + layout.columnGap) / pageStride));
|
|
290
|
+
if (cw > maxWidth) maxWidth = cw;
|
|
291
|
+
layout.chunks.push({
|
|
292
|
+
el: c, index: numAttr(c, ${JSON.stringify(CHUNK_INDEX_ATTR)}),
|
|
293
|
+
start: numAttr(c, START_ATTR), end: numAttr(c, END_ATTR),
|
|
294
|
+
top: 0, height: layout.pageHeight, pages: pages, firstPage: pageAcc
|
|
295
|
+
});
|
|
296
|
+
pageAcc += pages;
|
|
297
|
+
}
|
|
298
|
+
layout.pageCount = pageAcc;
|
|
299
|
+
layout.contentWidth = maxWidth;
|
|
300
|
+
layout.contentHeight = layout.pageHeight;
|
|
301
|
+
layout.firm = true;
|
|
302
|
+
applyWindow(currentPage());
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// content-visibility:auto on chunks outside the retained window. Geometry stays
|
|
306
|
+
// recorded in layout.chunks, so page numbers do not shift when a chunk is
|
|
307
|
+
// un-realized; it re-lays-out on return.
|
|
308
|
+
var activeChunk = 0;
|
|
309
|
+
function currentPage(){ return activeChunk; }
|
|
310
|
+
function applyWindow(centerPage){
|
|
311
|
+
var center = 0;
|
|
312
|
+
for (var i = 0; i < layout.chunks.length; i++){
|
|
313
|
+
var ch = layout.chunks[i];
|
|
314
|
+
if (centerPage >= ch.firstPage && centerPage < ch.firstPage + ch.pages){ center = i; break; }
|
|
315
|
+
}
|
|
316
|
+
activeChunk = center;
|
|
317
|
+
for (var k = 0; k < layout.chunks.length; k++){
|
|
318
|
+
var chunk = layout.chunks[k];
|
|
319
|
+
var near = Math.abs(k - center) <= layout.windowChunks;
|
|
320
|
+
chunk.el.style.contentVisibility = near ? 'visible' : 'auto';
|
|
321
|
+
chunk.el.style.containIntrinsicSize = near ? '' :
|
|
322
|
+
(layout.pageWidth + 'px ' + layout.pageHeight + 'px');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function realizedCount(){
|
|
327
|
+
var n = 0;
|
|
328
|
+
for (var i = 0; i < layout.chunks.length; i++){
|
|
329
|
+
if (layout.chunks[i].el.style.contentVisibility !== 'auto') n++;
|
|
330
|
+
}
|
|
331
|
+
return n;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function paginationState(){
|
|
335
|
+
return {
|
|
336
|
+
pageCount: layout.pageCount,
|
|
337
|
+
firm: layout.firm,
|
|
338
|
+
realizedChunks: realizedCount(),
|
|
339
|
+
totalChunks: layout.chunks.length,
|
|
340
|
+
contentWidth: layout.contentWidth,
|
|
341
|
+
contentHeight: layout.contentHeight
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Position the visible window so that page number \`page\` (0-based) is shown. Each
|
|
346
|
+
// chunk paints its own columns from its own left origin, so scrolling a page
|
|
347
|
+
// means translating the active chunk left by the intra-chunk column stride and
|
|
348
|
+
// hiding the others.
|
|
349
|
+
function goToPage(page){
|
|
350
|
+
if (layout.pageCount <= 0) return 0;
|
|
351
|
+
if (page < 0) page = 0;
|
|
352
|
+
if (page >= layout.pageCount) page = layout.pageCount - 1;
|
|
353
|
+
var target = chunkAtPage(page);
|
|
354
|
+
var localPage = target === null ? 0 : page - target.firstPage;
|
|
355
|
+
applyWindow(page);
|
|
356
|
+
for (var j = 0; j < layout.chunks.length; j++){
|
|
357
|
+
var c = layout.chunks[j];
|
|
358
|
+
if (c === target){
|
|
359
|
+
c.el.style.visibility = 'visible';
|
|
360
|
+
c.el.style.transform = 'translateX(' + (-(localPage * stride())) + 'px)';
|
|
361
|
+
} else {
|
|
362
|
+
c.el.style.visibility = 'hidden';
|
|
363
|
+
c.el.style.transform = '';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return page;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Map a page (0-based) to the character offset of its first painted glyph.
|
|
370
|
+
// Column flow lays text out monotonically: the column — and therefore the page
|
|
371
|
+
// band — a character paints in never decreases as its text offset grows. So the
|
|
372
|
+
// first character of a page is found by binary search over the chunk's text
|
|
373
|
+
// (~log n getClientRects probes over <= ~8000 chars), never one probe per
|
|
374
|
+
// character. Characters with no client rects (zero-width anchors, collapsed
|
|
375
|
+
// whitespace — the empty-rect gotcha) are skipped by scanning forward to the
|
|
376
|
+
// next measurable character inside each probe.
|
|
377
|
+
function offsetOfPage(page){
|
|
378
|
+
if (page < 0 || page >= layout.pageCount) return -1;
|
|
379
|
+
var chunk = chunkAtPage(page);
|
|
380
|
+
if (chunk === null) return -1;
|
|
381
|
+
realize(chunk.el);
|
|
382
|
+
var localPage = page - chunk.firstPage;
|
|
383
|
+
var boxLeft = chunk.el.getBoundingClientRect().left;
|
|
384
|
+
var pageStride = stride();
|
|
385
|
+
// Collect the chunk's text nodes once, with cumulative start offsets, so a
|
|
386
|
+
// probe maps a chunk-local character index to (node, intra-node offset) by
|
|
387
|
+
// binary search rather than re-walking the tree.
|
|
388
|
+
var walker = document.createTreeWalker(chunk.el, NodeFilter.SHOW_TEXT, null);
|
|
389
|
+
var nodes = [], starts = [], total = 0, node;
|
|
390
|
+
while ((node = walker.nextNode())){
|
|
391
|
+
nodes.push(node); starts.push(total); total += node.data.length;
|
|
392
|
+
}
|
|
393
|
+
if (total === 0) return chunk.start;
|
|
394
|
+
var range = document.createRange();
|
|
395
|
+
// The local page band painting the character at chunk-local index i, or -1
|
|
396
|
+
// when the character has no client rects. The +1 tolerates sub-pixel
|
|
397
|
+
// rounding, mirroring the old band check's bandLeft - 1.
|
|
398
|
+
function pageAt(i){
|
|
399
|
+
var lo = 0, hi = nodes.length - 1;
|
|
400
|
+
while (lo < hi){
|
|
401
|
+
var mid = (lo + hi + 1) >> 1;
|
|
402
|
+
if (starts[mid] <= i) lo = mid; else hi = mid - 1;
|
|
403
|
+
}
|
|
404
|
+
range.setStart(nodes[lo], i - starts[lo]);
|
|
405
|
+
range.setEnd(nodes[lo], i - starts[lo] + 1);
|
|
406
|
+
var rects = range.getClientRects();
|
|
407
|
+
if (rects.length === 0) return -1;
|
|
408
|
+
return Math.floor((rects[0].left - boxLeft + 1) / pageStride);
|
|
409
|
+
}
|
|
410
|
+
// First measurable character at index >= i (exclusive bound limit), as
|
|
411
|
+
// { index, page }, or null when every character in [i, limit) is unmeasurable.
|
|
412
|
+
function probeFrom(i, limit){
|
|
413
|
+
for (var j = i; j < limit; j++){
|
|
414
|
+
var pg = pageAt(j);
|
|
415
|
+
if (pg >= 0) return { index: j, page: pg };
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
// Binary-search the smallest measurable index whose page is >= localPage;
|
|
420
|
+
// monotonicity of page-per-offset is what makes the halving sound.
|
|
421
|
+
var low = 0, high = total, best = -1, bestPage = -1;
|
|
422
|
+
while (low < high){
|
|
423
|
+
var midpoint = (low + high) >> 1;
|
|
424
|
+
var probe = probeFrom(midpoint, high);
|
|
425
|
+
if (probe === null){
|
|
426
|
+
high = midpoint;
|
|
427
|
+
} else if (probe.page >= localPage){
|
|
428
|
+
best = probe.index;
|
|
429
|
+
bestPage = probe.page;
|
|
430
|
+
high = probe.index;
|
|
431
|
+
} else {
|
|
432
|
+
low = probe.index + 1;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// A page whose band holds no measurable character keeps the old fallback: the
|
|
436
|
+
// chunk's own start offset.
|
|
437
|
+
if (best >= 0 && bestPage === localPage) return chunk.start + best;
|
|
438
|
+
return chunk.start;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Inverse: which page paints the glyph at this section-text offset.
|
|
442
|
+
function pageOfOffset(offset){
|
|
443
|
+
var chunk = chunkAtOffset(offset);
|
|
444
|
+
if (chunk === null){
|
|
445
|
+
// Past the end: last page. Before the start: first page.
|
|
446
|
+
if (layout.chunks.length === 0) return 0;
|
|
447
|
+
return offset < layout.chunks[0].start ? 0 : layout.pageCount - 1;
|
|
448
|
+
}
|
|
449
|
+
realize(chunk.el);
|
|
450
|
+
var local = offset - chunk.start;
|
|
451
|
+
var walker = document.createTreeWalker(chunk.el, NodeFilter.SHOW_TEXT, null);
|
|
452
|
+
var acc = 0, node;
|
|
453
|
+
var boxLeft = chunk.el.getBoundingClientRect().left;
|
|
454
|
+
while ((node = walker.nextNode())){
|
|
455
|
+
var len = node.data.length;
|
|
456
|
+
if (local < acc + len){
|
|
457
|
+
var p = local - acc;
|
|
458
|
+
var range = document.createRange();
|
|
459
|
+
range.setStart(node, p);
|
|
460
|
+
range.setEnd(node, Math.min(p + 1, len));
|
|
461
|
+
var rects = range.getClientRects();
|
|
462
|
+
if (rects.length > 0){
|
|
463
|
+
var left = rects[0].left - boxLeft;
|
|
464
|
+
var localPage = Math.max(0, Math.floor(left / stride()));
|
|
465
|
+
return chunk.firstPage + Math.min(localPage, chunk.pages - 1);
|
|
466
|
+
}
|
|
467
|
+
return chunk.firstPage;
|
|
468
|
+
}
|
|
469
|
+
acc += len;
|
|
470
|
+
}
|
|
471
|
+
return chunk.firstPage;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Map an element id to its character offset into the section's concatenated text.
|
|
475
|
+
// Find the element, walk up to its enclosing chunk container, then measure the
|
|
476
|
+
// text length from the chunk container's start to just before the element with a
|
|
477
|
+
// Range: range.toString().length added to the chunk's cumulative start offset is
|
|
478
|
+
// the offset. Returns -1 when no element carries the id.
|
|
479
|
+
function offsetOfElementId(elementId){
|
|
480
|
+
var el = document.getElementById(elementId);
|
|
481
|
+
if (el === null) return -1;
|
|
482
|
+
// Walk up to the enclosing chunk container.
|
|
483
|
+
var chunkEl = el;
|
|
484
|
+
while (chunkEl && !(chunkEl.nodeType === 1 && chunkEl.className &&
|
|
485
|
+
(' ' + chunkEl.className + ' ').indexOf(' ' + CHUNK_CLASS + ' ') !== -1)){
|
|
486
|
+
chunkEl = chunkEl.parentNode;
|
|
487
|
+
}
|
|
488
|
+
if (!chunkEl || chunkEl.nodeType !== 1) return -1;
|
|
489
|
+
// The element (or its chunk) may be un-realized; force it visible so the Range
|
|
490
|
+
// measures real text, mirroring offsetOfPage/pageOfOffset.
|
|
491
|
+
realize(chunkEl);
|
|
492
|
+
var chunkStart = numAttr(chunkEl, START_ATTR);
|
|
493
|
+
var range = document.createRange();
|
|
494
|
+
try {
|
|
495
|
+
range.setStart(chunkEl, 0);
|
|
496
|
+
range.setEndBefore(el);
|
|
497
|
+
// Touch the rects (length-checked) so the layout is up to date, matching the
|
|
498
|
+
// other seams; the offset itself comes from the range's text length.
|
|
499
|
+
var rects = range.getClientRects();
|
|
500
|
+
void (rects.length > 0);
|
|
501
|
+
return chunkStart + range.toString().length;
|
|
502
|
+
} finally {
|
|
503
|
+
range.detach && range.detach();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function sectionText(){
|
|
508
|
+
var text = '';
|
|
509
|
+
if (layout.chunks.length > 0){
|
|
510
|
+
for (var i = 0; i < layout.chunks.length; i++) text += layout.chunks[i].el.textContent || '';
|
|
511
|
+
} else {
|
|
512
|
+
// Before the first paginate the layout is empty; read the containers directly.
|
|
513
|
+
var els = chunkContainers();
|
|
514
|
+
for (var j = 0; j < els.length; j++) text += els[j].textContent || '';
|
|
515
|
+
}
|
|
516
|
+
return text;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function applyOptions(o){
|
|
520
|
+
layout.mode = o.mode;
|
|
521
|
+
layout.pageWidth = o.pageWidth;
|
|
522
|
+
layout.pageHeight = o.pageHeight;
|
|
523
|
+
layout.columnGap = o.columnGap;
|
|
524
|
+
layout.columnCount = o.columnCount > 0 ? o.columnCount : 1;
|
|
525
|
+
layout.windowChunks = o.windowChunks;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Draw-only decorations. The host resolves a Position to a UTF-16 offset range over
|
|
529
|
+
// the tiled section text and sends it here; the frame maps that range to a DOM Range,
|
|
530
|
+
// reads its client rects, and paints one absolutely-positioned overlay box per rect.
|
|
531
|
+
// The library only DRAWS — it holds no annotation data beyond the live intent needed
|
|
532
|
+
// to re-paint across a re-layout, and never persists or fetches. Overlays are
|
|
533
|
+
// pointer-transparent (must not eat a selection or a link click) and layout-neutral
|
|
534
|
+
// (appended inside the chunk container, absolutely positioned, so they change no page
|
|
535
|
+
// geometry and translate with the chunk on a page turn).
|
|
536
|
+
var decorations = {}; // decorationId -> { start, end, className }
|
|
537
|
+
var decorationBoxes = {}; // decorationId -> [box elements]
|
|
538
|
+
var DECORATION_CLASS = ${JSON.stringify(DECORATION_CLASS)};
|
|
539
|
+
|
|
540
|
+
// Find the (node, offset) DOM point for a global UTF-16 offset into the concatenated
|
|
541
|
+
// chunk-container text — the inverse of offsetOfPoint used by selection. Walks the
|
|
542
|
+
// chunk containers, accumulating live textContent length, and stops at the node that
|
|
543
|
+
// spans the offset. textContent is DOM, not layout, so the accumulation walk must not
|
|
544
|
+
// realize the chunks it merely passes over — only the chunk that owns the offset is
|
|
545
|
+
// realized (the caller needs its client rects), so a decoration in a late chunk
|
|
546
|
+
// leaves the content-visibility eviction window intact. Returns null when the offset
|
|
547
|
+
// is past the realized text (a soft-miss the caller draws nothing for).
|
|
548
|
+
function pointAtOffset(target){
|
|
549
|
+
var chunks = chunkContainers();
|
|
550
|
+
var acc = 0;
|
|
551
|
+
for (var i = 0; i < chunks.length; i++){
|
|
552
|
+
var el = chunks[i];
|
|
553
|
+
var len = (el.textContent || '').length;
|
|
554
|
+
if (target <= acc + len){
|
|
555
|
+
realize(el);
|
|
556
|
+
var local = target - acc;
|
|
557
|
+
var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
|
|
558
|
+
var nodeAcc = 0, node;
|
|
559
|
+
while ((node = walker.nextNode())){
|
|
560
|
+
var nlen = node.data.length;
|
|
561
|
+
if (local <= nodeAcc + nlen){
|
|
562
|
+
return { chunk: el, node: node, offset: local - nodeAcc };
|
|
563
|
+
}
|
|
564
|
+
nodeAcc += nlen;
|
|
565
|
+
}
|
|
566
|
+
// Offset lands exactly at the chunk's end with no trailing text node.
|
|
567
|
+
return { chunk: el, node: el, offset: el.childNodes.length };
|
|
568
|
+
}
|
|
569
|
+
acc += len;
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Erase every overlay box painted for a decoration id.
|
|
575
|
+
function removeDecorationBoxes(decorationId){
|
|
576
|
+
var boxes = decorationBoxes[decorationId];
|
|
577
|
+
if (boxes){
|
|
578
|
+
for (var i = 0; i < boxes.length; i++){
|
|
579
|
+
if (boxes[i].parentNode) boxes[i].parentNode.removeChild(boxes[i]);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
decorationBoxes[decorationId] = [];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Paint one overlay box per client rect of the decoration's offset range. Boxes are
|
|
586
|
+
// appended inside the range's start chunk container (the positioned, page-turn-
|
|
587
|
+
// translated element) and positioned relative to it, so they ride the same transform
|
|
588
|
+
// as the text they cover. Guards an empty getClientRects() (the zero-width-anchor /
|
|
589
|
+
// empty-inline gotcha) — an empty rect list draws nothing, never a malformed box.
|
|
590
|
+
// Returns the number of boxes painted (0 on a soft-miss).
|
|
591
|
+
function paintDecoration(decorationId, start, end, className){
|
|
592
|
+
removeDecorationBoxes(decorationId);
|
|
593
|
+
if (end < start){ var swap = start; start = end; end = swap; }
|
|
594
|
+
var from = pointAtOffset(start);
|
|
595
|
+
var to = pointAtOffset(end);
|
|
596
|
+
if (from === null || to === null) return 0;
|
|
597
|
+
var range = document.createRange();
|
|
598
|
+
try {
|
|
599
|
+
range.setStart(from.node, from.offset);
|
|
600
|
+
range.setEnd(to.node, to.offset);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
return 0;
|
|
603
|
+
}
|
|
604
|
+
var rects = range.getClientRects();
|
|
605
|
+
if (rects.length === 0) return 0;
|
|
606
|
+
// The overlay boxes live inside the chunk that owns the range start, positioned in
|
|
607
|
+
// that chunk's coordinate space so a page-turn transform carries them along. Each
|
|
608
|
+
// box's absolute offset is the rect's viewport position minus the chunk's padding-
|
|
609
|
+
// box origin; both are measured post-transform, so the offset is transform-invariant
|
|
610
|
+
// and the box rides the same translateX the text does on a page turn.
|
|
611
|
+
var container = from.chunk;
|
|
612
|
+
var origin = container.getBoundingClientRect();
|
|
613
|
+
var boxes = [];
|
|
614
|
+
for (var i = 0; i < rects.length; i++){
|
|
615
|
+
var rect = rects[i];
|
|
616
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
617
|
+
var box = document.createElement('div');
|
|
618
|
+
box.className = DECORATION_CLASS + (className ? ' ' + className : '');
|
|
619
|
+
box.setAttribute('data-decoration', decorationId);
|
|
620
|
+
box.style.position = 'absolute';
|
|
621
|
+
box.style.pointerEvents = 'none';
|
|
622
|
+
box.style.left = (rect.left - origin.left) + 'px';
|
|
623
|
+
box.style.top = (rect.top - origin.top) + 'px';
|
|
624
|
+
box.style.width = rect.width + 'px';
|
|
625
|
+
box.style.height = rect.height + 'px';
|
|
626
|
+
container.appendChild(box);
|
|
627
|
+
boxes.push(box);
|
|
628
|
+
}
|
|
629
|
+
decorationBoxes[decorationId] = boxes;
|
|
630
|
+
return boxes.length;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Re-paint every live decoration. Called after a relayout / window shift so overlays
|
|
634
|
+
// track the text their offset ranges cover across a re-flow.
|
|
635
|
+
function repaintDecorations(){
|
|
636
|
+
for (var decorationId in decorations){
|
|
637
|
+
if (!Object.prototype.hasOwnProperty.call(decorations, decorationId)) continue;
|
|
638
|
+
var d = decorations[decorationId];
|
|
639
|
+
paintDecoration(decorationId, d.start, d.end, d.className);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
window.addEventListener('message', function(event){
|
|
644
|
+
if (event.source !== host) return;
|
|
645
|
+
var data = validateHost(event.data);
|
|
646
|
+
if (data === null) return;
|
|
647
|
+
var id = data.id;
|
|
648
|
+
if (data.type === 'ping') { send({ v: VERSION, type: 'pong', id: id }); return; }
|
|
649
|
+
if (data.type === 'measure') {
|
|
650
|
+
var root = document.getElementById(ROOT_ID) || document.documentElement;
|
|
651
|
+
send({ v: VERSION, type: 'measured', id: id, width: root.scrollWidth, height: root.scrollHeight });
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (data.type === 'paginate') { applyOptions(data.options); relayout(); repaintDecorations(); send({ v: VERSION, type: 'paginated', id: id, state: paginationState() }); return; }
|
|
655
|
+
if (data.type === 'relayout') { relayout(); repaintDecorations(); send({ v: VERSION, type: 'paginated', id: id, state: paginationState() }); return; }
|
|
656
|
+
if (data.type === 'goToPage') { var moved = goToPage(data.page); repaintDecorations(); send({ v: VERSION, type: 'movedToPage', id: id, page: moved }); return; }
|
|
657
|
+
if (data.type === 'offsetOfPage') { send({ v: VERSION, type: 'offset', id: id, offset: offsetOfPage(data.page) }); return; }
|
|
658
|
+
if (data.type === 'pageOfOffset') { send({ v: VERSION, type: 'page', id: id, page: pageOfOffset(data.offset) }); return; }
|
|
659
|
+
if (data.type === 'offsetOfElementId') { send({ v: VERSION, type: 'offset', id: id, offset: offsetOfElementId(data.elementId) }); return; }
|
|
660
|
+
if (data.type === 'sectionText') { send({ v: VERSION, type: 'text', id: id, text: sectionText() }); return; }
|
|
661
|
+
if (data.type === 'diagnostics') {
|
|
662
|
+
send({ v: VERSION, type: 'diagnosticsReport', id: id,
|
|
663
|
+
domNodes: document.getElementsByTagName('*').length,
|
|
664
|
+
realizedChunks: realizedCount(), totalChunks: layout.chunks.length });
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (data.type === 'decorate') {
|
|
668
|
+
decorations[data.decorationId] = { start: data.start, end: data.end, className: data.className };
|
|
669
|
+
var boxes = paintDecoration(data.decorationId, data.start, data.end, data.className);
|
|
670
|
+
send({ v: VERSION, type: 'decorated', id: id, boxes: boxes });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (data.type === 'undecorate') {
|
|
674
|
+
removeDecorationBoxes(data.decorationId);
|
|
675
|
+
delete decorationBoxes[data.decorationId];
|
|
676
|
+
delete decorations[data.decorationId];
|
|
677
|
+
send({ v: VERSION, type: 'decorated', id: id, boxes: 0 });
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
document.addEventListener('securitypolicyviolation', function(event){
|
|
682
|
+
send({
|
|
683
|
+
v: VERSION,
|
|
684
|
+
type: 'violation',
|
|
685
|
+
directive: String(event.effectiveDirective || event.violatedDirective || ''),
|
|
686
|
+
blockedUri: String(event.blockedURI || '')
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
window.addEventListener('error', function(event){
|
|
690
|
+
send({ v: VERSION, type: 'error', message: String((event && event.message) || 'error') });
|
|
691
|
+
});
|
|
692
|
+
// Walk up from a node to an enclosing <a href>, or null if none. Shared by the
|
|
693
|
+
// click (link-click) path and the tap path so a link tap stays a link click.
|
|
694
|
+
function linkAncestor(node){
|
|
695
|
+
while (node && node.nodeType === 1) {
|
|
696
|
+
if (node.localName === 'a' && node.hasAttribute('href')) return node;
|
|
697
|
+
node = node.parentNode;
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
// Walk up from a node to an enclosing <img src> (or SVG <image>), or null. Mirrors
|
|
702
|
+
// linkAncestor so a tap on a figure is distinguishable from a page-turn tap. The
|
|
703
|
+
// src is already a data: URL — applyResources substituted the full-resolution bytes
|
|
704
|
+
// into it — so forwarding it re-uses those bytes; no blob URL is ever minted.
|
|
705
|
+
function imageAncestor(node){
|
|
706
|
+
while (node && node.nodeType === 1) {
|
|
707
|
+
var name = node.localName;
|
|
708
|
+
if (name === 'img' && node.getAttribute('src')) return node;
|
|
709
|
+
if (name === 'image' && (node.getAttribute('href') || node.getAttribute('xlink:href'))) return node;
|
|
710
|
+
node = node.parentNode;
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
document.addEventListener('click', function(event){
|
|
715
|
+
var link = linkAncestor(event.target);
|
|
716
|
+
if (link) {
|
|
717
|
+
event.preventDefault();
|
|
718
|
+
// Report the raw authored href, not node.href: the opaque origin resolves
|
|
719
|
+
// the property against a null base and mangles it. The host/facade parses.
|
|
720
|
+
send({ v: VERSION, type: 'linkclick', href: String(link.getAttribute('href') || '') });
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// Navigation-relevant keydowns are forwarded to the host, which owns the key
|
|
725
|
+
// map (direction-aware). Only the keys the reader acts on are forwarded. The
|
|
726
|
+
// forwarding is unconditional (the wire is not gated), but preventDefault is:
|
|
727
|
+
// KEYBOARD_NAV is the host's keyboard-input setting baked in at document
|
|
728
|
+
// assembly, and when the host has keyboard input disabled it will ignore the
|
|
729
|
+
// message, so suppressing the key's default action too would leave it dead.
|
|
730
|
+
// Space is Space/Shift+Space paging (a near-universal reader convention); it is
|
|
731
|
+
// forwarded as the normalized tokens 'Space' / 'Shift+Space' because event.key
|
|
732
|
+
// is ' ' for both and the wire carries no modifier field. Every other key is
|
|
733
|
+
// left untouched.
|
|
734
|
+
var NAV_KEYS = { ArrowLeft:1, ArrowRight:1, ArrowUp:1, ArrowDown:1, PageUp:1, PageDown:1, Home:1, End:1 };
|
|
735
|
+
document.addEventListener('keydown', function(event){
|
|
736
|
+
var isSpace = event.key === ' ' || event.key === 'Spacebar';
|
|
737
|
+
// Keyboard equivalent of a figure tap: Enter/Space on a focused image opens the
|
|
738
|
+
// same host-side zoom overlay a pointer tap does, so zoom is reachable without a
|
|
739
|
+
// pointer. Checked before paging so Space-to-activate wins on a focused image.
|
|
740
|
+
// Images are made focusable below (markImages); imageAncestor also catches
|
|
741
|
+
// Enter fired on a wrapper the image sits inside.
|
|
742
|
+
if (event.key === 'Enter' || isSpace) {
|
|
743
|
+
var img = imageAncestor(event.target);
|
|
744
|
+
if (img) {
|
|
745
|
+
event.preventDefault();
|
|
746
|
+
var isrc = img.getAttribute('src') || img.getAttribute('href') || img.getAttribute('xlink:href') || '';
|
|
747
|
+
send({ v: VERSION, type: 'imagetap', src: String(isrc), alt: String(img.getAttribute('alt') || '') });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (NAV_KEYS[event.key] === 1 || isSpace) {
|
|
752
|
+
if (KEYBOARD_NAV) event.preventDefault();
|
|
753
|
+
send({ v: VERSION, type: 'key', key: isSpace ? (event.shiftKey ? 'Shift+Space' : 'Space') : event.key });
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// A figure is only tap-to-zoom by default; making each content image focusable and
|
|
758
|
+
// button-roled is what lets a keyboard user reach the zoom overlay. Marked as chunks
|
|
759
|
+
// realize (content is virtualized), idempotently, without touching the sandbox: these
|
|
760
|
+
// are ARIA/tabindex attributes only, no new capability.
|
|
761
|
+
function markImage(img){
|
|
762
|
+
if (img.getAttribute('data-wr-activatable') === '1') return;
|
|
763
|
+
img.setAttribute('data-wr-activatable', '1');
|
|
764
|
+
if (!img.hasAttribute('tabindex')) img.setAttribute('tabindex', '0');
|
|
765
|
+
if (!img.hasAttribute('role')) img.setAttribute('role', 'button');
|
|
766
|
+
if (!img.getAttribute('aria-label')) {
|
|
767
|
+
var label = img.getAttribute('alt') || 'Image';
|
|
768
|
+
img.setAttribute('aria-label', label + ' — press Enter to zoom');
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function markImages(scope){
|
|
772
|
+
if (!scope || !scope.getElementsByTagName) return;
|
|
773
|
+
var imgs = scope.getElementsByTagName('img');
|
|
774
|
+
for (var i = 0; i < imgs.length; i++) markImage(imgs[i]);
|
|
775
|
+
}
|
|
776
|
+
function setupImageActivation(){
|
|
777
|
+
var root = document.getElementById(ROOT_ID) || document.body;
|
|
778
|
+
if (!root) return;
|
|
779
|
+
// Content ships in the initial srcdoc, so a one-shot pass marks everything present
|
|
780
|
+
// at parse; the observer then catches images added by later chunk realization.
|
|
781
|
+
markImages(root);
|
|
782
|
+
if (typeof MutationObserver === 'function') {
|
|
783
|
+
new MutationObserver(function(records){
|
|
784
|
+
for (var i = 0; i < records.length; i++) {
|
|
785
|
+
var added = records[i].addedNodes;
|
|
786
|
+
for (var j = 0; j < added.length; j++) {
|
|
787
|
+
var n = added[j];
|
|
788
|
+
if (!n || n.nodeType !== 1) continue;
|
|
789
|
+
if (n.localName === 'img') markImage(n);
|
|
790
|
+
else markImages(n);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}).observe(root, { childList: true, subtree: true });
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (document.readyState === 'loading') {
|
|
797
|
+
document.addEventListener('DOMContentLoaded', setupImageActivation);
|
|
798
|
+
} else {
|
|
799
|
+
setupImageActivation();
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Pointer/touch: a drag that clears the threshold and is horizontal-dominant is
|
|
803
|
+
// a swipe; a small movement that is not on a link is a tap. The host maps both
|
|
804
|
+
// (direction-aware). A link tap is left to the click handler as a linkclick.
|
|
805
|
+
var SWIPE_THRESHOLD = 30;
|
|
806
|
+
var TAP_SLOP = 10;
|
|
807
|
+
var gesture = null; // { x, y, target }
|
|
808
|
+
function beginGesture(x, y, target){ gesture = { x: x, y: y, target: target }; }
|
|
809
|
+
function endGesture(x, y){
|
|
810
|
+
if (gesture === null) return;
|
|
811
|
+
var dx = x - gesture.x;
|
|
812
|
+
var dy = y - gesture.y;
|
|
813
|
+
var start = gesture;
|
|
814
|
+
gesture = null;
|
|
815
|
+
if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy)) {
|
|
816
|
+
// A horizontal drag that left a live text selection is the reader selecting
|
|
817
|
+
// text, not turning a page. Suppress the swipe so selection wins; the trailing
|
|
818
|
+
// pointerup still flushes the selection to the host.
|
|
819
|
+
var sel = document.getSelection();
|
|
820
|
+
if (sel !== null && !sel.isCollapsed && sel.toString().length !== 0) return;
|
|
821
|
+
send({ v: VERSION, type: 'swipe', dx: dx, dy: dy });
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (Math.abs(dx) <= TAP_SLOP && Math.abs(dy) <= TAP_SLOP && !linkAncestor(start.target)) {
|
|
825
|
+
// A tap on an image (and not a link) opens the host-side zoom overlay: forward
|
|
826
|
+
// the image's resolved data: URL + alt. Every other tap is a page-turn gesture
|
|
827
|
+
// the host maps to a zone — unchanged, so tapIntent never regresses.
|
|
828
|
+
var image = imageAncestor(start.target);
|
|
829
|
+
if (image) {
|
|
830
|
+
var src = image.getAttribute('src') || image.getAttribute('href') || image.getAttribute('xlink:href') || '';
|
|
831
|
+
send({ v: VERSION, type: 'imagetap', src: String(src), alt: String(image.getAttribute('alt') || '') });
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
send({ v: VERSION, type: 'tap', x: x, y: y, width: window.innerWidth, height: window.innerHeight });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (typeof window.PointerEvent === 'function') {
|
|
838
|
+
document.addEventListener('pointerdown', function(event){
|
|
839
|
+
if (event.isPrimary === false) return;
|
|
840
|
+
beginGesture(event.clientX, event.clientY, event.target);
|
|
841
|
+
});
|
|
842
|
+
document.addEventListener('pointerup', function(event){
|
|
843
|
+
endGesture(event.clientX, event.clientY);
|
|
844
|
+
});
|
|
845
|
+
document.addEventListener('pointercancel', function(){ gesture = null; });
|
|
846
|
+
} else {
|
|
847
|
+
document.addEventListener('touchstart', function(event){
|
|
848
|
+
var t = event.changedTouches[0];
|
|
849
|
+
if (t) beginGesture(t.clientX, t.clientY, event.target);
|
|
850
|
+
});
|
|
851
|
+
document.addEventListener('touchend', function(event){
|
|
852
|
+
var t = event.changedTouches[0];
|
|
853
|
+
if (t) endGesture(t.clientX, t.clientY);
|
|
854
|
+
});
|
|
855
|
+
document.addEventListener('touchcancel', function(){ gesture = null; });
|
|
856
|
+
}
|
|
857
|
+
// Text selection is forwarded as a semantic offset range, not caught on the host:
|
|
858
|
+
// the selection lives inside this opaque-origin frame's document and never reaches
|
|
859
|
+
// the parent. The host cannot read the frame's Selection, so the frame computes
|
|
860
|
+
// the UTF-16 offset range over the same tiled section text sectionText() reports
|
|
861
|
+
// and forwards { start, end, text }. Same forwarding reasoning as link/key/tap.
|
|
862
|
+
function enclosingChunk(node){
|
|
863
|
+
var el = node && node.nodeType === 1 ? node : (node ? node.parentNode : null);
|
|
864
|
+
while (el && el.nodeType === 1){
|
|
865
|
+
if (el.className && (' ' + el.className + ' ').indexOf(' ' + CHUNK_CLASS + ' ') !== -1) return el;
|
|
866
|
+
el = el.parentNode;
|
|
867
|
+
}
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
// UTF-16 offset of (node, offset) into the concatenated section text: sum every
|
|
871
|
+
// prior chunk container's text length, then add the text length from this chunk's
|
|
872
|
+
// start up to the boundary via a Range. Returns -1 if the point is not inside a
|
|
873
|
+
// realized chunk container (a selection anchored outside the tiled text).
|
|
874
|
+
function offsetOfPoint(node, offset){
|
|
875
|
+
var chunk = enclosingChunk(node);
|
|
876
|
+
if (chunk === null) return -1;
|
|
877
|
+
var chunks = chunkContainers();
|
|
878
|
+
var base = 0;
|
|
879
|
+
for (var i = 0; i < chunks.length; i++){
|
|
880
|
+
if (chunks[i] === chunk) break;
|
|
881
|
+
base += (chunks[i].textContent || '').length;
|
|
882
|
+
}
|
|
883
|
+
var range = document.createRange();
|
|
884
|
+
try {
|
|
885
|
+
range.setStart(chunk, 0);
|
|
886
|
+
range.setEnd(node, offset);
|
|
887
|
+
return base + range.toString().length;
|
|
888
|
+
} catch (error) {
|
|
889
|
+
return -1;
|
|
890
|
+
} finally {
|
|
891
|
+
range.detach && range.detach();
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function forwardSelection(){
|
|
895
|
+
var selection = document.getSelection();
|
|
896
|
+
if (selection === null || selection.rangeCount === 0 || selection.isCollapsed) return;
|
|
897
|
+
var text = selection.toString();
|
|
898
|
+
if (text.length === 0) return;
|
|
899
|
+
var range = selection.getRangeAt(0);
|
|
900
|
+
// Guard the zero-width-anchor / empty-inline gotcha: a selection whose rects are
|
|
901
|
+
// empty carries no painted geometry, so forwarding it would be a malformed range.
|
|
902
|
+
if (range.getClientRects().length === 0) return;
|
|
903
|
+
var start = offsetOfPoint(range.startContainer, range.startOffset);
|
|
904
|
+
var end = offsetOfPoint(range.endContainer, range.endOffset);
|
|
905
|
+
if (start < 0 || end < 0) return;
|
|
906
|
+
if (start > end){ var swap = start; start = end; end = swap; }
|
|
907
|
+
send({ v: VERSION, type: 'selection', start: start, end: end, text: text });
|
|
908
|
+
}
|
|
909
|
+
// A completed selection is a selectionchange settled by a pointer/key release, so
|
|
910
|
+
// forward on release rather than on every intermediate selectionchange (which
|
|
911
|
+
// fires per character during a drag). A bare click collapses the selection and is
|
|
912
|
+
// dropped by the isCollapsed guard above.
|
|
913
|
+
var selectionDirty = false;
|
|
914
|
+
document.addEventListener('selectionchange', function(){ selectionDirty = true; });
|
|
915
|
+
function flushSelection(){ if (selectionDirty){ selectionDirty = false; forwardSelection(); } }
|
|
916
|
+
document.addEventListener('pointerup', flushSelection);
|
|
917
|
+
document.addEventListener('keyup', flushSelection);
|
|
918
|
+
document.addEventListener('mouseup', flushSelection);
|
|
919
|
+
|
|
920
|
+
if (document.readyState === 'loading') {
|
|
921
|
+
document.addEventListener('DOMContentLoaded', function(){ send({ v: VERSION, type: 'ready' }); });
|
|
922
|
+
} else {
|
|
923
|
+
send({ v: VERSION, type: 'ready' });
|
|
924
|
+
}
|
|
925
|
+
})();`;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Wraps each chunk in its own container element, tagging it with the cumulative
|
|
929
|
+
* character range it covers in the section's concatenated text. Offsets are
|
|
930
|
+
* end-exclusive: chunk *i* covers `[start, end)`, and chunk *i+1* starts exactly
|
|
931
|
+
* at the previous `end`, so the ranges tile the section text without gap or
|
|
932
|
+
* overlap. The frame reads these attributes to build per-chunk multi-column
|
|
933
|
+
* contexts and to map pages to character offsets.
|
|
934
|
+
*
|
|
935
|
+
* The container is a `<div class="wolfy-reader-chunk">`; the char count comes from
|
|
936
|
+
* `chunkElement` (`textContent.length` of the chunk's nodes), so it matches what
|
|
937
|
+
* the frame measures with `textContent` at runtime.
|
|
938
|
+
*/
|
|
939
|
+
export function assembleChunkedBody(chunks) {
|
|
940
|
+
const parts = [];
|
|
941
|
+
let offset = 0;
|
|
942
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
943
|
+
const chunk = chunks[index];
|
|
944
|
+
const start = offset;
|
|
945
|
+
const end = offset + chunk.chars;
|
|
946
|
+
offset = end;
|
|
947
|
+
parts.push(`<div class="${CHUNK_CLASS}" ${CHUNK_INDEX_ATTR}="${index}" ` +
|
|
948
|
+
`${CHUNK_START_ATTR}="${start}" ${CHUNK_END_ATTR}="${end}">${chunk.html}</div>`);
|
|
949
|
+
}
|
|
950
|
+
return parts.join('');
|
|
951
|
+
}
|
|
952
|
+
export function assembleFrameDocument(parts) {
|
|
953
|
+
// The coordination script sits in the head, before any content, so violations
|
|
954
|
+
// raised while the body is still parsing are already being listened for. It
|
|
955
|
+
// announces readiness on DOMContentLoaded, not on evaluation.
|
|
956
|
+
//
|
|
957
|
+
// The theme stylesheet follows the minimal reset and precedes the publisher's
|
|
958
|
+
// headHtml: its `@layer` is declared first so book styles outrank it on their
|
|
959
|
+
// own properties, but the `--wr-*` variables it defines are names the book does
|
|
960
|
+
// not know and cannot clobber.
|
|
961
|
+
return [
|
|
962
|
+
'<!doctype html>',
|
|
963
|
+
'<html>',
|
|
964
|
+
'<head>',
|
|
965
|
+
`<meta http-equiv="Content-Security-Policy" content="${escapeXmlAttribute(contentSecurityPolicy(parts.nonce))}">`,
|
|
966
|
+
'<meta charset="utf-8">',
|
|
967
|
+
`<script nonce="${parts.nonce}">${coordinationScript(parts.hostOrigin, parts.keyboardNav !== false)}</script>`,
|
|
968
|
+
`<style>${RESET_CSS}</style>`,
|
|
969
|
+
...(parts.themeCss !== undefined && parts.themeCss !== '' ? [`<style>${parts.themeCss}</style>`] : []),
|
|
970
|
+
parts.headHtml,
|
|
971
|
+
'</head>',
|
|
972
|
+
'<body>',
|
|
973
|
+
`<div id="${CONTENT_ROOT_ID}">`,
|
|
974
|
+
parts.bodyHtml,
|
|
975
|
+
'</div>',
|
|
976
|
+
'</body>',
|
|
977
|
+
'</html>',
|
|
978
|
+
].join('\n');
|
|
979
|
+
}
|
|
980
|
+
//# sourceMappingURL=frame.js.map
|