@papyrus-sdk/ui-react-native 0.2.35-beta.9 → 0.2.36

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.
@@ -109,6 +109,67 @@
109
109
  /* @papyrus-comic-runtime:end */
110
110
 
111
111
  (function () {
112
+ /* @papyrus-epub-reader:start */
113
+ (function(root){
114
+ function mode(value){return value==='chapter-scroll'?'chapter-scroll':'paged';}
115
+ function validCfi(value){return typeof value==='string' && value.length<=4096 && /^epubcfi\([^\n<>]+\)$/.test(value);}
116
+ function options(value){return {manager:'default',flow:mode(value)==='paged'?'paginated':'scrolled-doc',spread:'none',width:'100%',height:'100%',allowScriptedContent:false};}
117
+ function gesture(start,end,state){
118
+ if(!start || !end || state.selected || state.interactive || state.multitouch)return null;
119
+ var dx=end.x-start.x,dy=end.y-start.y,elapsed=end.time-start.time;
120
+ if(state.mode==='paged' && Math.abs(dx)>=48 && Math.abs(dx)>Math.abs(dy)*1.5 && elapsed>=0 && elapsed<1500)return (dx<0)!==Boolean(state.rtl)?'next':'prev';
121
+ if(elapsed>=0 && elapsed<450 && Math.hypot(dx,dy)<=12)return 'tap';
122
+ return null;
123
+ }
124
+ function location(raw,count,readingMode,geometry){
125
+ var start=raw && raw.start;if(!start || !validCfi(start.cfi))return null;
126
+ var displayed=start.displayed||{},chapter=Math.max(1,(Number(start.index)||0)+1);
127
+ var visualPage=mode(readingMode)==='paged' && Number.isFinite(displayed.page)?displayed.page:null;
128
+ var total=mode(readingMode)==='paged' && Number.isFinite(displayed.total)?displayed.total:null;
129
+ var atStart=Boolean(raw.atStart),atEnd=Boolean(raw.atEnd);
130
+ // Chromium may round a column offset down by a fraction of a CSS pixel.
131
+ // Keep epub.js authoritative outside a 1px boundary, and for RTL layouts.
132
+ if(visualPage!==null && total>0 && geometry && geometry.direction==='ltr' && geometry.pageWidth>0 && Number.isFinite(geometry.offset)){
133
+ var boundary=Math.round(geometry.offset/geometry.pageWidth);
134
+ if(Math.abs(geometry.offset-boundary*geometry.pageWidth)<=1){
135
+ visualPage=Math.min(total,Math.max(1,boundary+1));
136
+ atStart=chapter===(geometry.firstIndex ?? 0)+1 && visualPage===1;
137
+ atEnd=chapter===(geometry.lastIndex ?? count-1)+1 && visualPage===total;
138
+ }
139
+ }
140
+ var fraction=total?Math.max(0,(visualPage-1)/total):0;
141
+ return {version:1,cfi:start.cfi,href:start.href||'',chapter:chapter,chapterCount:count,visualPage:visualPage,visualPageCount:total,progress:Math.min(1,Math.max(0,(chapter-1+fraction)/Math.max(1,count))),mode:mode(readingMode),atStart:atStart,atEnd:atEnd};
142
+ }
143
+ function installResizeAnchor(manager,readCfi){
144
+ var original=manager.resize,anchor=null;
145
+ function resize(width,height,target){
146
+ if(validCfi(target))anchor=target;
147
+ if(!anchor){var current=readCfi();if(validCfi(current))anchor=current;}
148
+ // epub.js accepts a logical target as its third argument. Keep it across
149
+ // keyboard/dialog reflows instead of adopting each new viewport start.
150
+ return original.call(this,width,height,anchor || target);
151
+ }
152
+ manager.resize=resize;
153
+ return {cfi:function(){return anchor;},invalidate:function(){anchor=null;},destroy:function(){if(manager.resize===resize)manager.resize=original;anchor=null;}};
154
+ }
155
+ function install(contents,config){
156
+ var doc=contents.document,win=contents.window,start=null,blocked=false;
157
+ function point(e){var t=e.changedTouches&&e.changedTouches[0]||e.touches&&e.touches[0];return t?{x:t.clientX,y:t.clientY,time:Date.now()}:null;}
158
+ function selected(){return Boolean(String(win.getSelection()||'').trim()) || config.blocked();}
159
+ function interactive(target){return Boolean(target && target.closest && target.closest('a,button,input,textarea,select,[contenteditable="true"],[data-papyrus-overlay],[data-papyrus-ink-capture]'));}
160
+ function begin(e){start=point(e);blocked=e.touches.length!==1 || selected() || interactive(e.target);}
161
+ function invalidateScroll(finish){if(config.current() && start && finish && config.mode()==='chapter-scroll' && !blocked && !selected() && Math.abs(finish.y-start.y)>12 && Math.abs(finish.y-start.y)>Math.abs(finish.x-start.x) && config.interact)config.interact();}
162
+ function move(e){if(e.touches.length!==1)blocked=true;invalidateScroll(point(e));}
163
+ function end(e){var finish=point(e);invalidateScroll(finish);var action=gesture(start,finish,{mode:config.mode(),rtl:config.rtl(),selected:selected(),interactive:blocked||interactive(e.target)||Boolean(config.annotationHit && config.annotationHit(point(e))),multitouch:blocked});start=null;if(!config.current())return;if(action==='tap')config.tap();else if(action)config.turn(action==='next'?1:-1);}
164
+ function cancel(){start=null;blocked=true;}
165
+ function link(e){if(config.current() && e.target?.closest?.('a') && config.interact)config.interact();}
166
+ doc.addEventListener('touchstart',begin,{passive:true});doc.addEventListener('touchmove',move,{passive:true});doc.addEventListener('touchend',end,{passive:true});doc.addEventListener('touchcancel',cancel,{passive:true});doc.addEventListener('click',link);
167
+ return function(){doc.removeEventListener('touchstart',begin);doc.removeEventListener('touchmove',move);doc.removeEventListener('touchend',end);doc.removeEventListener('touchcancel',cancel);doc.removeEventListener('click',link);};
168
+ }
169
+ root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,installResizeAnchor:installResizeAnchor,install:install};
170
+ })(typeof window==='undefined'?globalThis:window);
171
+
172
+ /* @papyrus-epub-reader:end */
112
173
  /* @papyrus-epub-annotations:start */
113
174
  (function (root, factory) {
114
175
  if (typeof module === 'object' && module.exports) module.exports = factory();
@@ -120,6 +181,84 @@
120
181
  typeof anchor.href === 'string' && !/^(javascript|data):/i.test(anchor.href) &&
121
182
  typeof anchor.quote === 'string' && anchor.quote.length > 0 && anchor.quote.length <= 100000;
122
183
  }
184
+ function simplifyPath(points, tolerance, maxPoints) {
185
+ if (!Array.isArray(points) || points.length <= 2) return Array.isArray(points) ? points.slice() : [];
186
+ tolerance = Number.isFinite(tolerance) ? Math.max(0, tolerance) : 1.5;
187
+ maxPoints = Number.isSafeInteger(maxPoints) ? Math.max(2, maxPoints) : 512;
188
+ var keep = new Uint8Array(points.length); keep[0] = 1; keep[points.length - 1] = 1;
189
+ var stack = [[0, points.length - 1]], toleranceSquared = tolerance * tolerance;
190
+ while (stack.length) {
191
+ var interval = stack.pop(), first = interval[0], last = interval[1], a = points[first], b = points[last];
192
+ var dx = b.x - a.x, dy = b.y - a.y, lengthSquared = dx * dx + dy * dy, maxDistance = toleranceSquared, split = -1;
193
+ for (var i = first + 1; i < last; i++) {
194
+ var p = points[i], ratio = lengthSquared ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / lengthSquared)) : 0;
195
+ var px = a.x + ratio * dx, py = a.y + ratio * dy, ox = p.x - px, oy = p.y - py, distance = ox * ox + oy * oy;
196
+ if (distance > maxDistance) { maxDistance = distance; split = i; }
197
+ }
198
+ if (split >= 0) { keep[split] = 1; stack.push([first, split], [split, last]); }
199
+ }
200
+ var result = []; for (var j = 0; j < points.length; j++) if (keep[j]) result.push(points[j]);
201
+ if (result.length <= maxPoints) return result;
202
+ var capped = []; for (var k = 0; k < maxPoints; k++) capped.push(result[Math.round(k * (result.length - 1) / (maxPoints - 1))]);
203
+ return capped;
204
+ }
205
+ function compactGesturePoints(points, maxPoints) {
206
+ if (!Array.isArray(points)) return [];
207
+ maxPoints = Number.isSafeInteger(maxPoints) ? Math.max(2, maxPoints) : 1024;
208
+ if (points.length <= maxPoints) return points.slice();
209
+ var compacted = [];
210
+ for (var i = 0; i < maxPoints; i++) compacted.push(points[Math.round(i * (points.length - 1) / (maxPoints - 1))]);
211
+ return compacted;
212
+ }
213
+ function densifyPath(points, maxStep, maxPoints) {
214
+ if (!Array.isArray(points) || !points.length) return [];
215
+ if (points.length === 1) return points.slice();
216
+ maxStep = Number.isFinite(maxStep) ? Math.max(1, maxStep) : 12;
217
+ maxPoints = Number.isSafeInteger(maxPoints) ? Math.max(2, maxPoints) : 512;
218
+ var cumulative = [0], total = 0;
219
+ for (var i = 1; i < points.length; i++) {
220
+ if (![points[i-1].x,points[i-1].y,points[i].x,points[i].y].every(Number.isFinite)) return [];
221
+ total += Math.hypot(points[i].x-points[i-1].x, points[i].y-points[i-1].y);
222
+ cumulative.push(total);
223
+ }
224
+ if (!total) return [points[0], points[points.length-1]];
225
+ var intervals = Math.min(maxPoints-1, Math.max(1, Math.ceil(total/maxStep))), result = [], segment = 1;
226
+ for (var sample = 0; sample <= intervals; sample++) {
227
+ var distance = total*sample/intervals;
228
+ while (segment < cumulative.length-1 && cumulative[segment] < distance) segment++;
229
+ var start = points[segment-1], end = points[segment], span = cumulative[segment]-cumulative[segment-1];
230
+ var ratio = span ? (distance-cumulative[segment-1])/span : 0;
231
+ result.push({x:start.x+(end.x-start.x)*ratio,y:start.y+(end.y-start.y)*ratio});
232
+ }
233
+ return result;
234
+ }
235
+ function createSpatialIndex(entries, cellSize) {
236
+ cellSize = Number.isFinite(cellSize) ? Math.max(8, cellSize) : 64;
237
+ var cells = new Map();
238
+ entries.forEach(function(entry) {
239
+ var left = Math.floor(entry.left / cellSize), right = Math.floor(entry.right / cellSize);
240
+ var top = Math.floor(entry.top / cellSize), bottom = Math.floor(entry.bottom / cellSize);
241
+ for (var x = left; x <= right; x++) for (var y = top; y <= bottom; y++) {
242
+ var key = x + ':' + y, bucket = cells.get(key); if (!bucket) { bucket = []; cells.set(key, bucket); } bucket.push(entry);
243
+ }
244
+ });
245
+ return {cells:cells,cellSize:cellSize};
246
+ }
247
+ function queryNearestTextRect(index, x, y, maxDistance) {
248
+ if (!index || !Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(maxDistance) || maxDistance < 0) return null;
249
+ var size = index.cellSize, minX = Math.floor((x-maxDistance)/size), maxX = Math.floor((x+maxDistance)/size);
250
+ var minY = Math.floor((y-maxDistance)/size), maxY = Math.floor((y+maxDistance)/size), seen = new Set(), best = null;
251
+ for (var cx=minX; cx<=maxX; cx++) for (var cy=minY; cy<=maxY; cy++) {
252
+ var bucket = index.cells.get(cx+':'+cy); if (!bucket) continue;
253
+ bucket.forEach(function(entry) {
254
+ if (seen.has(entry)) return; seen.add(entry);
255
+ var nearestX = Math.max(entry.left,Math.min(entry.right,x)), nearestY = Math.max(entry.top,Math.min(entry.bottom,y));
256
+ var dx=x-nearestX,dy=y-nearestY,distance=Math.sqrt(dx*dx+dy*dy);
257
+ if (distance<=maxDistance && (!best || distance<best.distance)) best={entry:entry,distance:distance,x:nearestX,y:nearestY};
258
+ });
259
+ }
260
+ return best;
261
+ }
123
262
  function textNodes(document) {
124
263
  var walker = document.createTreeWalker(document.body, 4);
125
264
  var nodes = [], text = '', node;
@@ -169,13 +308,196 @@
169
308
  if(win.getComputedStyle(doc.body).position==='static')doc.body.style.position='relative';
170
309
  doc.body.appendChild(overlay);
171
310
  var registry = new Map(), hitRegions = [];
311
+ var inkRegistry = new Map(), rangeCache = new Map(), spatialIndex = null, activeGesture = null, inkConfig = {enabled:false,preset:'ink'};
312
+ var inkSvg = doc.createElementNS('http://www.w3.org/2000/svg','svg');
313
+ inkSvg.setAttribute('data-papyrus-ink-overlay','');
314
+ inkSvg.style.cssText = 'position:absolute;left:0;top:0;overflow:visible;pointer-events:none;z-index:1';
315
+ overlay.appendChild(inkSvg);
316
+ var inkCanvas = doc.createElement('canvas');
317
+ inkCanvas.dataset.papyrusInkCapture = '';
318
+ inkCanvas.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;z-index:2147483000;background:transparent;pointer-events:none;touch-action:none';
319
+ doc.body.appendChild(inkCanvas);
320
+ var inkContext = null; try { if(typeof win.CanvasRenderingContext2D==='function')inkContext = inkCanvas.getContext('2d'); } catch (_) {}
321
+ function resizeCanvas() {
322
+ var bodyRect=doc.body.getBoundingClientRect(), ratio = Math.max(1,Math.min(3,win.devicePixelRatio || 1));
323
+ var width=Math.max(1,Math.min(4096,doc.body.clientWidth||bodyRect.width||Math.min(4096,win.innerWidth||1)));
324
+ var height=Math.max(1,Math.min(4096,doc.body.clientHeight||bodyRect.height||Math.min(4096,win.innerHeight||1)));
325
+ inkCanvas.style.width=width+'px';inkCanvas.style.height=height+'px';
326
+ inkCanvas.width = Math.round(width*ratio); inkCanvas.height = Math.round(height*ratio);
327
+ if (inkContext) { inkContext.setTransform(ratio,0,0,ratio,0,0); inkContext.clearRect(0,0,width,height); }
328
+ }
329
+ function collectTextRects() {
330
+ var walker = doc.createTreeWalker(doc.body,4), node, entries=[];
331
+ while ((node=walker.nextNode())) {
332
+ var parent=node.parentElement;
333
+ if (!parent || parent.closest('script,style,noscript,textarea,[data-papyrus-overlay],[data-papyrus-ink-capture]')) continue;
334
+ var text=node.nodeValue || '', style=win.getComputedStyle(parent), computed=parseFloat(style.lineHeight);
335
+ for (var start=0; start<text.length;) {
336
+ var end=Math.min(text.length,start+24), last=text.charCodeAt(end-1), next=text.charCodeAt(end);
337
+ if(end<text.length&&last>=0xD800&&last<=0xDBFF&&next>=0xDC00&&next<=0xDFFF)end++;
338
+ var range=doc.createRange(); range.setStart(node,start); range.setEnd(node,end);
339
+ var rects=[]; try { rects=Array.from(range.getClientRects()); } catch (_) {}
340
+ rects.forEach(function(rect) {
341
+ if (!(rect.width>0 && rect.height>0)) return;
342
+ entries.push({left:rect.left+win.scrollX,top:rect.top+win.scrollY,right:rect.right+win.scrollX,bottom:rect.bottom+win.scrollY,width:rect.width,height:rect.height,
343
+ node:node,start:start,end:end,lineHeight:Number.isFinite(computed)&&computed>0?computed:rect.height});
344
+ });
345
+ start=end;
346
+ }
347
+ }
348
+ return entries;
349
+ }
350
+ function rebuildSpatialIndex() { spatialIndex=createSpatialIndex(collectTextRects(),64); rangeCache.clear(); }
351
+ function inkViewportExtent() {
352
+ var root=doc.documentElement,body=doc.body,bodyRect=body.getBoundingClientRect();
353
+ var width=root.clientWidth||body.clientWidth||bodyRect.width||win.innerWidth||1;
354
+ var height=root.clientHeight||body.clientHeight||bodyRect.height||win.innerHeight||1;
355
+ return Math.max(1,Math.min(4096,width),Math.min(4096,height));
356
+ }
357
+ function inkStrokeWidthPx(width,preset) {
358
+ var scale=preset==='highlight'?0.8:preset==='underline'?0.1:0.15;
359
+ return Math.max(1,Math.min(24,(Number.isFinite(width)?width:0.006)*inkViewportExtent()*scale));
360
+ }
361
+ function closestGlyph(entry,x,y,maxDistance) {
362
+ var source=(entry.node.nodeValue || '').slice(entry.start,entry.end), best=null, offset=0;
363
+ for (var glyph of source) {
364
+ var range=doc.createRange(); range.setStart(entry.node,entry.start+offset); range.setEnd(entry.node,entry.start+offset+glyph.length); offset+=glyph.length;
365
+ var rects=[]; try { rects=Array.from(range.getClientRects()); } catch (_) {}
366
+ rects.forEach(function(rect) {
367
+ if (!(rect.width>0 && rect.height>0)) return;
368
+ var nx=Math.max(rect.left+win.scrollX,Math.min(rect.right+win.scrollX,x)), ny=Math.max(rect.top+win.scrollY,Math.min(rect.bottom+win.scrollY,y));
369
+ var dx=x-nx,dy=y-ny,distance=Math.sqrt(dx*dx+dy*dy);
370
+ if (distance<=maxDistance && (!best || distance<best.distance)) best={range:range,rect:rect,distance:distance};
371
+ });
372
+ }
373
+ if (!best || typeof contents.cfiFromRange!=='function') return null;
374
+ var quote=best.range.toString(), cfiRange; try { cfiRange=contents.cfiFromRange(best.range); } catch (_) { return null; }
375
+ if (!quote || typeof cfiRange!=='string' || !/^epubcfi\([^\n<>]+\)$/.test(cfiRange)) return null;
376
+ var lineHeight=Math.max(1,entry.lineHeight || best.rect.height), px=x-(best.rect.left+win.scrollX), py=y-(best.rect.top+win.scrollY);
377
+ return {cfiRange:cfiRange,quote:quote,x:Math.max(-3,Math.min(3,px/lineHeight)),y:Math.max(-3,Math.min(3,py/lineHeight))};
378
+ }
379
+ function renderInkAnnotation(annotation,keep,bodyRect) {
380
+ var baseRect=bodyRect || doc.body.getBoundingClientRect() || {left:0,top:0};
381
+ var data=annotation.epubInk;
382
+ if (!data || data.version!==1 || data.kind!=='epub-ink' || !Array.isArray(data.segments)) return;
383
+ keep.add(annotation.id);
384
+ var group=inkRegistry.get(annotation.id);
385
+ if (!group) { group=doc.createElementNS('http://www.w3.org/2000/svg','g'); group.setAttribute('data-epub-ink-id',annotation.id); inkSvg.appendChild(group); inkRegistry.set(annotation.id,group); }
386
+ group.replaceChildren();
387
+ var color=/^#[0-9a-f]{6}$/i.test(annotation.color)?annotation.color:'#2563eb';
388
+ var opacity=Number.isFinite(annotation.opacity)?Math.max(0,Math.min(1,annotation.opacity)):1;
389
+ var preset=['ink','highlight','underline'].indexOf(data.preset)>=0?data.preset:'ink';
390
+ var stroke=inkStrokeWidthPx(annotation.strokeWidth,preset);
391
+ data.segments.forEach(function(segment) {
392
+ if (segment.href!==options.href || !Array.isArray(segment.points)) return;
393
+ var paths=[], current=[];
394
+ segment.points.forEach(function(point) {
395
+ var range=rangeCache.get(point.cfiRange);
396
+ if (!range) { try { range=contents.range(point.cfiRange); } catch (_) { range=null; } if (range) rangeCache.set(point.cfiRange,range); }
397
+ if (!range || range.toString()!==point.quote) { if(current.length>1)paths.push(current);current=[];return; }
398
+ var rect; try { rect=range.getBoundingClientRect(); } catch (_) { rect=null; }
399
+ if (!rect || !(rect.width>0 && rect.height>0)) { if(current.length>1)paths.push(current);current=[];return; }
400
+ var lineHeight=Math.max(1,rect.height), x=rect.left-baseRect.left+point.x*lineHeight, y=rect.top-baseRect.top+point.y*lineHeight;
401
+ var previous=current[current.length-1], jumpLimit=Math.max(24,lineHeight*2.5);
402
+ if (previous && Math.hypot(x-previous.x,y-previous.y)>jumpLimit) { if(current.length>1)paths.push(current);current=[]; }
403
+ current.push({x:x,y:y,lineHeight:lineHeight});
404
+ });
405
+ if(current.length>1)paths.push(current);
406
+ paths.forEach(function(points) {
407
+ var d='M '+points[0].x.toFixed(1)+' '+points[0].y.toFixed(1);
408
+ for(var i=1;i<points.length;i++){var prev=points[i-1],point=points[i],mx=(prev.x+point.x)/2,my=(prev.y+point.y)/2;d+=' Q '+prev.x.toFixed(1)+' '+prev.y.toFixed(1)+' '+mx.toFixed(1)+' '+my.toFixed(1);}
409
+ var last=points[points.length-1];d+=' L '+last.x.toFixed(1)+' '+last.y.toFixed(1);
410
+ var path=doc.createElementNS('http://www.w3.org/2000/svg','path');path.setAttribute('d',d);path.setAttribute('fill','none');path.setAttribute('stroke',color);path.setAttribute('stroke-width',String(stroke));path.setAttribute('stroke-linecap','round');path.setAttribute('stroke-linejoin','round');path.setAttribute('opacity',String(opacity));group.appendChild(path);
411
+ });
412
+ });
413
+ }
414
+ function clearPreview() {
415
+ if(inkContext)inkContext.clearRect(0,0,inkCanvas.width,inkCanvas.height);
416
+ inkSvg.querySelectorAll('[data-papyrus-ink-preview]').forEach(function(path){path.remove();});
417
+ }
418
+ function createPreviewPath(x,y) {
419
+ var path=doc.createElementNS('http://www.w3.org/2000/svg','path');
420
+ path.setAttribute('data-papyrus-ink-preview','');path.setAttribute('d','M '+x.toFixed(1)+' '+y.toFixed(1));path.setAttribute('fill','none');
421
+ path.setAttribute('stroke',inkConfig.color);path.setAttribute('stroke-width',String(inkStrokeWidthPx(inkConfig.strokeWidth,inkConfig.preset)));
422
+ path.setAttribute('stroke-linecap','round');path.setAttribute('stroke-linejoin','round');path.setAttribute('opacity',String(inkConfig.opacity));
423
+ path.style.pointerEvents='none';inkSvg.appendChild(path);return path;
424
+ }
425
+ function updatePreviewPath(clientX,clientY) {
426
+ var bodyRect=doc.body.getBoundingClientRect(),x=clientX-bodyRect.left,y=clientY-bodyRect.top;
427
+ var gesture=activeGesture;
428
+ if(!gesture.previewPath||gesture.previewPathPoints>=32){
429
+ gesture.previewPath=createPreviewPath(gesture.previewLast?gesture.previewLast.x:x,gesture.previewLast?gesture.previewLast.y:y);
430
+ gesture.previewPathPoints=1;
431
+ }
432
+ var pathData=gesture.previewPath.getAttribute('d')||'';
433
+ gesture.previewPath.setAttribute('d',pathData+' L '+x.toFixed(1)+' '+y.toFixed(1));
434
+ gesture.previewPathPoints++;
435
+ gesture.previewLast={x:x,y:y};
436
+ }
437
+ function cancelGesture() {
438
+ var gesture=activeGesture; activeGesture=null; clearPreview();
439
+ if(gesture)try{inkCanvas.releasePointerCapture(gesture.pointerId);}catch(_){}
440
+ }
441
+ function commitGesture() {
442
+ var gesture=activeGesture; activeGesture=null; clearPreview();
443
+ if(!gesture || !alive || !options.isCurrent() || !inkConfig.enabled || !spatialIndex) return;
444
+ var simplified=simplifyPath(gesture.points,1.5,512), anchorSamples=densifyPath(simplified,12,512), segments=[], current=[];
445
+ anchorSamples.forEach(function(point) {
446
+ var coarse=queryNearestTextRect(spatialIndex,point.x,point.y,28);
447
+ var cutoff=coarse?Math.max(12,Math.min(28,0.8*coarse.entry.lineHeight)):12;
448
+ var anchored=coarse && closestGlyph(coarse.entry,point.x,point.y,cutoff);
449
+ if(!anchored){if(current.length>1)segments.push({href:options.href,spineIndex:options.spineIndex,points:current});current=[];return;}
450
+ current.push(anchored);
451
+ });
452
+ if(current.length>1)segments.push({href:options.href,spineIndex:options.spineIndex,points:current});
453
+ segments=segments.slice(0,64);
454
+ var pointCount=segments.reduce(function(total,segment){return total+segment.points.length;},0);
455
+ if(!pointCount)return;
456
+ var width=Math.max(1,doc.documentElement.scrollWidth,win.innerWidth),height=Math.max(1,doc.documentElement.scrollHeight,win.innerHeight);
457
+ var normalized=simplified.map(function(p){return {x:Math.max(0,Math.min(1,p.x/width)),y:Math.max(0,Math.min(1,p.y/height))};});
458
+ var minX=Math.min.apply(null,normalized.map(function(p){return p.x;})),maxX=Math.max.apply(null,normalized.map(function(p){return p.x;}));
459
+ var minY=Math.min.apply(null,normalized.map(function(p){return p.y;})),maxY=Math.max.apply(null,normalized.map(function(p){return p.y;}));
460
+ var first=segments[0].points[0], href=segments[0].href, documentId=typeof options.documentId==='function'?options.documentId():options.documentId;
461
+ var annotation={id:'epub-ink-'+Date.now()+'-'+Math.random().toString(36).slice(2,9),type:'ink',pageIndex:(options.spineIndex||0)+1,
462
+ rect:{x:minX,y:minY,width:Math.min(1-minX,Math.max(0.001,maxX-minX)),height:Math.min(1-minY,Math.max(0.001,maxY-minY))},path:normalized,color:inkConfig.color,opacity:inkConfig.opacity,strokeWidth:inkConfig.strokeWidth,createdAt:Date.now(),content:first.quote,
463
+ anchor:{version:1,kind:'epub-cfi',cfiRange:first.cfiRange,href:href,spineIndex:options.spineIndex,documentId:documentId,quote:first.quote},
464
+ epubInk:{version:1,kind:'epub-ink',documentId:documentId,preset:inkConfig.preset,segments:segments}};
465
+ if(typeof options.onInkCommit==='function')options.onInkCommit(annotation);
466
+ }
467
+ function pointerDown(event) {
468
+ if(!inkConfig.enabled || !alive || !options.isCurrent() || event.button!==undefined && event.button!==0)return;
469
+ if(activeGesture){cancelGesture();return;}
470
+ event.preventDefault();event.stopPropagation();
471
+ activeGesture={pointerId:event.pointerId,points:[{x:event.clientX+win.scrollX,y:event.clientY+win.scrollY}],last:{x:event.clientX,y:event.clientY},previewPath:null,previewPathPoints:0,previewLast:null};
472
+ updatePreviewPath(event.clientX,event.clientY);
473
+ try{inkCanvas.setPointerCapture(event.pointerId);}catch(_){}
474
+ }
475
+ function pointerMove(event) {
476
+ if(!activeGesture || activeGesture.pointerId!==event.pointerId)return;
477
+ if(!options.isCurrent()||!inkConfig.enabled){cancelGesture();return;}
478
+ event.preventDefault();event.stopPropagation();
479
+ var point={x:event.clientX+win.scrollX,y:event.clientY+win.scrollY};activeGesture.points.push(point);
480
+ if(activeGesture.points.length>2048)activeGesture.points=compactGesturePoints(activeGesture.points,1024);
481
+ if(inkContext){inkContext.beginPath();inkContext.moveTo(activeGesture.last.x,activeGesture.last.y);inkContext.lineTo(event.clientX,event.clientY);inkContext.strokeStyle=inkConfig.color;inkContext.globalAlpha=inkConfig.opacity;inkContext.lineWidth=inkStrokeWidthPx(inkConfig.strokeWidth,inkConfig.preset);inkContext.lineCap='round';inkContext.lineJoin='round';inkContext.stroke();}
482
+ updatePreviewPath(event.clientX,event.clientY);
483
+ activeGesture.last={x:event.clientX,y:event.clientY};
484
+ }
485
+ function pointerUp(event) {if(!activeGesture || activeGesture.pointerId!==event.pointerId)return;event.preventDefault();event.stopPropagation();var end={x:event.clientX+win.scrollX,y:event.clientY+win.scrollY},last=activeGesture.points[activeGesture.points.length-1];if(Math.hypot(end.x-last.x,end.y-last.y)>0.5){activeGesture.points.push(end);updatePreviewPath(event.clientX,event.clientY);}commitGesture();}
486
+ function pointerCancel(event) {if(activeGesture && (event.pointerId===undefined || activeGesture.pointerId===event.pointerId))cancelGesture();}
487
+ resizeCanvas();rebuildSpatialIndex();
488
+ inkCanvas.addEventListener('pointerdown',pointerDown);inkCanvas.addEventListener('pointermove',pointerMove);inkCanvas.addEventListener('pointerup',pointerUp);inkCanvas.addEventListener('pointercancel',pointerCancel);inkCanvas.addEventListener('lostpointercapture',pointerCancel);
489
+ win.addEventListener('blur',pointerCancel);win.addEventListener('resize',resizeCanvas);
172
490
  function render() {
173
491
  frame = 0; if (!alive || !options.isCurrent()) return;
174
- var keep = new Set(), noteGroups = new Map();
492
+ var keep = new Set(), keepInk = new Set(), noteGroups = new Map();
175
493
  hitRegions = [];
494
+ var bodyRect=doc.body.getBoundingClientRect() || {left:0,top:0};
495
+ inkSvg.setAttribute('width',String(Math.max(doc.documentElement.scrollWidth,win.innerWidth)));
496
+ inkSvg.setAttribute('height',String(Math.max(doc.documentElement.scrollHeight,win.innerHeight)));
176
497
  options.annotations().forEach(function(annotation) {
177
498
  var anchor = annotation.anchor;
178
499
  if (!validAnchor(anchor) || anchor.href !== options.href || anchor.documentId && options.documentId && anchor.documentId !== options.documentId) return;
500
+ if(annotation.type==='ink' && annotation.epubInk){renderInkAnnotation(annotation,keepInk,bodyRect);return;}
179
501
  var range = resolveRange(contents,anchor); if (!range) return;
180
502
  var rects = Array.from(range.getClientRects()).filter(function(r){return r.width>0 && r.height>0;});
181
503
  if (!rects.length) return;
@@ -187,6 +509,7 @@
187
509
  var color = /^#[0-9a-f]{6}$/i.test(annotation.color) ? annotation.color : '#FFC928';
188
510
  var style = annotation.markupStyle || annotation.type;
189
511
  var opacity = Number.isFinite(annotation.opacity) ? Math.max(0,Math.min(1,annotation.opacity)) : 0.35;
512
+ if(style === 'highlight')opacity=Math.min(0.35,opacity);
190
513
  rects.forEach(function(rect) {
191
514
  var mark = doc.createElement('div');
192
515
  var bodyRect=doc.body.getBoundingClientRect();
@@ -199,8 +522,10 @@
199
522
  if (hasNote) {
200
523
  var last = rects[rects.length-1], button = doc.createElement('button');
201
524
  button.textContent = '●'; button.setAttribute('aria-label',options.labels().comment || '');
202
- button.style.cssText = 'position:absolute;pointer-events:auto;border:0;background:transparent;color:'+color+';width:44px;height:44px;left:'+Math.max(0,Math.min(doc.documentElement.clientWidth-44,last.right-doc.body.getBoundingClientRect().left))+'px;top:'+(Math.floor(last.top/44)*44-doc.body.getBoundingClientRect().top)+'px';
203
- var key = Math.floor(last.top/44);
525
+ var pageWidth=Math.max(44, options.pageWidth ? options.pageWidth() : win.innerWidth);
526
+ var bodyRect=doc.body.getBoundingClientRect(), column=Math.max(0,Math.floor((last.left-bodyRect.left)/pageWidth));
527
+ button.style.cssText = 'position:absolute;pointer-events:auto;border:0;background:transparent;color:'+color+';width:44px;height:44px;left:'+((column+1)*pageWidth-44)+'px;top:'+(Math.floor(last.top/44)*44-doc.body.getBoundingClientRect().top)+'px';
528
+ var key = column+":"+Math.floor(last.top/44);
204
529
  var neighbors=noteGroups.get(key);
205
530
  if(!neighbors){neighbors={button:button,ids:[]};noteGroups.set(key,neighbors);group.appendChild(button);}
206
531
  neighbors.ids.push(annotation.id);
@@ -211,22 +536,26 @@
211
536
  group.button.addEventListener('click',function(e){e.stopPropagation();if(options.isCurrent())options.onTap(group.ids[0],group.ids);});
212
537
  });
213
538
  registry.forEach(function(group,id){if (!keep.has(id)) {group.remove();registry.delete(id);}});
539
+ inkRegistry.forEach(function(group,id){if (!keepInk.has(id)) {group.remove();inkRegistry.delete(id);}});
214
540
  }
215
541
  function schedule() { if (alive && !frame) frame = win.requestAnimationFrame(render); }
542
+ function rebuildAndSchedule(){if(!alive)return;rebuildSpatialIndex();schedule();}
543
+ function handleResize(){resizeCanvas();rebuildAndSchedule();}
544
+ function handleDocumentLoad(){rebuildAndSchedule();}
216
545
  function tapMarkup(event) {
217
546
  if(!alive || !options.isCurrent() || String(win.getSelection() || '').length || event.target.closest('[data-papyrus-overlay]'))return;
218
547
  var hit=hitRegions.find(function(item){return item.rects.some(function(r){return event.clientX>=r.left && event.clientX<=r.right && event.clientY>=r.top && event.clientY<=r.bottom;});});
219
548
  if(hit) {event.stopPropagation();options.onTap(hit.id);}
220
549
  }
221
550
  doc.addEventListener('click',tapMarkup);
222
- var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
551
+ var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(rebuildAndSchedule) : null;
223
552
  if (observer) observer.observe(doc.body);
224
- win.addEventListener('resize',schedule); doc.addEventListener('load',schedule,true);
225
- if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(schedule);
553
+ win.addEventListener('resize',handleResize);win.addEventListener('scroll',schedule); doc.addEventListener('load',handleDocumentLoad,true);
554
+ if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(function(){if(alive){rebuildSpatialIndex();schedule();}});
226
555
  schedule();
227
- return {schedule:schedule, range:function(anchor){return resolveRange(contents,anchor);}, destroy:function(){alive=false;if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',schedule);doc.removeEventListener('load',schedule,true);doc.removeEventListener('click',tapMarkup);overlay.remove();doc.body.style.position=originalPosition;registry.clear();}};
556
+ return {schedule:schedule, setInkMode:function(config){var preset=['ink','highlight','underline'].indexOf(config?.preset)>=0?config.preset:'ink';inkConfig={enabled:!!config?.enabled,preset:preset,color:/^#[0-9a-f]{6}$/i.test(config?.color)?config.color:'#2563eb',opacity:Number.isFinite(config?.opacity)?Math.max(0,Math.min(1,config.opacity)):1,strokeWidth:Number.isFinite(config?.strokeWidth)?Math.max(0.0025,Math.min(0.02,config.strokeWidth)):0.006};inkCanvas.style.pointerEvents=inkConfig.enabled?'auto':'none';if(!inkConfig.enabled)cancelGesture();}, invalidateLayout:rebuildAndSchedule, hit:function(x,y){return hitRegions.some(function(item){return item.rects.some(function(r){return x>=r.left && x<=r.right && y>=r.top && y<=r.bottom;});});}, range:function(anchor){return resolveRange(contents,anchor);}, destroy:function(){alive=false;cancelGesture();if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',handleResize);win.removeEventListener('scroll',schedule);win.removeEventListener('blur',pointerCancel);doc.removeEventListener('load',handleDocumentLoad,true);doc.removeEventListener('click',tapMarkup);inkCanvas.removeEventListener('pointerdown',pointerDown);inkCanvas.removeEventListener('pointermove',pointerMove);inkCanvas.removeEventListener('pointerup',pointerUp);inkCanvas.removeEventListener('pointercancel',pointerCancel);inkCanvas.removeEventListener('lostpointercapture',pointerCancel);inkCanvas.remove();overlay.remove();doc.body.style.position=originalPosition;registry.clear();inkRegistry.clear();rangeCache.clear();}};
228
557
  }
229
- return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
558
+ return {validAnchor:validAnchor,simplifyPath:simplifyPath,compactGesturePoints:compactGesturePoints,densifyPath:densifyPath,createSpatialIndex:createSpatialIndex,queryNearestTextRect:queryNearestTextRect,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
230
559
  });
231
560
 
232
561
  /* @papyrus-epub-annotations:end */
@@ -237,10 +566,12 @@
237
566
  let currentType = null;
238
567
  let book = null;
239
568
  let rendition = null;
569
+ let epubResizeAnchor = null;
240
570
  let documentSessionId = null;
241
571
  let annotationDocumentId;
242
572
  let annotationLabels = {};
243
573
  let epubAnnotations = [];
574
+ let epubInkMode = {enabled:false,color:'#2563eb',opacity:1,strokeWidth:0.006};
244
575
  const epubAnnotationRenderers = new Map();
245
576
  let spineItems = [];
246
577
  let textPages = [];
@@ -649,6 +980,7 @@
649
980
  };
650
981
 
651
982
  const clearViewer = () => {
983
+ if(epubResizeAnchor){epubResizeAnchor.destroy();epubResizeAnchor=null;}
652
984
  epubAnnotationRenderers.forEach(renderer => renderer.destroy());
653
985
  epubAnnotationRenderers.clear();
654
986
  if (epubInteractionCleanup) {
@@ -1305,29 +1637,63 @@
1305
1637
  pageCount,
1306
1638
  });
1307
1639
 
1640
+ if (book.locations?.generate) {
1641
+ const activeBook=book;
1642
+ activeBook.locations.generate(150).then(() => {
1643
+ if (isCurrentLoad() && activeBook===book && epubLocation) {
1644
+ epubLocation.progress=activeBook.locations.percentageFromCfi(epubLocation.cfi);
1645
+ sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
1646
+ }
1647
+ }).catch(() => {});
1648
+ }
1649
+ return renderEpubRendition(loadId,generation,null);
1650
+ };
1651
+
1652
+ const renderEpubRendition = async (loadId,generation,target) => {
1653
+ const diagnosticPayload={loadId};
1654
+ const isCurrentLoad=()=>generation===epubLoadGeneration;
1655
+ if(epubInteractionCleanup)epubInteractionCleanup();
1656
+ if(rendition)rendition.destroy();
1308
1657
  clearViewer();
1309
- rendition = book.renderTo(viewer, {
1310
- manager: 'continuous',
1311
- allowScriptedContent: false,
1312
- width: '100%',
1313
- height: '100%',
1314
- flow: 'scrolled-continuous',
1315
- spread: 'none',
1316
- });
1658
+ rendition = book.renderTo(viewer, window.PapyrusEpubReader.options(epubViewMode === 'single' ? 'paged' : 'chapter-scroll'));
1317
1659
  sendEpubDiagnostic('epub.rendition.created', diagnosticPayload);
1318
1660
 
1319
1661
  if (rendition && rendition.hooks && rendition.hooks.content) {
1320
1662
  const session = documentSessionId;
1321
1663
  const activeRendition = rendition;
1322
1664
  rendition.hooks.content.register((contents) => {
1665
+ const removeGestureListeners = window.PapyrusEpubReader.install(contents, {
1666
+ mode: () => epubViewMode === 'single' ? 'paged' : 'chapter-scroll',
1667
+ rtl: () => book.package?.metadata?.direction === 'rtl' || book.spine?.direction === 'rtl',
1668
+ blocked: () => Boolean(nativeMenuSelection && String(nativeMenuSelection.contents?.window?.getSelection() || '').trim()),
1669
+ annotationHit: point => epubAnnotationRenderers.get(contents)?.hit(point.x,point.y),
1670
+ current: () => session === documentSessionId && activeRendition === rendition,
1671
+ interact: () => epubResizeAnchor?.invalidate(),
1672
+ tap: () => sendEvent('EPUB_CONTENT_TAP',{documentSessionId:session}),
1673
+ turn: direction => {
1674
+ epubNavigationQueue = epubNavigationQueue.catch(()=>{}).then(() => {
1675
+ if(session !== documentSessionId || activeRendition !== rendition)return;
1676
+ epubResizeAnchor?.invalidate();
1677
+ return direction<0?activeRendition.prev():activeRendition.next();
1678
+ });
1679
+ epubNavigationQueue.catch(error => sendEvent('EPUB_NAVIGATION_ERROR',{message:String(error)}));
1680
+ },
1681
+ });
1682
+ const gestureCleanup = () => {removeGestureListeners();epubSelectionCleanups.delete(gestureCleanup);};
1683
+ epubSelectionCleanups.add(gestureCleanup);
1684
+ contents.__papyrusGestureCleanup=gestureCleanup;
1323
1685
  const href = spineItems[contents.sectionIndex]?.href;
1324
1686
  if (href && window.PapyrusEpubAnnotations) {
1325
1687
  const renderer = window.PapyrusEpubAnnotations.install(contents, {
1326
1688
  href, get documentId() { return annotationDocumentId; },
1689
+ spineIndex:contents.sectionIndex,
1327
1690
  isCurrent: () => session === documentSessionId && activeRendition === rendition,
1691
+ pageWidth: () => activeRendition.layout?.width || viewer.clientWidth,
1328
1692
  annotations: () => epubAnnotations, labels: () => annotationLabels,
1693
+ onInkCommit: annotation => sendEvent('EPUB_INK_CREATED',{documentSessionId:session,annotation}),
1329
1694
  onTap: (id, ids) => sendEvent("EPUB_ANNOTATION_TAP", {documentSessionId: session, id, ids}),
1330
1695
  });
1696
+ renderer.setInkMode(epubInkMode);
1331
1697
  epubAnnotationRenderers.set(contents,renderer);
1332
1698
  }
1333
1699
  const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
@@ -1343,8 +1709,7 @@
1343
1709
  typeof selectionWindow.getSelection === 'function'
1344
1710
  ) {
1345
1711
  let selectionTimer = null;
1346
- let lastSelectionKey = '';
1347
- let lastSelectionAt = 0;
1712
+
1348
1713
  const reportSelection = () => {
1349
1714
  selectionTimer = null;
1350
1715
  if (session !== documentSessionId || activeRendition !== rendition) return;
@@ -1359,11 +1724,7 @@
1359
1724
  try { cfi = contents.cfiFromRange(range); } catch (_) { return; }
1360
1725
  const anchor = window.PapyrusEpubAnnotations?.selection(contents, cfi, href, annotationDocumentId);
1361
1726
  if (!anchor) return;
1362
- const key = anchor.cfiRange + ':' + anchor.quote;
1363
- const now = Date.now();
1364
- if (key === lastSelectionKey && now - lastSelectionAt < 750) return;
1365
- lastSelectionKey = key;
1366
- lastSelectionAt = now;
1727
+ if (nativeMenuSelection?.payload?.anchor?.cfiRange === anchor.cfiRange && nativeMenuSelection?.payload?.anchor?.quote === anchor.quote) return;
1367
1728
  const payload = {documentSessionId: session, text: anchor.quote,
1368
1729
  pageIndex: contents.sectionIndex + 1, anchor};
1369
1730
  nativeMenuSelection = {contents, payload};
@@ -1378,6 +1739,7 @@
1378
1739
  // The content document still emits selectionchange, so resolve its
1379
1740
  // live Range to a CFI before the native menu action can fire.
1380
1741
  if (text) reportSelection();
1742
+ else if (nativeMenuSelection?.contents === contents) nativeMenuSelection = null;
1381
1743
  };
1382
1744
  selectionDocument.addEventListener('selectionchange', handleSelectionChange);
1383
1745
  const selectionCleanup = () => {
@@ -1400,6 +1762,7 @@
1400
1762
  typeof rendition.hooks.unloaded.register === 'function'
1401
1763
  ) {
1402
1764
  rendition.hooks.unloaded.register((view) => {
1765
+ view?.contents?.__papyrusGestureCleanup?.();
1403
1766
  const cleanup = view && view.contents && view.contents.__papyrusSelectionCleanup;
1404
1767
  if (typeof cleanup === 'function') cleanup();
1405
1768
  const renderer = epubAnnotationRenderers.get(view?.contents);
@@ -1415,21 +1778,37 @@
1415
1778
  if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
1416
1779
  const href = spineItems[contents.sectionIndex]?.href;
1417
1780
  const anchor = href && window.PapyrusEpubAnnotations?.selection(contents,cfiRange,href,annotationDocumentId);
1418
- if (!anchor) return;
1781
+ if (!anchor || nativeMenuSelection?.payload?.anchor?.cfiRange === anchor.cfiRange && nativeMenuSelection?.payload?.anchor?.quote === anchor.quote) return;
1419
1782
  epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(true);
1420
1783
  const payload = {documentSessionId: selectionSession, text: anchor.quote, pageIndex: contents.sectionIndex+1, anchor};
1421
1784
  nativeMenuSelection = {contents, payload};
1422
1785
  sendEvent('EPUB_TEXT_SELECTED', payload);
1423
1786
  });
1424
- ['rendered','resized','layout'].forEach(name => rendition.on(name, () => {
1425
- if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => renderer.schedule());
1787
+ ['rendered','resized','layout','relocated'].forEach(name => rendition.on(name, () => {
1788
+ if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => name==='relocated'?renderer.schedule():renderer.invalidateLayout());
1426
1789
  }));
1427
1790
  rendition.on('relocated', (location) => {
1791
+ if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
1428
1792
  const start = location && location.start;
1429
1793
  let index = start && Number.isInteger(start.index) ? start.index : -1;
1430
1794
  if (index < 0 && start && start.href) index = getSpineIndexByHref(start.href);
1431
1795
  if (index < 0 || index >= spineItems.length) return;
1432
1796
  epubScrollDiagnostics && epubScrollDiagnostics.relocated(location);
1797
+ let pageGeometry;
1798
+ const manager=rendition.manager,layout=manager?.layout;
1799
+ if(epubViewMode==='single' && layout?.divisor===1 && !manager?.settings?.fullsize && manager?.settings?.direction==='ltr'){
1800
+ const visible=manager.visible?.();
1801
+ const view=visible?.find(view=>view.index===index);
1802
+ if(view && manager.container){
1803
+ pageGeometry={direction:'ltr',firstIndex:book.spine.first()?.index,lastIndex:book.spine.last()?.index,pageWidth:layout.pageWidth,offset:manager.container.getBoundingClientRect().left-view.position().left};
1804
+ }
1805
+ }
1806
+ epubLocation=window.PapyrusEpubReader.location(location,spineItems.length,epubViewMode==='single'?'paged':'chapter-scroll',pageGeometry);
1807
+ if(epubLocation){
1808
+ epubLocation.cfi=epubResizeAnchor?.cfi() || epubLocation.cfi;
1809
+ if(book.locations?.length?.()>0)epubLocation.progress=book.locations.percentageFromCfi(epubLocation.cfi);
1810
+ sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
1811
+ }
1433
1812
  const nextPage = index + 1;
1434
1813
  if (nextPage === currentPage) return;
1435
1814
  currentPage = nextPage;
@@ -1437,21 +1816,23 @@
1437
1816
  });
1438
1817
  }
1439
1818
 
1819
+ applyEpubZoom();
1440
1820
  sendEpubDiagnostic('epub.display.start', diagnosticPayload);
1441
1821
  await withEpubStageTimeout(
1442
- displayEpubPage(0),
1822
+ rendition.display(target || spineItems[0]?.href),
1443
1823
  'rendition.display',
1444
1824
  diagnosticPayload,
1445
1825
  );
1446
1826
  if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
1447
1827
  sendEpubDiagnostic('epub.display.end', diagnosticPayload);
1828
+ const resizeRendition=rendition;
1829
+ epubResizeAnchor=window.PapyrusEpubReader.installResizeAnchor(resizeRendition.manager,()=>resizeRendition.location?.start?.cfi);
1448
1830
  if (
1449
1831
  rendition &&
1450
1832
  rendition.manager &&
1451
1833
  typeof rendition.manager.on === 'function' &&
1452
1834
  typeof rendition.on === 'function'
1453
1835
  ) {
1454
- let touchStart = null;
1455
1836
  let lastScrollOffset = -1;
1456
1837
  const manager = rendition.manager;
1457
1838
  epubScrollDiagnostics = createEpubScrollDiagnostics(manager);
@@ -1526,48 +1907,19 @@
1526
1907
  return result;
1527
1908
  };
1528
1909
  }
1529
- const getTouchPoint = (event) => {
1530
- const touch =
1531
- (event && event.changedTouches && event.changedTouches[0]) ||
1532
- (event && event.touches && event.touches[0]);
1533
- if (!touch) return null;
1534
- const x = Number(touch.screenX ?? touch.clientX);
1535
- const y = Number(touch.screenY ?? touch.clientY);
1536
- return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
1537
- };
1538
- const handleScroll = (event) => {
1910
+ const handleScroll = event => {
1539
1911
  const offsetY = Math.max(0, Number(event && event.top) || 0);
1540
1912
  epubScrollDiagnostics && epubScrollDiagnostics.scroll(event);
1541
- if (Math.abs(offsetY - lastScrollOffset) < 1) return;
1542
- lastScrollOffset = offsetY;
1543
- sendEvent('VIEWER_SCROLL', { offsetY });
1544
- };
1545
- const handleTouchStart = (event) => {
1546
- touchStart = getTouchPoint(event);
1547
- epubScrollDiagnostics && epubScrollDiagnostics.touchStart(event);
1548
- };
1549
- const handleTouchMove = (event) => {
1550
- epubScrollDiagnostics && epubScrollDiagnostics.touchMove(event);
1551
- };
1552
- const handleTouchEnd = (event) => {
1553
- const end = getTouchPoint(event);
1554
- const start = touchStart;
1555
- touchStart = null;
1556
- const selectedText = event?.target?.ownerDocument?.defaultView?.getSelection?.()?.toString().trim() || '';
1557
- if (!selectedText && start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
1558
- sendEvent('VIEWER_TAP', {});
1559
- }
1560
- epubScrollDiagnostics && epubScrollDiagnostics.touchEnd(event);
1913
+ if (Math.abs(offsetY-lastScrollOffset)<1) return;
1914
+ lastScrollOffset=offsetY;
1915
+ sendEvent('VIEWER_SCROLL',{offsetY});
1561
1916
  };
1562
-
1563
1917
  manager.on('scroll', handleScroll);
1564
1918
  const handleScrolled = (event) => {
1565
1919
  epubScrollDiagnostics && epubScrollDiagnostics.settled(event);
1566
1920
  };
1567
1921
  manager.on('scrolled', handleScrolled);
1568
- rendition.on('touchstart', handleTouchStart);
1569
- rendition.on('touchmove', handleTouchMove);
1570
- rendition.on('touchend', handleTouchEnd);
1922
+
1571
1923
  epubInteractionCleanup = () => {
1572
1924
  epubSelectionCleanups.forEach((cleanup) => cleanup());
1573
1925
  epubSelectionCleanups.clear();
@@ -1576,16 +1928,9 @@
1576
1928
  if (originalEnqueue) queue.enqueue = originalEnqueue;
1577
1929
  if (typeof manager.off === 'function') manager.off('scroll', handleScroll);
1578
1930
  if (typeof manager.off === 'function') manager.off('scrolled', handleScrolled);
1579
- if (typeof rendition.off === 'function') {
1580
- rendition.off('touchstart', handleTouchStart);
1581
- rendition.off('touchmove', handleTouchMove);
1582
- rendition.off('touchend', handleTouchEnd);
1583
- }
1584
1931
  epubScrollDiagnostics = null;
1585
1932
  };
1586
1933
  }
1587
- applyEpubZoom();
1588
-
1589
1934
  const outline = await withEpubStageTimeout(
1590
1935
  buildOutline(),
1591
1936
  'outline',
@@ -1599,6 +1944,8 @@
1599
1944
  return { pageCount, outline };
1600
1945
  };
1601
1946
 
1947
+ let epubNavigationQueue = Promise.resolve();
1948
+ let epubNavigationRequest = 0;
1602
1949
  const displayEpubPage = async (pageIndex) => {
1603
1950
  if (!rendition) return;
1604
1951
  const item = spineItems[pageIndex];
@@ -1608,9 +1955,19 @@
1608
1955
  target,
1609
1956
  pageIndex,
1610
1957
  });
1611
- await rendition.display(target);
1612
- currentPage = pageIndex + 1;
1613
- sendState();
1958
+ const activeRendition = rendition;
1959
+ const session = documentSessionId;
1960
+ const request = ++epubNavigationRequest;
1961
+ const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
1962
+ if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
1963
+ epubResizeAnchor?.invalidate();
1964
+ await activeRendition.display(target);
1965
+ if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
1966
+ currentPage = pageIndex + 1;
1967
+ sendState();
1968
+ });
1969
+ epubNavigationQueue = navigation;
1970
+ await navigation;
1614
1971
  };
1615
1972
 
1616
1973
  const getTextContent = async (pageIndex) => {
@@ -1712,6 +2069,8 @@
1712
2069
  };
1713
2070
 
1714
2071
  let nativeMenuSelection = null;
2072
+ let epubViewMode = "single";
2073
+ let epubLocation = null;
1715
2074
 
1716
2075
  const handleCommand = async (message) => {
1717
2076
  const { id, kind, payload } = message;
@@ -1723,7 +2082,13 @@
1723
2082
  documentSessionId = payload.documentSessionId || null;
1724
2083
  epubAnnotations = [];
1725
2084
  annotationDocumentId = undefined;
2085
+ epubInkMode = {enabled:false,color:'#2563eb',opacity:1,strokeWidth:0.006};
1726
2086
  zoom = 1.0;
2087
+ epubViewMode = payload.epubMode === "chapter-scroll" ? "continuous" : "single";
2088
+ epubLocation = null;
2089
+ nativeMenuSelection = null;
2090
+ epubNavigationRequest += 1;
2091
+ epubNavigationQueue = Promise.resolve();
1727
2092
 
1728
2093
  if (currentType === 'text') {
1729
2094
  const result = await loadText(
@@ -1778,17 +2143,24 @@
1778
2143
  epubAnnotationRenderers.forEach(renderer => renderer.schedule());
1779
2144
  } else {
1780
2145
  const annotation = epubAnnotations.find(a => a.id === payload.annotationId);
1781
- if (!annotation) throw new Error('Annotation not available');
2146
+ if (!annotation || (annotation.anchor.documentId && annotation.anchor.documentId !== annotationDocumentId)) throw new Error('Annotation not available for this document');
1782
2147
  // Navigation uses the CFI, never a synthetic percentage/page rectangle.
1783
2148
  const navigationSession = documentSessionId;
1784
2149
  const activeRendition = rendition;
1785
2150
  // Open the confirmed chapter first, resolve its quote, then navigate its actual range.
1786
- await activeRendition.display(annotation.anchor.href);
1787
- if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
1788
- const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
1789
- const range = contents && window.PapyrusEpubAnnotations.resolveRange(contents,annotation.anchor);
1790
- if(!range)throw new Error('EPUB anchor unresolved');
1791
- await activeRendition.display(contents.cfiFromRange(range));
2151
+ epubNavigationRequest += 1;
2152
+ const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
2153
+ if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
2154
+ epubResizeAnchor?.invalidate();
2155
+ await activeRendition.display(annotation.anchor.href);
2156
+ if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
2157
+ const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
2158
+ const range = contents && window.PapyrusEpubAnnotations.resolveRange(contents,annotation.anchor);
2159
+ if(!range)throw new Error('EPUB anchor unresolved');
2160
+ await activeRendition.display(contents.cfiFromRange(range));
2161
+ });
2162
+ epubNavigationQueue = navigation;
2163
+ await navigation;
1792
2164
  }
1793
2165
  sendResponse(id,true,null); return;
1794
2166
  }
@@ -1815,12 +2187,61 @@
1815
2187
  return;
1816
2188
  }
1817
2189
 
2190
+ if (kind === 'epub-turn-page') {
2191
+ if (epubViewMode !== 'single') {sendResponse(id,true,null); return;}
2192
+ if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB navigation');
2193
+ const active = rendition;
2194
+ epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
2195
+ if (active !== rendition || payload.documentSessionId !== documentSessionId) return;
2196
+ epubResizeAnchor?.invalidate();
2197
+ return payload.direction < 0 ? active.prev() : active.next();
2198
+ });
2199
+ await epubNavigationQueue;
2200
+ sendResponse(id,true,null); return;
2201
+ }
2202
+
2203
+ if (kind === 'set-epub-view-mode') {
2204
+ if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB layout session');
2205
+ const next=payload.epubMode ? (payload.epubMode==='paged'?'single':'continuous') : (payload.viewMode==='single'?'single':'continuous');
2206
+ {
2207
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
2208
+ if(payload.documentSessionId!==documentSessionId || next===epubViewMode)return;
2209
+ const target=epubResizeAnchor?.cfi() || epubLocation?.cfi;
2210
+ epubViewMode=next;
2211
+ await renderEpubRendition('layout',epubLoadGeneration,target);
2212
+ });
2213
+ await epubNavigationQueue;
2214
+ }
2215
+ sendResponse(id,true,null);return;
2216
+ }
2217
+ if (kind === 'set-epub-ink-mode') {
2218
+ if(currentType!=='epub'||!documentSessionId||payload.documentSessionId!==documentSessionId)throw new Error('Stale EPUB ink session');
2219
+ epubInkMode={enabled:payload.enabled===true,preset:['ink','highlight','underline'].indexOf(payload.preset)>=0?payload.preset:'ink',color:/^#[0-9a-f]{6}$/i.test(payload.color||'')?payload.color:'#2563eb',opacity:Number.isFinite(payload.opacity)?Math.max(0,Math.min(1,payload.opacity)):1,strokeWidth:Number.isFinite(payload.strokeWidth)?Math.max(0.0025,Math.min(0.02,payload.strokeWidth)):0.006};
2220
+ epubAnnotationRenderers.forEach(renderer=>renderer.setInkMode(epubInkMode));
2221
+ sendResponse(id,true,null);return;
2222
+ }
2223
+ if(kind === 'epub-go-to-location'){
2224
+ if(currentType!=='epub' || payload.documentSessionId!==documentSessionId || !window.PapyrusEpubReader.validCfi(payload.cfi))throw new Error('Invalid EPUB location');
2225
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(()=>{if(payload.documentSessionId!==documentSessionId)return;epubResizeAnchor?.invalidate();return rendition.display(payload.cfi);});
2226
+ await epubNavigationQueue;sendResponse(id,true,null);return;
2227
+ }
2228
+
1818
2229
  if (kind === 'set-zoom') {
1819
- zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
1820
- applyZoom();
1821
- sendState();
1822
- sendResponse(id, true, { zoom });
1823
- return;
2230
+ const requestedZoom=Math.max(0.5,Math.min(4.0,payload.zoom || 1.0));
2231
+ if(currentType === 'epub') {
2232
+ const session=documentSessionId;
2233
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
2234
+ if(session!==documentSessionId)return;
2235
+ const active=rendition;
2236
+ const cfi=epubResizeAnchor?.cfi() || epubLocation?.cfi;
2237
+ zoom=requestedZoom;applyEpubZoom();
2238
+ // Let the content CSS update before redisplaying the logical anchor.
2239
+ await new Promise(resolve=>(window.requestAnimationFrame || (callback=>setTimeout(callback,0)))(resolve));
2240
+ if(cfi && active===rendition && session===documentSessionId)await active.display(cfi);
2241
+ });
2242
+ await epubNavigationQueue;
2243
+ } else {zoom=requestedZoom;applyZoom();}
2244
+ sendState();sendResponse(id,true,{zoom});return;
1824
2245
  }
1825
2246
 
1826
2247
  if (kind === 'set-rotation') {