gutterpress 0.9.0-alpha.2 → 0.10.0-alpha.4
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/README.md +18 -4
- package/dist/api/index.d.ts +11 -5
- package/dist/api/index.js +20 -15
- package/dist/assets/preview/scripts/preview-bridge.d.ts +1 -0
- package/dist/assets/preview/scripts/preview-interface.d.ts +1 -0
- package/dist/{audit-nhn2pjz3.js → audit-k1vnfwvc.js} +10 -7
- package/dist/{build-san7fv2z.js → build-hqmwgvdw.js} +14 -8
- package/dist/checks/source/index.d.ts +1 -0
- package/dist/checks/source/layout-markers.d.ts +3 -0
- package/dist/checks/source/local-ref-parser.d.ts +28 -0
- package/dist/{cli-e5zhb0xs.js → cli-0r0tq16s.js} +17 -24
- package/dist/cli-46ycxe6r.js +18 -0
- package/dist/cli-c41yr7he.js +241 -0
- package/dist/cli-hp9r2pzt.js +2343 -0
- package/dist/{index-yzrh708h.js → cli-k1065rkg.js} +2 -5
- package/dist/{cli-k4bd06sd.js → cli-n25qycwz.js} +4986 -5889
- package/dist/{cli-najycadg.js → cli-ra0ed2xt.js} +69 -253
- package/dist/{cli-506tg37g.js → cli-revgt4pr.js} +2 -1
- package/dist/{cli-wchtvxvw.js → cli-v5mp7a6q.js} +14 -1
- package/dist/cli.js +20 -18
- package/dist/{doctor-hrk0kxxz.js → doctor-dxms7ehm.js} +4 -2
- package/dist/engine/compiler/build.d.ts +139 -0
- package/dist/engine/compiler/postprocess.d.ts +23 -0
- package/dist/engine/compiler/tier2.d.ts +78 -0
- package/dist/engine/shared/cdp.d.ts +104 -0
- package/dist/engine/shared/content-value.d.ts +64 -0
- package/dist/engine/shared/gcpm-extract.d.ts +119 -0
- package/dist/engine/shared/margin-box-support.d.ts +12 -0
- package/dist/engine/shared/pdf-inspect.d.ts +24 -0
- package/dist/engine/shared/synthesis.d.ts +155 -0
- package/dist/engine-wa7y9av9.js +41 -0
- package/dist/engine-z4p9sr4h.js +40 -0
- package/dist/gutterpress-agent-1ctgfz92.js +576 -0
- package/dist/gutterpress-viewer-cem7dmr5.js +2349 -0
- package/dist/{index-bynn850m.js → index-05y3dnxq.js} +3049 -4015
- package/dist/index-9tyq9kks.js +708 -0
- package/dist/{cli-yzrh708h.js → index-mdefp0y5.js} +1 -1
- package/dist/{index-wchtvxvw.js → index-v5mp7a6q.js} +14 -1
- package/dist/index-xxg4zfrg.js +1907 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +24 -17
- package/dist/lib/asset-inline.d.ts +36 -0
- package/dist/lib/browser-pool.d.ts +18 -0
- package/dist/lib/build-error.d.ts +1 -1
- package/dist/lib/build-preflight.d.ts +35 -2
- package/dist/lib/build-runner.d.ts +45 -17
- package/dist/lib/build-staging.d.ts +9 -55
- package/dist/lib/cli-args.d.ts +8 -0
- package/dist/lib/desktop.d.ts +2 -2
- package/dist/lib/embedded-assets.d.ts +1 -1
- package/dist/lib/engine.d.ts +33 -0
- package/dist/lib/ghostscript.d.ts +46 -1
- package/dist/lib/markdown/assemble.d.ts +18 -10
- package/dist/lib/markdown/gp-pin-scope.d.ts +1 -0
- package/dist/lib/markdown/gutterpress-css.d.ts +125 -0
- package/dist/lib/markdown/images.d.ts +26 -0
- package/dist/lib/markdown/index.d.ts +4 -2
- package/dist/lib/markdown/inline-source.d.ts +10 -0
- package/dist/lib/markdown/markers.d.ts +32 -0
- package/dist/lib/markdown/renderer.d.ts +8 -3
- package/dist/lib/markdown/source-range.d.ts +61 -0
- package/dist/lib/missing-asset-placeholder.d.ts +52 -0
- package/dist/lib/presets.d.ts +1 -1
- package/dist/lib/printsafe.d.ts +2 -3
- package/dist/lib/remote-auth/converge-merge.d.ts +48 -0
- package/dist/lib/remote-auth/image-clash.d.ts +17 -0
- package/dist/lib/remote-auth/recovery/classify.d.ts +55 -67
- package/dist/lib/remote-auth/recovery/inspect.d.ts +14 -13
- package/dist/lib/remote-auth/recovery/locks.d.ts +20 -0
- package/dist/lib/remote-auth/recovery/repair.d.ts +29 -0
- package/dist/lib/remote-auth/recovery/types.d.ts +8 -204
- package/dist/lib/remote-auth/sync-messages.d.ts +2 -3
- package/dist/lib/remote-auth/sync-types.d.ts +35 -63
- package/dist/lib/remote-auth/sync.d.ts +11 -18
- package/dist/lib/remote-auth/transport.d.ts +14 -7
- package/dist/lib/theme-import.d.ts +4 -5
- package/dist/{lint-96j9hrj4.js → lint-xjwm5ep8.js} +10 -7
- package/dist/{manifest.schema-z61rzw44.json → manifest.schema-zxgxnbg7.json} +21 -0
- package/dist/{new-8p38wavc.js → new-kwdwpf0j.js} +12 -8
- package/dist/{plugin-ees6nhkc.js → plugin-rg4tnn96.js} +10 -7
- package/dist/{preflight-1q6c2edh.js → preflight-3127y25z.js} +10 -7
- package/dist/preview/file-watcher.d.ts +13 -17
- package/dist/preview/lifecycle.d.ts +1 -1
- package/dist/{pagedjs-bridge-vn4hk9fx.js → preview-bridge-fz7vpk8m.js} +8 -0
- package/dist/preview-interface-435cczt5.js +1059 -0
- package/dist/{preview-y5a2zen1.js → preview-ncgfhqmw.js} +16 -9
- package/dist/preview-shell-c5mfa3q0.js +346 -0
- package/dist/{project-source-p0gn1wd5.js → project-source-ekcyp63q.js} +1 -1
- package/dist/{publish-rm9yb3wh.js → publish-pr0rwh6p.js} +10 -7
- package/dist/render.d.ts +3 -4
- package/dist/render.js +728 -64
- package/dist/{repair-zgq7q2g6.js → repair-8270smfw.js} +45 -79
- package/dist/schema/manifest.types.d.ts +38 -0
- package/dist/{source-provider-c1rjm2c0.js → source-provider-3tcj6qg2.js} +2 -2
- package/dist/source-provider-vanafrt9.js +40 -0
- package/dist/{theme-zz2ktzqs.css → theme-h5recz6c.css} +8 -7
- package/dist/{theme-570zmh2t.css → theme-j2bagrfx.css} +8 -7
- package/dist/{theme-nya4nqh6.css → theme-nn6d53zy.css} +8 -7
- package/dist/types.d.ts +7 -0
- package/dist/{validate-nr0xa6sa.js → validate-54e17rae.js} +10 -7
- package/package.json +6 -6
- package/dist/cli-yja077f6.js +0 -92
- package/dist/git-http-yrb4ag6z.js +0 -17
- package/dist/index-yja077f6.js +0 -92
- package/dist/lib/markdown/markdown-it-paged.d.ts +0 -30
- package/dist/lib/pagedjs-marker.d.ts +0 -42
- package/dist/lib/pagedjs.d.ts +0 -26
- package/dist/lib/pagination.d.ts +0 -149
- package/dist/lib/remote-auth/conflict-resolution.d.ts +0 -29
- package/dist/lib/remote-auth/recovery/abort-interrupted-operation.d.ts +0 -103
- package/dist/lib/remote-auth/recovery/backup.d.ts +0 -113
- package/dist/lib/remote-auth/recovery/context.d.ts +0 -47
- package/dist/lib/remote-auth/recovery/dispatch.d.ts +0 -28
- package/dist/lib/remote-auth/recovery/failsafe.d.ts +0 -33
- package/dist/lib/remote-auth/recovery/manual-guidance.d.ts +0 -28
- package/dist/lib/remote-auth/recovery/outcome-mapping.d.ts +0 -59
- package/dist/lib/remote-auth/recovery/policy.d.ts +0 -47
- package/dist/lib/remote-auth/recovery/recover-auth.d.ts +0 -42
- package/dist/lib/remote-auth/recovery/recover-binary-conflict.d.ts +0 -37
- package/dist/lib/remote-auth/recovery/recover-corrupt-index.d.ts +0 -40
- package/dist/lib/remote-auth/recovery/recover-detached-head.d.ts +0 -70
- package/dist/lib/remote-auth/recovery/recover-interrupted-cherry-pick.d.ts +0 -23
- package/dist/lib/remote-auth/recovery/recover-interrupted-merge.d.ts +0 -28
- package/dist/lib/remote-auth/recovery/recover-interrupted-rebase.d.ts +0 -39
- package/dist/lib/remote-auth/recovery/recover-merge-conflict.d.ts +0 -34
- package/dist/lib/remote-auth/recovery/recover-missing-git-dir.d.ts +0 -37
- package/dist/lib/remote-auth/recovery/recover-missing-objects.d.ts +0 -56
- package/dist/lib/remote-auth/recovery/recover-network.d.ts +0 -34
- package/dist/lib/remote-auth/recovery/recover-non-fast-forward.d.ts +0 -27
- package/dist/lib/remote-auth/recovery/recover-stale-lock.d.ts +0 -68
- package/dist/lib/remote-auth/recovery/recover-unrelated-histories.d.ts +0 -44
- package/dist/lib/remote-auth/recovery/recover-wrong-remote.d.ts +0 -35
- package/dist/lib/remote-auth/resolution-plan.d.ts +0 -64
- package/dist/paged.polyfill-n95pbxfn.js +0 -33288
- package/dist/pagedjs-interface-qxvzgwd7.js +0 -557
- package/dist/preview-shell-6dqexx1m.js +0 -581
- /package/dist/assets/{preview/scripts/pagedjs-bridge.d.ts → engine/gutterpress-agent.d.ts} +0 -0
- /package/dist/assets/{preview/scripts/pagedjs-interface.d.ts → engine/gutterpress-viewer.d.ts} +0 -0
|
@@ -0,0 +1,1059 @@
|
|
|
1
|
+
// Interface adapter: exposes window.previewAPI for the parent toolbar.
|
|
2
|
+
//
|
|
3
|
+
// The Gutterpress engine viewer paginates into .gp-sheet elements (one per
|
|
4
|
+
// page, `dataset.page` = 1-based book page) and exposes
|
|
5
|
+
// window.Gutterpress.pageOf(el) (0-based); it fires 'gp:layout' when its
|
|
6
|
+
// pagination completes.
|
|
7
|
+
//
|
|
8
|
+
// The block-overlay fragment-grouping machinery below (blocksMatchingRange)
|
|
9
|
+
// groups by `{chapter, range}` (data-source-range) — the native viewer never
|
|
10
|
+
// clones an element across pages (an element that visually spans pages is
|
|
11
|
+
// still ONE element), so nativeRectsFor resolves rects straight off the
|
|
12
|
+
// single matching element via getClientRects() + pageOf().
|
|
13
|
+
|
|
14
|
+
(function () {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
var pages = [];
|
|
18
|
+
var currentPage = 1;
|
|
19
|
+
var debugMode = false;
|
|
20
|
+
var currentViewMode = 'two-column';
|
|
21
|
+
var ignoreScrollUntil = 0;
|
|
22
|
+
var lastSourceLine = -1;
|
|
23
|
+
var lastSourceChapter = null;
|
|
24
|
+
// Find-in-book state (see previewAPI.find below).
|
|
25
|
+
var findQuery = '';
|
|
26
|
+
var findTotal = 0;
|
|
27
|
+
|
|
28
|
+
function refreshPages() {
|
|
29
|
+
pages = Array.from(document.querySelectorAll('.gp-sheet')).sort(function (a, b) {
|
|
30
|
+
return (parseInt(a.dataset.page, 10) || 0) - (parseInt(b.dataset.page, 10) || 0);
|
|
31
|
+
});
|
|
32
|
+
return pages;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clampPage(n) {
|
|
36
|
+
if (pages.length === 0) return 1;
|
|
37
|
+
var page = Number(n);
|
|
38
|
+
if (!Number.isFinite(page)) page = 1;
|
|
39
|
+
return Math.max(1, Math.min(Math.round(page), pages.length));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The native viewer lays one CHAPTER per row (`.gp-run`), each row as
|
|
43
|
+
// wide as that chapter needs — rows stack vertically, but a long chapter's
|
|
44
|
+
// pages run off horizontally within its own row (viewer.css: `.gp-sheet`
|
|
45
|
+
// is `left`-positioned within the row, every sheet in a row shares `top`).
|
|
46
|
+
// A top-only scan can't tell two sheets in the same row apart — every one
|
|
47
|
+
// of them has the same `top`, so it always resolves to the LAST sheet of
|
|
48
|
+
// whichever row is vertically in view, ignoring scrollLeft entirely
|
|
49
|
+
// (measured: goToPage(18/30/34) in a 34pp book all landed on page 14 —
|
|
50
|
+
// row 1's last page — regardless of horizontal scroll position). Pick the
|
|
51
|
+
// sheet with the GREATEST visible overlap area with the viewport — a fixed
|
|
52
|
+
// reference-point distance was tried first but broke at the very end of a
|
|
53
|
+
// row: the browser clamps scrollIntoView({inline:'start'}) once there's no
|
|
54
|
+
// more row content to scroll past, so the last page of a short final row
|
|
55
|
+
// can land mid-viewport rather than flush against any fixed point
|
|
56
|
+
// (measured: goToPage(34) on a 34pp book left page 34 at ~40% visible
|
|
57
|
+
// width from the left edge, so a left-edge reference point missed it and
|
|
58
|
+
// matched page 33 instead, which still touched the reference point). Falls
|
|
59
|
+
// back to nearest-by-distance when nothing overlaps at all (e.g. mid-scroll
|
|
60
|
+
// between rows, or every sheet clipped by a shorter viewport than a page).
|
|
61
|
+
function detectVisiblePage() {
|
|
62
|
+
if (pages.length === 0) return 1;
|
|
63
|
+
var vw = window.innerWidth;
|
|
64
|
+
var vh = window.innerHeight;
|
|
65
|
+
var best = 0;
|
|
66
|
+
var bestArea = 0;
|
|
67
|
+
var nearest = 0;
|
|
68
|
+
var nearestScore = Infinity;
|
|
69
|
+
var refX = vw / 3;
|
|
70
|
+
var refY = vh / 3;
|
|
71
|
+
for (var i = 0; i < pages.length; i++) {
|
|
72
|
+
var r = pages[i].getBoundingClientRect();
|
|
73
|
+
var w = Math.min(r.right, vw) - Math.max(r.left, 0);
|
|
74
|
+
var h = Math.min(r.bottom, vh) - Math.max(r.top, 0);
|
|
75
|
+
var area = w > 0 && h > 0 ? w * h : 0;
|
|
76
|
+
if (area > bestArea) { bestArea = area; best = i; }
|
|
77
|
+
var dx = refX < r.left ? r.left - refX : (refX > r.right ? refX - r.right : 0);
|
|
78
|
+
var dy = refY < r.top ? r.top - refY : (refY > r.bottom ? refY - r.bottom : 0);
|
|
79
|
+
var score = dx * dx + dy * dy;
|
|
80
|
+
if (score < nearestScore) { nearestScore = score; nearest = i; }
|
|
81
|
+
}
|
|
82
|
+
return (bestArea > 0 ? best : nearest) + 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function scrollToCurrentPage() {
|
|
86
|
+
if (pages.length === 0) return;
|
|
87
|
+
var page = clampPage(currentPage);
|
|
88
|
+
currentPage = page;
|
|
89
|
+
ignoreScrollUntil = Date.now() + 300;
|
|
90
|
+
// Native's rows can be wider than the viewport (a long chapter scrolls
|
|
91
|
+
// horizontally within its own row) — align the target sheet's left edge
|
|
92
|
+
// to the viewport's left edge (matching detectVisiblePage's `refX`).
|
|
93
|
+
pages[page - 1].scrollIntoView({
|
|
94
|
+
behavior: 'instant',
|
|
95
|
+
block: 'start',
|
|
96
|
+
inline: 'start'
|
|
97
|
+
});
|
|
98
|
+
recordVisibleSource();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function pageStep(mode) {
|
|
102
|
+
return (mode || currentViewMode) === 'single' ? 1 : 2;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Source-mapping helpers (ADR 0005) ──────────────────────────────────────
|
|
106
|
+
// Every block element carries data-source-line (markdown-it-source-map). These
|
|
107
|
+
// map rendered DOM <-> markdown source line and rendered DOM <-> page. The
|
|
108
|
+
// native fragmenter MOVES elements into strips, it does not clone them, so
|
|
109
|
+
// these attributes survive intact.
|
|
110
|
+
function lineOf(el) {
|
|
111
|
+
if (!el || !el.getAttribute) return null;
|
|
112
|
+
var n = parseInt(el.getAttribute('data-source-line'), 10);
|
|
113
|
+
return Number.isFinite(n) ? n : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function pageIndexOf(el) {
|
|
117
|
+
if (!el) return 0;
|
|
118
|
+
if (!window.Gutterpress || typeof window.Gutterpress.pageOf !== 'function') return 0;
|
|
119
|
+
var native = window.Gutterpress.pageOf(el);
|
|
120
|
+
return native >= 0 ? native + 1 : 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sourcedBlocks() {
|
|
124
|
+
return Array.from(document.querySelectorAll('[data-source-line]'));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// data-source-line resets PER FILE, so a line number is only unambiguous when
|
|
128
|
+
// paired with its chapter (data-chapter-src — the source filename). These two
|
|
129
|
+
// helpers scope line lookups to a chapter so editor<->preview sync maps to the
|
|
130
|
+
// right file in a multi-chapter book.
|
|
131
|
+
function chapterOf(el) {
|
|
132
|
+
var c = el && el.closest ? el.closest('[data-chapter-src]') : null;
|
|
133
|
+
return c ? c.getAttribute('data-chapter-src') : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function blocksInChapter(chapter) {
|
|
137
|
+
if (!chapter) return sourcedBlocks();
|
|
138
|
+
return sourcedBlocks().filter(function (block) {
|
|
139
|
+
return chapterOf(block) === chapter;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function rectsOf(el) {
|
|
144
|
+
var rects = el && el.getClientRects ? Array.from(el.getClientRects()) : [];
|
|
145
|
+
return rects.length ? rects : [el.getBoundingClientRect()];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function visibleRectOf(el, pageRect) {
|
|
149
|
+
var vw = window.innerWidth;
|
|
150
|
+
var vh = window.innerHeight;
|
|
151
|
+
var ref = 4;
|
|
152
|
+
var rects = rectsOf(el);
|
|
153
|
+
var best = null;
|
|
154
|
+
var bestIndex = -1;
|
|
155
|
+
var bestDistance = Infinity;
|
|
156
|
+
var bestArea = -1;
|
|
157
|
+
for (var i = 0; i < rects.length; i++) {
|
|
158
|
+
var r = rects[i];
|
|
159
|
+
var width = Math.min(r.right, vw, pageRect ? pageRect.right : vw) -
|
|
160
|
+
Math.max(r.left, 0, pageRect ? pageRect.left : 0);
|
|
161
|
+
var height = Math.min(r.bottom, vh, pageRect ? pageRect.bottom : vh) -
|
|
162
|
+
Math.max(r.top, 0, pageRect ? pageRect.top : 0);
|
|
163
|
+
if (width <= 0 || height <= 0) continue;
|
|
164
|
+
var distance = r.top <= ref && r.bottom > ref ? 0 : Math.abs(r.top - ref);
|
|
165
|
+
var area = width * height;
|
|
166
|
+
if (distance < bestDistance || (distance === bestDistance && area > bestArea)) {
|
|
167
|
+
best = r;
|
|
168
|
+
bestIndex = i;
|
|
169
|
+
bestDistance = distance;
|
|
170
|
+
bestArea = area;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return best ? { rect: best, index: bestIndex, distance: bestDistance, area: bestArea } : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function rectOnPage(el, pageRect) {
|
|
177
|
+
if (!pageRect) return null;
|
|
178
|
+
var rects = rectsOf(el);
|
|
179
|
+
for (var i = 0; i < rects.length; i++) {
|
|
180
|
+
var r = rects[i];
|
|
181
|
+
var width = Math.min(r.right, pageRect.right) - Math.max(r.left, pageRect.left);
|
|
182
|
+
var height = Math.min(r.bottom, pageRect.bottom) - Math.max(r.top, pageRect.top);
|
|
183
|
+
if (width > 0 && height > 0) return r;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Find the source fragment nearest the viewport's top edge. getClientRects()
|
|
189
|
+
// matters here: one source block can fragment across several printed pages.
|
|
190
|
+
function topVisibleSource() {
|
|
191
|
+
var blocks = sourcedBlocks();
|
|
192
|
+
if (blocks.length === 0) return null;
|
|
193
|
+
if (pages.length === 0) refreshPages();
|
|
194
|
+
var page = pages[detectVisiblePage() - 1];
|
|
195
|
+
var pageRect = page ? page.getBoundingClientRect() : null;
|
|
196
|
+
var best = null, bestRect = null, bestIndex = -1, bestDistance = Infinity, bestArea = -1;
|
|
197
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
198
|
+
var visible = visibleRectOf(blocks[i], pageRect);
|
|
199
|
+
if (!visible) continue;
|
|
200
|
+
if (visible.distance < bestDistance || (visible.distance === bestDistance && visible.area > bestArea)) {
|
|
201
|
+
best = blocks[i];
|
|
202
|
+
bestRect = visible.rect;
|
|
203
|
+
bestIndex = visible.index;
|
|
204
|
+
bestDistance = visible.distance;
|
|
205
|
+
bestArea = visible.area;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return best ? { el: best, rect: bestRect, index: bestIndex, pageRect: pageRect } : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function topVisibleSourceEl() {
|
|
212
|
+
var source = topVisibleSource();
|
|
213
|
+
return source ? source.el : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// {el, line} of the viewport-top source position, with the line interpolated
|
|
217
|
+
// within the straddling block by the viewport top's fractional distance to the
|
|
218
|
+
// next annotated block (source-map only annotates top-level blocks, so the
|
|
219
|
+
// block's start line alone can be tens of lines above the visible content).
|
|
220
|
+
function visibleSourcePosition() {
|
|
221
|
+
var source = topVisibleSource();
|
|
222
|
+
if (!source) return null;
|
|
223
|
+
var el = source.el;
|
|
224
|
+
var line = lineOf(el);
|
|
225
|
+
if (line == null) return { el: el, rect: source.rect, index: source.index, line: null };
|
|
226
|
+
var ref = 4; // same reference line as topVisibleSourceEl
|
|
227
|
+
var top = source.rect.top;
|
|
228
|
+
if (top < ref) {
|
|
229
|
+
// Find the next annotated sibling-in-flow (document order, higher line —
|
|
230
|
+
// explicitly constrained to this chapter because source lines restart per
|
|
231
|
+
// file and a following chapter may begin at any line after front matter).
|
|
232
|
+
var blocks = sourcedBlocks();
|
|
233
|
+
var idx = blocks.indexOf(el);
|
|
234
|
+
var chapter = chapterOf(el);
|
|
235
|
+
for (var i = idx + 1; i < blocks.length; i++) {
|
|
236
|
+
if (chapterOf(blocks[i]) !== chapter) break;
|
|
237
|
+
var nl = lineOf(blocks[i]);
|
|
238
|
+
if (nl == null) continue;
|
|
239
|
+
if (nl <= line) break;
|
|
240
|
+
var nextRect = source.pageRect ? rectOnPage(blocks[i], source.pageRect) : null;
|
|
241
|
+
if (!nextRect) break;
|
|
242
|
+
var nextTop = nextRect.top;
|
|
243
|
+
if (nextTop > top) {
|
|
244
|
+
var f = Math.max(0, Math.min(1, (ref - top) / (nextTop - top)));
|
|
245
|
+
line = line + Math.round(f * (nl - line));
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { el: el, rect: source.rect, index: source.index, line: line };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function recordVisibleSource() {
|
|
254
|
+
var pos = visibleSourcePosition();
|
|
255
|
+
lastSourceLine = pos && pos.line != null ? pos.line : -1;
|
|
256
|
+
lastSourceChapter = pos ? chapterOf(pos.el) : null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function preserveViewport(change) {
|
|
260
|
+
refreshPages();
|
|
261
|
+
var position = visibleSourcePosition();
|
|
262
|
+
var anchor = position ? {
|
|
263
|
+
el: position.el,
|
|
264
|
+
index: position.index,
|
|
265
|
+
top: position.rect.top,
|
|
266
|
+
left: position.rect.left
|
|
267
|
+
} : null;
|
|
268
|
+
change();
|
|
269
|
+
refreshPages();
|
|
270
|
+
if (!anchor) {
|
|
271
|
+
scrollToCurrentPage();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
ignoreScrollUntil = Date.now() + 300;
|
|
275
|
+
var rects = rectsOf(anchor.el);
|
|
276
|
+
var currentRect = rects[anchor.index] || anchor.el.getBoundingClientRect();
|
|
277
|
+
var delta = currentRect.top - anchor.top;
|
|
278
|
+
var deltaX = currentRect.left - anchor.left;
|
|
279
|
+
if (delta || deltaX) {
|
|
280
|
+
window.scrollBy({ top: delta, left: deltaX, behavior: 'instant' });
|
|
281
|
+
}
|
|
282
|
+
currentPage = detectVisiblePage();
|
|
283
|
+
recordVisibleSource();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Resolve a {line, chapter?} target to its enclosing annotated block PLUS the
|
|
287
|
+
// following annotated block, so callers can interpolate within long blocks
|
|
288
|
+
// (markdown-it-source-map only annotates top-level *_open tokens, so a target
|
|
289
|
+
// line deep inside a list/table/paragraph would otherwise snap to the block's
|
|
290
|
+
// start line — measured 35-line snap-backs on real chapters).
|
|
291
|
+
function resolveLinePosition(target) {
|
|
292
|
+
var line = Number(target.line);
|
|
293
|
+
var blocks = blocksInChapter(target.chapter);
|
|
294
|
+
var best = null, bestLine = -Infinity, next = null, nextLine = Infinity;
|
|
295
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
296
|
+
var l = lineOf(blocks[i]);
|
|
297
|
+
if (l == null) continue;
|
|
298
|
+
if (l <= line && l > bestLine) { bestLine = l; best = blocks[i]; }
|
|
299
|
+
else if (l > line && l < nextLine) { nextLine = l; next = blocks[i]; }
|
|
300
|
+
}
|
|
301
|
+
if (!best) return blocks[0] ? { el: blocks[0], line: lineOf(blocks[0]), nextEl: null, nextLine: null } : null;
|
|
302
|
+
return { el: best, line: bestLine, nextEl: next, nextLine: next ? nextLine : null };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Resolve a scrollTo/highlight target ({line}|{id}|{selector}|{page} or a bare
|
|
306
|
+
// line number) to a DOM element.
|
|
307
|
+
function resolveTarget(target) {
|
|
308
|
+
if (target == null) return null;
|
|
309
|
+
if (typeof target === 'number') target = { line: target };
|
|
310
|
+
if (target.selector) { try { return document.querySelector(target.selector); } catch (_e) { return null; } }
|
|
311
|
+
if (target.id) return document.getElementById(target.id);
|
|
312
|
+
if (target.page != null) { refreshPages(); return pages[clampPage(target.page) - 1] || null; }
|
|
313
|
+
if (target.line != null) {
|
|
314
|
+
var pos = resolveLinePosition(target);
|
|
315
|
+
return pos ? pos.el : null;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Context-menu target resolution (protocol v4) ────────────────────────────
|
|
321
|
+
// docs/inline-editing-plan.md §3.1 / ADR 0009. getContextTargetAt() is the
|
|
322
|
+
// single resolution routine shared by the public previewAPI member and both
|
|
323
|
+
// event listeners below (contextmenu + keyboard) — see that doc for the
|
|
324
|
+
// exact kind-precedence contract implemented here.
|
|
325
|
+
var LAYOUT_MARKER_CLASSES = ['chapter', 'spread', 'page', 'section', 'gp-page-break', 'gp-column-break'];
|
|
326
|
+
|
|
327
|
+
function elementAtPoint(x, y) {
|
|
328
|
+
try {
|
|
329
|
+
if (typeof document.elementFromPoint === 'function') return document.elementFromPoint(x, y);
|
|
330
|
+
} catch (_e) { /* unsupported host — degrade to null */ }
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function elementOf(node) {
|
|
335
|
+
if (!node) return null;
|
|
336
|
+
return node.nodeType === 1 ? node : (node.parentElement || null);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Parse a `data-source-range="<start>:<end>"` value into a [start, end)
|
|
340
|
+
// pair — token.map's own 0-based half-open convention, verbatim.
|
|
341
|
+
function sourceRangeOf(el) {
|
|
342
|
+
if (!el || !el.getAttribute) return null;
|
|
343
|
+
var raw = el.getAttribute('data-source-range');
|
|
344
|
+
if (!raw) return null;
|
|
345
|
+
var sep = raw.indexOf(':');
|
|
346
|
+
if (sep < 0) return null;
|
|
347
|
+
var start = parseInt(raw.slice(0, sep), 10);
|
|
348
|
+
var end = parseInt(raw.slice(sep + 1), 10);
|
|
349
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
350
|
+
return [start, end];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Fence gotcha (§2.6): markdown-it's default fence renderer puts token
|
|
354
|
+
// attrs on the inner <code>, never the <pre> wrapper — a click on the code
|
|
355
|
+
// block's padding hits <pre>'s box, which never carries data-source-range.
|
|
356
|
+
// Prefer an annotated <code> child over climbing to a coarser ancestor.
|
|
357
|
+
function fenceCodeChild(el) {
|
|
358
|
+
if (!el || !el.tagName || el.tagName.toLowerCase() !== 'pre' || !el.children) return null;
|
|
359
|
+
for (var i = 0; i < el.children.length; i++) {
|
|
360
|
+
var c = el.children[i];
|
|
361
|
+
if (c.tagName && c.tagName.toLowerCase() === 'code' && c.getAttribute && c.getAttribute('data-source-range')) {
|
|
362
|
+
return c;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Innermost [data-source-range] element at/around `el` (self-or-ancestor),
|
|
369
|
+
// with the fence <pre>-hits-<code> special case above taking priority.
|
|
370
|
+
function resolveAnnotatedBlock(el) {
|
|
371
|
+
if (!el) return null;
|
|
372
|
+
var fenceCode = fenceCodeChild(el);
|
|
373
|
+
if (fenceCode) return fenceCode;
|
|
374
|
+
return el.closest ? el.closest('[data-source-range]') : null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function isMarkerBlock(el) {
|
|
378
|
+
if (!el || !el.classList) return false;
|
|
379
|
+
for (var i = 0; i < LAYOUT_MARKER_CLASSES.length; i++) {
|
|
380
|
+
if (el.classList.contains(LAYOUT_MARKER_CLASSES[i])) return true;
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// The native fragmenter MOVES elements into strips rather than cloning them,
|
|
386
|
+
// so there is no split-fragment marker to detect here — a block that visually
|
|
387
|
+
// spans pages is still exactly one element.
|
|
388
|
+
function isSplitFragment() {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Plain, JSON-cloneable rect — the payload crosses two postMessage
|
|
393
|
+
// boundaries, so no DOMRect instances (§3.5).
|
|
394
|
+
function plainRect(el) {
|
|
395
|
+
if (!el || !el.getBoundingClientRect) return null;
|
|
396
|
+
var r = el.getBoundingClientRect();
|
|
397
|
+
return { top: r.top, left: r.left, width: r.width, height: r.height };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Non-collapsed selection info, or null. NEVER Range.toString() — a
|
|
401
|
+
// cross-page range's raw text is polluted with inter-page structural
|
|
402
|
+
// whitespace and can include every intervening page's content (§3.5);
|
|
403
|
+
// selection.toString() is the display-only source of truth here.
|
|
404
|
+
function selectionInfo() {
|
|
405
|
+
var sel = typeof window.getSelection === 'function' ? window.getSelection() : null;
|
|
406
|
+
if (!sel || sel.isCollapsed) return null;
|
|
407
|
+
var anchorBlock = resolveAnnotatedBlock(elementOf(sel.anchorNode));
|
|
408
|
+
var focusBlock = resolveAnnotatedBlock(elementOf(sel.focusNode));
|
|
409
|
+
var withinSingleBlock = !!(anchorBlock && focusBlock && anchorBlock === focusBlock);
|
|
410
|
+
return {
|
|
411
|
+
text: sel.toString(),
|
|
412
|
+
withinSingleBlock: withinSingleBlock,
|
|
413
|
+
range: withinSingleBlock ? sourceRangeOf(anchorBlock) : null,
|
|
414
|
+
chapter: withinSingleBlock ? chapterOf(anchorBlock) : null
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Shared resolution: builds the full getContextTargetAt() payload for a
|
|
419
|
+
// point element. `selection`/`image`/`link` are populated whenever
|
|
420
|
+
// applicable REGARDLESS of which `kind` wins, so the menu can offer
|
|
421
|
+
// secondary items (§3.1).
|
|
422
|
+
//
|
|
423
|
+
// kind precedence (decided): selection -> image -> link -> marker -> block
|
|
424
|
+
// -> none.
|
|
425
|
+
function buildContextTarget(pointEl) {
|
|
426
|
+
var selection = selectionInfo();
|
|
427
|
+
var imageEl = pointEl && pointEl.closest ? pointEl.closest('img') : null;
|
|
428
|
+
var sourceOf = function (el) {
|
|
429
|
+
if (!el || !el.hasAttribute || !el.hasAttribute('data-gp-source-token')) return null;
|
|
430
|
+
var token = el.getAttribute('data-gp-source-token');
|
|
431
|
+
var rawOccurrence = el.getAttribute('data-gp-source-occurrence');
|
|
432
|
+
if (!token || !rawOccurrence) return null;
|
|
433
|
+
for (var i = 0; i < rawOccurrence.length; i++) {
|
|
434
|
+
if (rawOccurrence[i] < '0' || rawOccurrence[i] > '9') return null;
|
|
435
|
+
}
|
|
436
|
+
var occurrence = Number(rawOccurrence);
|
|
437
|
+
if (!Number.isSafeInteger(occurrence)) return null;
|
|
438
|
+
return {
|
|
439
|
+
token: token,
|
|
440
|
+
occurrence: occurrence
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
var image = imageEl ? {
|
|
444
|
+
src: imageEl.getAttribute('src'),
|
|
445
|
+
alt: imageEl.getAttribute('alt'),
|
|
446
|
+
source: sourceOf(imageEl)
|
|
447
|
+
} : null;
|
|
448
|
+
var linkEl = pointEl && pointEl.closest ? pointEl.closest('a') : null;
|
|
449
|
+
var link = linkEl ? {
|
|
450
|
+
href: linkEl.getAttribute('href'),
|
|
451
|
+
text: (linkEl.textContent || '').trim(),
|
|
452
|
+
source: sourceOf(linkEl)
|
|
453
|
+
} : null;
|
|
454
|
+
|
|
455
|
+
var block = resolveAnnotatedBlock(pointEl);
|
|
456
|
+
|
|
457
|
+
var kind;
|
|
458
|
+
if (selection) kind = 'selection';
|
|
459
|
+
else if (image) kind = 'image';
|
|
460
|
+
else if (link) kind = 'link';
|
|
461
|
+
else if (block && isMarkerBlock(block)) kind = 'marker';
|
|
462
|
+
else if (block) kind = 'block';
|
|
463
|
+
else kind = 'none';
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
kind: kind,
|
|
467
|
+
chapter: chapterOf(pointEl),
|
|
468
|
+
range: block ? sourceRangeOf(block) : null,
|
|
469
|
+
blockTag: block && block.tagName ? block.tagName.toLowerCase() : null,
|
|
470
|
+
split: isSplitFragment(block),
|
|
471
|
+
rect: plainRect(block || pointEl),
|
|
472
|
+
image: image,
|
|
473
|
+
link: link,
|
|
474
|
+
selection: selection
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// The anchor point for a keyboard-invoked menu: the current selection's
|
|
479
|
+
// focus position if non-collapsed, else the block at the top of the
|
|
480
|
+
// viewport (topVisibleSourceEl(), which already exists for scroll sync).
|
|
481
|
+
// Keyboard events targeted at a focused element in a cross-origin iframe
|
|
482
|
+
// never reach the parent SPA — this resolution MUST live here, not
|
|
483
|
+
// SPA-side.
|
|
484
|
+
function keyboardAnchorPoint() {
|
|
485
|
+
var sel = typeof window.getSelection === 'function' ? window.getSelection() : null;
|
|
486
|
+
var el = sel && !sel.isCollapsed ? elementOf(sel.focusNode) : null;
|
|
487
|
+
if (!el) el = topVisibleSourceEl();
|
|
488
|
+
var rect = el ? plainRect(el) : null;
|
|
489
|
+
if (!rect) return { x: 0, y: 0 };
|
|
490
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── Block-overlay geometry + masking (protocol v6) ──────────────────────────
|
|
494
|
+
// docs/inline-editing-plan.md §5.3 / ADR 0009. A block split across pages
|
|
495
|
+
// exists as MULTIPLE DOM fragments that duplicate every data attribute
|
|
496
|
+
// (§3.5's split-fragment gotcha applies here too) — `data-source-range` is
|
|
497
|
+
// duplicated onto every fragment identically, so `{chapter, range}` groups
|
|
498
|
+
// them (protocol v5 dropped `data-ref` from the wire contract entirely: the
|
|
499
|
+
// native viewer never mints one at all — it never clones, so there is
|
|
500
|
+
// nothing to give a shared identity to — and a source range already
|
|
501
|
+
// uniquely identifies one block).
|
|
502
|
+
|
|
503
|
+
function rangedBlocks() {
|
|
504
|
+
return Array.from(document.querySelectorAll('[data-source-range]'));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function rangedBlocksInChapter(chapter) {
|
|
508
|
+
if (!chapter) return rangedBlocks();
|
|
509
|
+
return rangedBlocks().filter(function (el) { return chapterOf(el) === chapter; });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function blocksMatchingRange(chapter, range) {
|
|
513
|
+
if (!range) return [];
|
|
514
|
+
return rangedBlocksInChapter(chapter).filter(function (el) {
|
|
515
|
+
var r = sourceRangeOf(el);
|
|
516
|
+
return r && r[0] === range[0] && r[1] === range[1];
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// getRectsFor(): the viewer never clones, so a spec resolves to AT MOST ONE
|
|
521
|
+
// element. Its fragment rects come straight from getClientRects() — a block
|
|
522
|
+
// can still visually span pages if the browser's own multicol layout breaks
|
|
523
|
+
// it there. Resolve each rect against the sheet it actually intersects;
|
|
524
|
+
// pageOf(el) can only identify the element's starting fragmentainer.
|
|
525
|
+
function nativeRectsFor(spec) {
|
|
526
|
+
spec = spec || {};
|
|
527
|
+
var el = blocksMatchingRange(spec.chapter, spec.range)[0] || null;
|
|
528
|
+
if (!el) return { rects: [] };
|
|
529
|
+
var geometryEl = el.tagName && el.tagName.toLowerCase() === 'code' &&
|
|
530
|
+
el.parentElement && el.parentElement.tagName.toLowerCase() === 'pre'
|
|
531
|
+
? el.parentElement
|
|
532
|
+
: el;
|
|
533
|
+
if (pages.length === 0) refreshPages();
|
|
534
|
+
var fallbackPage = pageIndexOf(el);
|
|
535
|
+
var raw = geometryEl.getClientRects ? Array.from(geometryEl.getClientRects()) : [];
|
|
536
|
+
if (!raw.length) {
|
|
537
|
+
var r0 = plainRect(geometryEl);
|
|
538
|
+
raw = r0 ? [r0] : [];
|
|
539
|
+
}
|
|
540
|
+
var rects = raw.map(function (r) {
|
|
541
|
+
var page = fallbackPage;
|
|
542
|
+
var bestArea = 0;
|
|
543
|
+
for (var i = 0; i < pages.length; i++) {
|
|
544
|
+
var sheetRect = pages[i].getBoundingClientRect();
|
|
545
|
+
var width = Math.min(r.right, sheetRect.right) - Math.max(r.left, sheetRect.left);
|
|
546
|
+
var height = Math.min(r.bottom, sheetRect.bottom) - Math.max(r.top, sheetRect.top);
|
|
547
|
+
var area = width > 0 && height > 0 ? width * height : 0;
|
|
548
|
+
if (area > bestArea) {
|
|
549
|
+
bestArea = area;
|
|
550
|
+
page = i + 1;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return { top: r.top, left: r.left, width: r.width, height: r.height, page: page };
|
|
554
|
+
});
|
|
555
|
+
return { rects: rects };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
var api = {
|
|
559
|
+
getTotalPages: function () { refreshPages(); return pages.length; },
|
|
560
|
+
getCurrentPage: function () { return currentPage; },
|
|
561
|
+
goToPage: function (n) {
|
|
562
|
+
refreshPages();
|
|
563
|
+
currentPage = clampPage(n);
|
|
564
|
+
scrollToCurrentPage();
|
|
565
|
+
return api.notifyPageChange();
|
|
566
|
+
},
|
|
567
|
+
getPageDimensions: function () {
|
|
568
|
+
refreshPages();
|
|
569
|
+
var page = pages[0] || null;
|
|
570
|
+
if (!page) return null;
|
|
571
|
+
var width = page.offsetWidth;
|
|
572
|
+
if (currentViewMode !== 'single') {
|
|
573
|
+
var runs = Array.from(document.querySelectorAll('.gp-run'));
|
|
574
|
+
for (var i = 0; i < runs.length; i++) {
|
|
575
|
+
var gap = parseFloat(getComputedStyle(runs[i]).getPropertyValue('--gp-sheet-gap')) || 0;
|
|
576
|
+
var runSheets = Array.from(runs[i].querySelectorAll('.gp-sheet'));
|
|
577
|
+
if (runSheets.length) {
|
|
578
|
+
var left = 0;
|
|
579
|
+
var right = page.offsetWidth;
|
|
580
|
+
for (var j = 0; j < runSheets.length; j++) {
|
|
581
|
+
left = Math.min(left, runSheets[j].offsetLeft || 0);
|
|
582
|
+
right = Math.max(right, (runSheets[j].offsetLeft || 0) + (runSheets[j].offsetWidth || 0));
|
|
583
|
+
}
|
|
584
|
+
width = Math.max(width, right - left);
|
|
585
|
+
} else {
|
|
586
|
+
width = Math.max(width, Math.max(0, (runs[i].offsetWidth || 0) - gap));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
width: width,
|
|
592
|
+
height: page.offsetHeight,
|
|
593
|
+
// The host iframe's clientWidth includes this document's vertical
|
|
594
|
+
// scrollbar. Report the actual layout viewport so fit-width can center
|
|
595
|
+
// within the pixels available to the book.
|
|
596
|
+
viewportWidth: document.documentElement.clientWidth
|
|
597
|
+
};
|
|
598
|
+
},
|
|
599
|
+
firstPage: function () { return api.goToPage(1); },
|
|
600
|
+
prevPage: function (mode) { return api.goToPage(currentPage - pageStep(mode)); },
|
|
601
|
+
nextPage: function (mode) { return api.goToPage(currentPage + pageStep(mode)); },
|
|
602
|
+
lastPage: function () { refreshPages(); return api.goToPage(pages.length); },
|
|
603
|
+
setViewMode: function (mode, silent) {
|
|
604
|
+
preserveViewport(function () {
|
|
605
|
+
currentViewMode = mode || 'two-column';
|
|
606
|
+
document.body.classList.remove('view-single', 'view-spread', 'view-two-column');
|
|
607
|
+
if (mode) document.body.classList.add('view-' + mode);
|
|
608
|
+
// `pageStep()` above makes next/prevPage step by 1 or 2 book pages.
|
|
609
|
+
// `setSpread()` does the real relayout: `column-wrap: wrap` wraps each
|
|
610
|
+
// chapter's columns into a 2-column grid so content, not just sheet
|
|
611
|
+
// chrome, actually moves into pairs — see fragment.ts's
|
|
612
|
+
// `applySpreadMode`. No-ops to single-row on a browser without the
|
|
613
|
+
// capability (Firefox/Safari as of this writing).
|
|
614
|
+
if (window.Gutterpress && typeof window.Gutterpress.setSpread === 'function') {
|
|
615
|
+
window.Gutterpress.setSpread(currentViewMode !== 'single');
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
if (!silent) return api.notifyPageChange();
|
|
619
|
+
},
|
|
620
|
+
setZoom: function (z, silent) {
|
|
621
|
+
preserveViewport(function () {
|
|
622
|
+
// Embedded previews have one zoom owner. Clear the standalone
|
|
623
|
+
// viewer's narrow-screen fit before applying the host scale.
|
|
624
|
+
document.body.style.removeProperty('--gutterpress-fit-zoom');
|
|
625
|
+
document.documentElement.style.setProperty('--gutterpress-zoom', z);
|
|
626
|
+
});
|
|
627
|
+
if (!silent) api.notifyPageChange();
|
|
628
|
+
},
|
|
629
|
+
toggleDebugMode: function () {
|
|
630
|
+
debugMode = !debugMode;
|
|
631
|
+
document.body.classList.toggle('debug', debugMode);
|
|
632
|
+
if (window.Gutterpress && window.Gutterpress.decoration) {
|
|
633
|
+
window.Gutterpress.decoration.setDesigner(debugMode);
|
|
634
|
+
}
|
|
635
|
+
return debugMode;
|
|
636
|
+
},
|
|
637
|
+
notifyPageChange: function () {
|
|
638
|
+
var detail = { currentPage: api.getCurrentPage(), totalPages: pages.length };
|
|
639
|
+
window.dispatchEvent(new CustomEvent('pageChanged', { detail: detail }));
|
|
640
|
+
return detail;
|
|
641
|
+
},
|
|
642
|
+
notifyRenderingComplete: function () {
|
|
643
|
+
window.dispatchEvent(new CustomEvent('renderingComplete', {
|
|
644
|
+
detail: { totalPages: pages.length }
|
|
645
|
+
}));
|
|
646
|
+
},
|
|
647
|
+
// Re-read the page list and recompute the current page from the scroll
|
|
648
|
+
// position, then notify. Hosts may call this after replacing paginated DOM
|
|
649
|
+
// so cached page counters and scroll synchronization stay current.
|
|
650
|
+
refresh: function () {
|
|
651
|
+
refreshPages();
|
|
652
|
+
currentPage = detectVisiblePage();
|
|
653
|
+
recordVisibleSource();
|
|
654
|
+
return api.notifyPageChange();
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
// ── ADR 0005 generic primitives ─────────────────────────────────────────
|
|
658
|
+
// Bumped whenever a command/event is added so a hot-updated SPA can
|
|
659
|
+
// feature-detect against an older bundled lib.
|
|
660
|
+
// v6 (WORK PACKAGE B item 2): getRectsFor()/setEditMask() dropped the
|
|
661
|
+
// {ref} form entirely — {chapter, range} is the only target shape now
|
|
662
|
+
// (the native viewer never mints a ref — it never clones).
|
|
663
|
+
getProtocolVersion: function () { return 6; },
|
|
664
|
+
|
|
665
|
+
// Resolve the annotated element/selection at a viewport point (protocol
|
|
666
|
+
// v4). Pure read; see buildContextTarget() above for the full contract.
|
|
667
|
+
getContextTargetAt: function (spec) {
|
|
668
|
+
spec = spec || {};
|
|
669
|
+
return buildContextTarget(elementAtPoint(spec.x, spec.y));
|
|
670
|
+
},
|
|
671
|
+
|
|
672
|
+
// All fragment rects for one logical block (protocol v6, §5.3), targeted
|
|
673
|
+
// by {chapter, range}. Pure read; never mutates the DOM. Plain,
|
|
674
|
+
// JSON-cloneable objects only (§3.5) — no DOMRect instances.
|
|
675
|
+
getRectsFor: function (spec) {
|
|
676
|
+
return nativeRectsFor(spec);
|
|
677
|
+
},
|
|
678
|
+
|
|
679
|
+
// Toggle a masking class on EVERY fragment of a block ({chapter, range}
|
|
680
|
+
// match, protocol v6, §5.1/§5.3), plus a scroll lock on the book document
|
|
681
|
+
// element. Purely cosmetic and fully reversible — nothing here may touch
|
|
682
|
+
// anything layout-affecting; see the class definitions below and ADR
|
|
683
|
+
// 0009. `masked: false` always removes the lock class too, even if this
|
|
684
|
+
// particular range has zero live fragments (e.g. called defensively
|
|
685
|
+
// during teardown after a splice) — it is a document-level toggle, not
|
|
686
|
+
// scoped per-block, and there is at most one overlay open at a time.
|
|
687
|
+
setEditMask: function (spec) {
|
|
688
|
+
spec = spec || {};
|
|
689
|
+
var els = blocksMatchingRange(spec.chapter, spec.range);
|
|
690
|
+
for (var i = 0; i < els.length; i++) {
|
|
691
|
+
if (spec.masked) els[i].classList.add('gutterpress-edit-mask');
|
|
692
|
+
else els[i].classList.remove('gutterpress-edit-mask');
|
|
693
|
+
}
|
|
694
|
+
var root = document.documentElement;
|
|
695
|
+
if (root && root.classList) {
|
|
696
|
+
if (spec.masked) root.classList.add('gutterpress-edit-scroll-lock');
|
|
697
|
+
else root.classList.remove('gutterpress-edit-scroll-lock');
|
|
698
|
+
}
|
|
699
|
+
return { count: els.length };
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
// Publish any debounced reader movement before a host atomically replaces
|
|
703
|
+
// this frame. `silent` lets the shell relay the returned event itself so a
|
|
704
|
+
// queued postMessage from the outgoing frame cannot arrive after the swap.
|
|
705
|
+
flushScroll: function (silent) {
|
|
706
|
+
if (scrollTimer) clearTimeout(scrollTimer);
|
|
707
|
+
scrollTimer = null;
|
|
708
|
+
return publishScrollPosition(true, silent === true);
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
// Heading tree with page + source line — powers chapter jump (UX-013), TOC,
|
|
712
|
+
// minimap, scrollspy. Page math needs same-origin engine access, so it
|
|
713
|
+
// lives here rather than being derived host-side.
|
|
714
|
+
getOutline: function () {
|
|
715
|
+
refreshPages();
|
|
716
|
+
var hs = Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6'));
|
|
717
|
+
var out = [];
|
|
718
|
+
for (var i = 0; i < hs.length; i++) {
|
|
719
|
+
var h = hs[i];
|
|
720
|
+
var text = (h.textContent || '').trim();
|
|
721
|
+
if (!text) continue;
|
|
722
|
+
out.push({
|
|
723
|
+
level: parseInt(h.tagName.charAt(1), 10),
|
|
724
|
+
text: text,
|
|
725
|
+
id: h.id || null,
|
|
726
|
+
sourceLine: lineOf(h),
|
|
727
|
+
chapter: chapterOf(h),
|
|
728
|
+
page: pageIndexOf(h),
|
|
729
|
+
index: i
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
return out;
|
|
733
|
+
},
|
|
734
|
+
|
|
735
|
+
// Single anchored-jump primitive: target {line}|{id}|{selector}|{page}.
|
|
736
|
+
// Returns the resolved {page, sourceLine}. Suppresses the scroll-driven
|
|
737
|
+
// pageChanged/sourceLineChanged echo so host-driven jumps don't loop back.
|
|
738
|
+
scrollTo: function (target, opts) {
|
|
739
|
+
opts = opts || {};
|
|
740
|
+
if (typeof target === 'number') target = { line: target };
|
|
741
|
+
var el = null;
|
|
742
|
+
var fraction = 0;
|
|
743
|
+
var nextEl = null;
|
|
744
|
+
if (target && target.line != null && target.selector == null && target.id == null && target.page == null) {
|
|
745
|
+
var pos = resolveLinePosition(target);
|
|
746
|
+
if (pos) {
|
|
747
|
+
el = pos.el;
|
|
748
|
+
if (pos.nextEl && pos.nextLine != null && pos.nextLine > pos.line) {
|
|
749
|
+
nextEl = pos.nextEl;
|
|
750
|
+
fraction = (Number(target.line) - pos.line) / (pos.nextLine - pos.line);
|
|
751
|
+
fraction = Math.max(0, Math.min(1, fraction));
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
el = resolveTarget(target);
|
|
756
|
+
}
|
|
757
|
+
if (!el) return null;
|
|
758
|
+
ignoreScrollUntil = Date.now() + 350;
|
|
759
|
+
var behavior = opts.smooth ? 'smooth' : 'instant';
|
|
760
|
+
el.scrollIntoView({
|
|
761
|
+
behavior: fraction > 0 ? 'instant' : behavior,
|
|
762
|
+
block: opts.block || 'start',
|
|
763
|
+
inline: 'nearest'
|
|
764
|
+
});
|
|
765
|
+
if (fraction > 0 && nextEl) {
|
|
766
|
+
// Interpolate within the block: nudge the scroll by the target line's
|
|
767
|
+
// fractional distance between this block's top and the NEXT annotated
|
|
768
|
+
// block's top. Only when the next block is genuinely lower on screen —
|
|
769
|
+
// a facing-page or next-column neighbour (top <= ours) degrades to the
|
|
770
|
+
// plain block-top scroll, which is the pre-existing behaviour.
|
|
771
|
+
var top = el.getBoundingClientRect().top;
|
|
772
|
+
var nextTop = nextEl.getBoundingClientRect().top;
|
|
773
|
+
if (nextTop > top) {
|
|
774
|
+
window.scrollBy({ top: fraction * (nextTop - top), behavior: behavior });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
var page = pageIndexOf(el);
|
|
778
|
+
if (page) currentPage = page;
|
|
779
|
+
if (!opts.smooth) recordVisibleSource();
|
|
780
|
+
return { page: page || currentPage, sourceLine: lineOf(el) };
|
|
781
|
+
},
|
|
782
|
+
|
|
783
|
+
// Source line + page of the block at the top of the viewport (host scroll
|
|
784
|
+
// position read for preview->editor sync).
|
|
785
|
+
getVisibleSource: function () {
|
|
786
|
+
if (pages.length === 0) refreshPages();
|
|
787
|
+
var pos = visibleSourcePosition();
|
|
788
|
+
return pos ? { sourceLine: pos.line, chapter: chapterOf(pos.el), page: detectVisiblePage() } : null;
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
// Generic, read-only DOM extraction. fields: 'text'|'id'|'sourceLine'|
|
|
792
|
+
// 'page'|'tag'|'rectTop'|{attr:'name'}. No eval, no innerHTML — this is the
|
|
793
|
+
// forward-compat hook that lets the SPA build find/figure-list/etc. with no
|
|
794
|
+
// further lib change.
|
|
795
|
+
queryDom: function (spec) {
|
|
796
|
+
spec = spec || {};
|
|
797
|
+
if (!spec.selector) return [];
|
|
798
|
+
var fields = spec.fields || ['text'];
|
|
799
|
+
var els;
|
|
800
|
+
try { els = Array.from(document.querySelectorAll(spec.selector)); }
|
|
801
|
+
catch (_e) { return []; }
|
|
802
|
+
if (spec.limit && els.length > spec.limit) els = els.slice(0, spec.limit);
|
|
803
|
+
return els.map(function (el) {
|
|
804
|
+
var row = {};
|
|
805
|
+
for (var i = 0; i < fields.length; i++) {
|
|
806
|
+
var f = fields[i];
|
|
807
|
+
if (f === 'text') row.text = (el.textContent || '').trim();
|
|
808
|
+
else if (f === 'id') row.id = el.id || null;
|
|
809
|
+
else if (f === 'sourceLine') row.sourceLine = lineOf(el);
|
|
810
|
+
else if (f === 'page') row.page = pageIndexOf(el);
|
|
811
|
+
else if (f === 'chapter') row.chapter = chapterOf(el);
|
|
812
|
+
else if (f === 'tag') row.tag = el.tagName.toLowerCase();
|
|
813
|
+
else if (f === 'rectTop') row.rectTop = el.getBoundingClientRect().top;
|
|
814
|
+
else if (f && typeof f === 'object' && f.attr) row['attr:' + f.attr] = el.getAttribute(f.attr);
|
|
815
|
+
}
|
|
816
|
+
return row;
|
|
817
|
+
});
|
|
818
|
+
},
|
|
819
|
+
|
|
820
|
+
// Toggle a marker class on matched elements (DOM write inside the frame).
|
|
821
|
+
// spec: {line?|id?|selector?, group?, scroll?, transient?, transientMs?}.
|
|
822
|
+
// Powers find-in-page, editor-cursor echo, annotations.
|
|
823
|
+
highlight: function (spec) {
|
|
824
|
+
spec = spec || {};
|
|
825
|
+
var group = spec.group || 'default';
|
|
826
|
+
var els = [];
|
|
827
|
+
if (spec.selector) { try { els = Array.from(document.querySelectorAll(spec.selector)); } catch (_e) {} }
|
|
828
|
+
else if (spec.id) { var byId = document.getElementById(spec.id); if (byId) els = [byId]; }
|
|
829
|
+
else if (spec.line != null) { var one = resolveTarget({ line: spec.line }); if (one) els = [one]; }
|
|
830
|
+
for (var i = 0; i < els.length; i++) {
|
|
831
|
+
els[i].classList.add('gutterpress-hl');
|
|
832
|
+
els[i].setAttribute('data-gutterpress-hl-group', group);
|
|
833
|
+
}
|
|
834
|
+
if (spec.scroll && els[0]) {
|
|
835
|
+
ignoreScrollUntil = Date.now() + 350;
|
|
836
|
+
els[0].scrollIntoView({ behavior: 'instant', block: 'center', inline: 'nearest' });
|
|
837
|
+
recordVisibleSource();
|
|
838
|
+
}
|
|
839
|
+
if (spec.transient && els.length) {
|
|
840
|
+
setTimeout(function () {
|
|
841
|
+
for (var j = 0; j < els.length; j++) {
|
|
842
|
+
if (els[j].getAttribute('data-gutterpress-hl-group') === group) {
|
|
843
|
+
els[j].classList.remove('gutterpress-hl');
|
|
844
|
+
els[j].removeAttribute('data-gutterpress-hl-group');
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}, spec.transientMs || 1200);
|
|
848
|
+
}
|
|
849
|
+
return { count: els.length };
|
|
850
|
+
},
|
|
851
|
+
|
|
852
|
+
// ── Find in book (the app's Ctrl+F) ─────────────────────────────────
|
|
853
|
+
// window.find() runs inside THIS frame, so it can only ever match book
|
|
854
|
+
// content — never the app's toolbar chrome or the editor. Each call with
|
|
855
|
+
// the same query advances the native selection (wrapping at the ends)
|
|
856
|
+
// and Chromium scrolls the match into view; a NEW query restarts from
|
|
857
|
+
// the top and counts total occurrences once (case-insensitive).
|
|
858
|
+
find: function (query, backwards) {
|
|
859
|
+
query = String(query == null ? '' : query);
|
|
860
|
+
if (!query) return api.clearFind();
|
|
861
|
+
if (query !== findQuery) {
|
|
862
|
+
findQuery = query;
|
|
863
|
+
var text = (document.body.textContent || '').toLowerCase();
|
|
864
|
+
var needle = query.toLowerCase();
|
|
865
|
+
findTotal = 0;
|
|
866
|
+
var idx = 0;
|
|
867
|
+
while (findTotal < 10000) {
|
|
868
|
+
idx = text.indexOf(needle, idx);
|
|
869
|
+
if (idx === -1) break;
|
|
870
|
+
findTotal++;
|
|
871
|
+
idx += needle.length;
|
|
872
|
+
}
|
|
873
|
+
var sel = window.getSelection();
|
|
874
|
+
if (sel) sel.removeAllRanges();
|
|
875
|
+
}
|
|
876
|
+
var found = false;
|
|
877
|
+
try {
|
|
878
|
+
found = window.find(query, false, !!backwards, true, false, false, false);
|
|
879
|
+
} catch (_e) {
|
|
880
|
+
found = false;
|
|
881
|
+
}
|
|
882
|
+
return { found: !!found, total: findTotal };
|
|
883
|
+
},
|
|
884
|
+
|
|
885
|
+
clearFind: function () {
|
|
886
|
+
findQuery = '';
|
|
887
|
+
findTotal = 0;
|
|
888
|
+
var sel = window.getSelection();
|
|
889
|
+
if (sel) sel.removeAllRanges();
|
|
890
|
+
return { found: false, total: 0 };
|
|
891
|
+
},
|
|
892
|
+
|
|
893
|
+
clearHighlights: function (group) {
|
|
894
|
+
var sel = group ? '.gutterpress-hl[data-gutterpress-hl-group="' + group + '"]' : '.gutterpress-hl';
|
|
895
|
+
var els = Array.from(document.querySelectorAll(sel));
|
|
896
|
+
for (var i = 0; i < els.length; i++) {
|
|
897
|
+
els[i].classList.remove('gutterpress-hl');
|
|
898
|
+
els[i].removeAttribute('data-gutterpress-hl-group');
|
|
899
|
+
}
|
|
900
|
+
return { cleared: els.length };
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
window.previewAPI = api;
|
|
905
|
+
|
|
906
|
+
// Default highlight style (preview-only; never part of the PDF build path).
|
|
907
|
+
// Guarded so a minimal/headless DOM (e.g. unit-test harness) can't crash here.
|
|
908
|
+
if (typeof document.createElement === 'function') {
|
|
909
|
+
try {
|
|
910
|
+
var hlStyle = document.createElement('style');
|
|
911
|
+
hlStyle.textContent =
|
|
912
|
+
'.gutterpress-hl{outline:2px solid var(--gutterpress-hl-color,#4ea1ff);outline-offset:2px;' +
|
|
913
|
+
'background:var(--gutterpress-hl-bg,rgba(78,161,255,.14));' +
|
|
914
|
+
'transition:outline-color .2s,background .2s;}';
|
|
915
|
+
(document.head || document.documentElement).appendChild(hlStyle);
|
|
916
|
+
} catch (_e) { /* non-fatal: highlight just renders unstyled */ }
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Block-overlay mask + scroll-lock style (protocol v5, plan §5.1/§5.3).
|
|
920
|
+
// Preview-only, never part of the PDF build path. Purely cosmetic: dims the
|
|
921
|
+
// masked fragment(s) so stale rendered text doesn't show behind/beside the
|
|
922
|
+
// overlay, and disables the book document's own scroll while an overlay is
|
|
923
|
+
// open (the overlay is positioned in host-SPA coordinates from a rect
|
|
924
|
+
// snapshot; an unlocked scroll would silently drift it over unrelated
|
|
925
|
+
// content — see BlockOverlayController). Exact visual treatment (dim vs.
|
|
926
|
+
// blank) is a placeholder pending design review (plan §7.6 open item).
|
|
927
|
+
if (typeof document.createElement === 'function') {
|
|
928
|
+
try {
|
|
929
|
+
var maskStyle = document.createElement('style');
|
|
930
|
+
maskStyle.textContent =
|
|
931
|
+
'.gutterpress-edit-mask{opacity:.2;filter:saturate(.4);pointer-events:none;' +
|
|
932
|
+
'transition:opacity .12s,filter .12s;}' +
|
|
933
|
+
'html.gutterpress-edit-scroll-lock{overflow:hidden !important;}';
|
|
934
|
+
(document.head || document.documentElement).appendChild(maskStyle);
|
|
935
|
+
} catch (_e) { /* non-fatal: mask just renders unstyled */ }
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Click-to-source: emit elementActivated when the user clicks a source-mapped
|
|
939
|
+
// block. Never preventDefault (links/selection keep working); the host decides
|
|
940
|
+
// whether to act. (ADR 0005)
|
|
941
|
+
if (typeof document.addEventListener === 'function') {
|
|
942
|
+
document.addEventListener('click', function (e) {
|
|
943
|
+
var el = e.target && e.target.closest ? e.target.closest('[data-source-line]') : null;
|
|
944
|
+
if (!el) return;
|
|
945
|
+
window.dispatchEvent(new CustomEvent('elementActivated', {
|
|
946
|
+
detail: { sourceLine: lineOf(el), chapter: chapterOf(el), id: el.id || null, tag: el.tagName.toLowerCase() }
|
|
947
|
+
}));
|
|
948
|
+
}, true);
|
|
949
|
+
|
|
950
|
+
// Context menu (protocol v4, docs/inline-editing-plan.md §3.1). Both the
|
|
951
|
+
// mouse and keyboard paths dispatch the same contextMenuRequested window
|
|
952
|
+
// event carrying the getContextTargetAt() payload plus the viewport
|
|
953
|
+
// x/y and `via`.
|
|
954
|
+
//
|
|
955
|
+
// preventDefault() ONLY when kind !== 'none' — right-clicks on page
|
|
956
|
+
// furniture (margin boxes, running headers, page numbers) keep native
|
|
957
|
+
// behavior: that text is selectable/copyable, and killing native copy
|
|
958
|
+
// with no replacement menu would be a strict regression. No event is
|
|
959
|
+
// dispatched for 'none' either.
|
|
960
|
+
document.addEventListener('contextmenu', function (e) {
|
|
961
|
+
var detail = api.getContextTargetAt({ x: e.clientX, y: e.clientY });
|
|
962
|
+
if (detail.kind === 'none') return;
|
|
963
|
+
e.preventDefault();
|
|
964
|
+
detail.x = e.clientX;
|
|
965
|
+
detail.y = e.clientY;
|
|
966
|
+
detail.via = 'mouse';
|
|
967
|
+
window.dispatchEvent(new CustomEvent('contextMenuRequested', { detail: detail }));
|
|
968
|
+
}, true);
|
|
969
|
+
|
|
970
|
+
// Shift+F10 / the dedicated ContextMenu key. This listener MUST live
|
|
971
|
+
// inside the book iframe: keyboard events targeted at a focused element
|
|
972
|
+
// in a cross-origin iframe never reach the parent SPA, so an SPA-side
|
|
973
|
+
// listener cannot implement this.
|
|
974
|
+
document.addEventListener('keydown', function (e) {
|
|
975
|
+
var isShiftF10 = e.shiftKey && e.key === 'F10';
|
|
976
|
+
var isMenuKey = e.key === 'ContextMenu';
|
|
977
|
+
if (!isShiftF10 && !isMenuKey) return;
|
|
978
|
+
var anchor = keyboardAnchorPoint();
|
|
979
|
+
var detail = api.getContextTargetAt({ x: anchor.x, y: anchor.y });
|
|
980
|
+
detail.x = anchor.x;
|
|
981
|
+
detail.y = anchor.y;
|
|
982
|
+
detail.via = 'keyboard';
|
|
983
|
+
e.preventDefault();
|
|
984
|
+
window.dispatchEvent(new CustomEvent('contextMenuRequested', { detail: detail }));
|
|
985
|
+
}, true);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Scroll tracking
|
|
989
|
+
var scrollTimer = null;
|
|
990
|
+
var viewportFrame = null;
|
|
991
|
+
function publishScrollPosition(force, silent) {
|
|
992
|
+
scrollTimer = null;
|
|
993
|
+
var remainingGuard = ignoreScrollUntil - Date.now();
|
|
994
|
+
if (!force && remainingGuard > 0) {
|
|
995
|
+
scrollTimer = setTimeout(publishScrollPosition, remainingGuard);
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
if (pages.length === 0) refreshPages();
|
|
999
|
+
if (pages.length === 0) return null;
|
|
1000
|
+
var page = detectVisiblePage();
|
|
1001
|
+
if (page !== currentPage) {
|
|
1002
|
+
currentPage = page;
|
|
1003
|
+
if (!silent) api.notifyPageChange();
|
|
1004
|
+
}
|
|
1005
|
+
// Emit finer-grained source position for editor sync (ADR 0005).
|
|
1006
|
+
var pos = visibleSourcePosition();
|
|
1007
|
+
var sl = pos ? pos.line : null;
|
|
1008
|
+
var chapter = pos ? chapterOf(pos.el) : null;
|
|
1009
|
+
if (sl != null && (sl !== lastSourceLine || chapter !== lastSourceChapter)) {
|
|
1010
|
+
lastSourceLine = sl;
|
|
1011
|
+
lastSourceChapter = chapter;
|
|
1012
|
+
var detail = { sourceLine: sl, chapter: chapter, page: page };
|
|
1013
|
+
if (!silent) {
|
|
1014
|
+
window.dispatchEvent(new CustomEvent('sourceLineChanged', { detail: detail }));
|
|
1015
|
+
}
|
|
1016
|
+
return detail;
|
|
1017
|
+
}
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
function scheduleViewportChanged() {
|
|
1021
|
+
if (!viewportFrame) {
|
|
1022
|
+
var schedule = typeof window.requestAnimationFrame === 'function'
|
|
1023
|
+
? window.requestAnimationFrame.bind(window)
|
|
1024
|
+
: function (fn) { return setTimeout(fn, 0); };
|
|
1025
|
+
viewportFrame = schedule(function () {
|
|
1026
|
+
viewportFrame = null;
|
|
1027
|
+
window.dispatchEvent(new CustomEvent('viewportChanged', { detail: {} }));
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
window.addEventListener('scroll', function () {
|
|
1032
|
+
scheduleViewportChanged();
|
|
1033
|
+
if (scrollTimer) clearTimeout(scrollTimer);
|
|
1034
|
+
scrollTimer = setTimeout(publishScrollPosition, 150);
|
|
1035
|
+
});
|
|
1036
|
+
window.addEventListener('resize', scheduleViewportChanged);
|
|
1037
|
+
|
|
1038
|
+
// Reset to page 1, scroll to top, and announce completion once the native
|
|
1039
|
+
// viewer's pagination finishes.
|
|
1040
|
+
function onRenderingComplete() {
|
|
1041
|
+
// Latch: a host that attaches its 'renderingComplete' listener after the
|
|
1042
|
+
// event already fired (preview-shell.js binds on the iframe's `load`, which
|
|
1043
|
+
// the native viewer's DOMContentLoaded mount can beat) has no other way to
|
|
1044
|
+
// know pagination is done.
|
|
1045
|
+
window.__GUTTERPRESS_RENDERED__ = true;
|
|
1046
|
+
refreshPages();
|
|
1047
|
+
currentPage = 1;
|
|
1048
|
+
ignoreScrollUntil = Date.now() + 300;
|
|
1049
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
|
1050
|
+
recordVisibleSource();
|
|
1051
|
+
console.log('Gutterpress engine rendered ' + pages.length + ' pages');
|
|
1052
|
+
api.notifyRenderingComplete();
|
|
1053
|
+
setTimeout(api.notifyPageChange, 0);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// The Gutterpress engine viewer dispatches this when its pagination
|
|
1057
|
+
// completes (engine/viewer/index.ts's mount()).
|
|
1058
|
+
window.addEventListener('gp:layout', onRenderingComplete);
|
|
1059
|
+
})();
|