gutterpress 0.9.0-alpha.2 → 0.10.0-alpha.3

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