@papyrus-sdk/ui-react-native 0.2.35-beta.11 → 0.2.35-beta.11.epub.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papyrus-sdk/ui-react-native",
3
- "version": "0.2.35-beta.11",
3
+ "version": "0.2.35-beta.11.epub.1",
4
4
  "license": "MIT",
5
5
  "keywords": [
6
6
  "papyrus",
@@ -38,17 +38,13 @@
38
38
  "bugs": {
39
39
  "url": "https://github.com/solrachix/Papyrus/issues"
40
40
  },
41
- "scripts": {
42
- "build": "node runtime/syncComicRuntime.mjs && tsup",
43
- "check:ink-worklets": "pnpm run build && node ../../scripts/ci/check-native-ink-worklets.cjs"
44
- },
45
41
  "dependencies": {
46
42
  "react-native-svg": "^15.5.0"
47
43
  },
48
44
  "peerDependencies": {
49
- "@papyrus-sdk/core": "0.2.23-beta.3",
50
- "@papyrus-sdk/engine-native": "0.2.22-beta.9",
51
- "@papyrus-sdk/types": "0.2.16-beta.3",
45
+ "@papyrus-sdk/core": "0.2.23-beta.3.epub.1",
46
+ "@papyrus-sdk/engine-native": "0.2.22-beta.9.epub.1",
47
+ "@papyrus-sdk/types": "0.2.16-beta.3.epub.1",
52
48
  "@react-native-clipboard/clipboard": "^1.16.3",
53
49
  "react": ">=18.2.0 <20",
54
50
  "react-native": ">=0.81.0",
@@ -63,15 +59,19 @@
63
59
  }
64
60
  },
65
61
  "devDependencies": {
66
- "@papyrus-sdk/core": "workspace:*",
67
- "@papyrus-sdk/engine-native": "workspace:*",
68
- "@papyrus-sdk/types": "workspace:*",
69
62
  "@react-native-clipboard/clipboard": "^1.16.3",
70
63
  "@types/react": "19.2.14",
71
64
  "react": "^19.1.0",
72
65
  "react-native": "^0.81.0",
73
66
  "react-native-gesture-handler": "~2.28.0",
74
67
  "react-native-safe-area-context": "^5.6.2",
75
- "react-native-reanimated": "3.19.5"
68
+ "react-native-reanimated": "3.19.5",
69
+ "@papyrus-sdk/core": "0.2.23-beta.3.epub.1",
70
+ "@papyrus-sdk/types": "0.2.16-beta.3.epub.1",
71
+ "@papyrus-sdk/engine-native": "0.2.22-beta.9.epub.1"
72
+ },
73
+ "scripts": {
74
+ "build": "node runtime/syncComicRuntime.mjs && tsup",
75
+ "check:ink-worklets": "pnpm run build && node ../../scripts/ci/check-native-ink-worklets.cjs"
76
76
  }
77
- }
77
+ }
@@ -75,6 +75,7 @@
75
75
  var color = /^#[0-9a-f]{6}$/i.test(annotation.color) ? annotation.color : '#FFC928';
76
76
  var style = annotation.markupStyle || annotation.type;
77
77
  var opacity = Number.isFinite(annotation.opacity) ? Math.max(0,Math.min(1,annotation.opacity)) : 0.35;
78
+ if(style === 'highlight')opacity=Math.min(0.35,opacity);
78
79
  rects.forEach(function(rect) {
79
80
  var mark = doc.createElement('div');
80
81
  var bodyRect=doc.body.getBoundingClientRect();
@@ -87,8 +88,10 @@
87
88
  if (hasNote) {
88
89
  var last = rects[rects.length-1], button = doc.createElement('button');
89
90
  button.textContent = '●'; button.setAttribute('aria-label',options.labels().comment || '');
90
- 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';
91
- var key = Math.floor(last.top/44);
91
+ var pageWidth=Math.max(44, options.pageWidth ? options.pageWidth() : win.innerWidth);
92
+ var bodyRect=doc.body.getBoundingClientRect(), column=Math.max(0,Math.floor((last.left-bodyRect.left)/pageWidth));
93
+ 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';
94
+ var key = column+":"+Math.floor(last.top/44);
92
95
  var neighbors=noteGroups.get(key);
93
96
  if(!neighbors){neighbors={button:button,ids:[]};noteGroups.set(key,neighbors);group.appendChild(button);}
94
97
  neighbors.ids.push(annotation.id);
@@ -109,10 +112,10 @@
109
112
  doc.addEventListener('click',tapMarkup);
110
113
  var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
111
114
  if (observer) observer.observe(doc.body);
112
- win.addEventListener('resize',schedule); doc.addEventListener('load',schedule,true);
115
+ win.addEventListener('resize',schedule);win.addEventListener('scroll',schedule); doc.addEventListener('load',schedule,true);
113
116
  if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(schedule);
114
117
  schedule();
115
- 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();}};
118
+ return {schedule:schedule, 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;if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',schedule);win.removeEventListener('scroll',schedule);doc.removeEventListener('load',schedule,true);doc.removeEventListener('click',tapMarkup);overlay.remove();doc.body.style.position=originalPosition;registry.clear();}};
116
119
  }
117
120
  return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
118
121
  });
@@ -15,14 +15,18 @@ it('renders true strikeout, grouped notes and reconciles 120 stable IDs after re
15
15
  let top=20;Object.defineProperty(range,'getClientRects',{value:()=>[{left:10,right:50,top,bottom:top+20,width:40,height:20}]});
16
16
  let queue:FrameRequestCallback[]=[];
17
17
  const originalRAF=window.requestAnimationFrame;window.requestAnimationFrame=cb=>{queue.push(cb);return queue.length;};
18
- let snapshot=Array.from({length:120},(_,i)=>({id:`n${i}`,anchor,color:'#ff0000',markupStyle:'strikeout',noteContent:`note ${i}`}));
18
+ let snapshot=Array.from({length:120},(_,i)=>({id:`n${i}`,anchor,color:'#ff0000',opacity:1,markupStyle:'strikeout',noteContent:`note ${i}`}));
19
19
  const taps:string[]=[];
20
- const renderer=annotations.install({document,window,range:()=>range},{href:'chapter.xhtml',documentId:undefined,annotations:()=>snapshot,labels:()=>({comment:'Nota'}),isCurrent:()=>true,onTap:(id:string)=>taps.push(id)});
20
+ const renderer=annotations.install({document,window,range:()=>range},{href:'chapter.xhtml',documentId:undefined,pageWidth:()=>1024,annotations:()=>snapshot,labels:()=>({comment:'Nota'}),isCurrent:()=>true,onTap:(id:string)=>taps.push(id)});
21
21
  const flush=()=>{const tasks=queue;queue=[];tasks.forEach(cb=>cb(0));};flush();
22
22
  expect(document.querySelectorAll('[data-annotation-id]')).toHaveLength(120);
23
23
  expect(document.querySelectorAll('[data-papyrus-overlay] button')).toHaveLength(1);
24
24
  const group=document.querySelector('[data-annotation-id="n0"]')!;
25
25
  expect((group.firstChild as HTMLElement).style.top).toBe('30px');
26
+ expect((group.lastChild as HTMLElement).style.left).toBe('980px');
27
+ snapshot=snapshot.map(item=>({...item,markupStyle:'highlight'}));renderer.schedule();flush();
28
+ expect(Number((group.firstChild as HTMLElement).style.opacity)).toBeLessThanOrEqual(0.35);
29
+ snapshot=snapshot.map(item=>({...item,markupStyle:'strikeout'}));renderer.schedule();flush();
26
30
  expect((group.lastChild as HTMLElement).textContent).toBe('120');
27
31
  (group.lastChild as HTMLElement).click();expect(taps).toEqual(['n0']);
28
32
  top=80;renderer.schedule();flush();expect(document.querySelector('[data-annotation-id="n0"]')).toBe(group);expect((group.firstChild as HTMLElement).style.top).toBe('90px');
@@ -0,0 +1,33 @@
1
+ (function(root){
2
+ function mode(value){return value==='chapter-scroll'?'chapter-scroll':'paged';}
3
+ function validCfi(value){return typeof value==='string' && value.length<=4096 && /^epubcfi\([^\n<>]+\)$/.test(value);}
4
+ function options(value){return {manager:'default',flow:mode(value)==='paged'?'paginated':'scrolled-doc',spread:'none',width:'100%',height:'100%',allowScriptedContent:false};}
5
+ function gesture(start,end,state){
6
+ if(!start || !end || state.selected || state.interactive || state.multitouch)return null;
7
+ var dx=end.x-start.x,dy=end.y-start.y,elapsed=end.time-start.time;
8
+ 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';
9
+ if(elapsed>=0 && elapsed<450 && Math.hypot(dx,dy)<=12)return 'tap';
10
+ return null;
11
+ }
12
+ function location(raw,count,readingMode){
13
+ var start=raw && raw.start;if(!start || !validCfi(start.cfi))return null;
14
+ var displayed=start.displayed||{},chapter=Math.max(1,(Number(start.index)||0)+1);
15
+ var visualPage=mode(readingMode)==='paged' && Number.isFinite(displayed.page)?displayed.page:null;
16
+ var total=mode(readingMode)==='paged' && Number.isFinite(displayed.total)?displayed.total:null;
17
+ var fraction=total?Math.max(0,(visualPage-1)/total):0;
18
+ 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:Boolean(raw.atStart),atEnd:Boolean(raw.atEnd)};
19
+ }
20
+ function install(contents,config){
21
+ var doc=contents.document,win=contents.window,start=null,blocked=false;
22
+ 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;}
23
+ function selected(){return Boolean(String(win.getSelection()||'').trim()) || config.blocked();}
24
+ function interactive(target){return Boolean(target && target.closest && target.closest('a,button,input,textarea,select,[contenteditable="true"],[data-papyrus-overlay]'));}
25
+ function begin(e){start=point(e);blocked=e.touches.length!==1 || selected() || interactive(e.target);}
26
+ function move(e){if(e.touches.length!==1)blocked=true;}
27
+ function end(e){var action=gesture(start,point(e),{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);}
28
+ function cancel(){start=null;blocked=true;}
29
+ doc.addEventListener('touchstart',begin,{passive:true});doc.addEventListener('touchmove',move,{passive:true});doc.addEventListener('touchend',end,{passive:true});doc.addEventListener('touchcancel',cancel,{passive:true});
30
+ return function(){doc.removeEventListener('touchstart',begin);doc.removeEventListener('touchmove',move);doc.removeEventListener('touchend',end);doc.removeEventListener('touchcancel',cancel);};
31
+ }
32
+ root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,install:install};
33
+ })(typeof window==='undefined'?globalThis:window);
@@ -0,0 +1,22 @@
1
+ import {readFileSync} from 'node:fs';
2
+ import {JSDOM} from 'jsdom';
3
+ import {describe,it,expect} from 'vitest';
4
+ const load=()=>{const d=new JSDOM('',{runScripts:'outside-only'});d.window.eval(readFileSync('packages/ui-react-native/runtime/epubReader.js','utf8'));return (d.window as any).PapyrusEpubReader;};
5
+ describe('EPUB reading policy',()=>{
6
+ it('defaults to real paginated default manager and one visual page',()=>{const p=load();expect(p.options('paged')).toMatchObject({manager:'default',flow:'paginated',spread:'none'});expect(p.mode()).toBe('paged');expect(p.options('chapter-scroll').flow).toBe('scrolled-doc');});
7
+ it('recognizes one swipe with RTL reversal, excluding selection/interactivity/multitouch',()=>{const p=load(),start={x:200,y:100,time:0},end={x:40,y:108,time:200};expect(p.gesture(start,end,{mode:'paged'})).toBe('next');expect(p.gesture(start,end,{mode:'paged',rtl:true})).toBe('prev');for(const k of ['selected','interactive','multitouch'])expect(p.gesture(start,end,{mode:'paged',[k]:true})).toBe(null);expect(p.gesture(start,{x:202,y:103,time:600},{mode:'paged'})).toBe(null);expect(p.gesture(start,{x:202,y:103,time:100},{mode:'paged'})).toBe('tap');});
8
+ it('separates chapter and visual page and never treats spine count as pages',()=>{const p=load();expect(p.location({start:{index:2,cfi:'epubcfi(/6/6!/4/2)',displayed:{page:4,total:12}}},10,'paged')).toMatchObject({chapter:3,chapterCount:10,visualPage:4,visualPageCount:12,cfi:'epubcfi(/6/6!/4/2)'});expect(p.location({start:{index:2,cfi:'epubcfi(/6/6!/4/2)'}},10,'chapter-scroll').visualPage).toBe(null);});
9
+ it('rejects invalid location CFIs',()=>{expect(load().validCfi('javascript:bad')).toBe(false);expect(load().validCfi('epubcfi(/6/4!/4/2)')).toBe(true);});
10
+ });
11
+
12
+ describe('EPUB iframe gestures', () => {
13
+ it('cleans listeners and excludes annotations and links from tap', () => {
14
+ const d=new JSDOM('<p>text</p><a href="#">link</a>',{runScripts:'outside-only'});
15
+ d.window.eval(readFileSync('packages/ui-react-native/runtime/epubReader.js','utf8'));
16
+ const p=(d.window as any).PapyrusEpubReader;
17
+ let taps=0,hit=true;
18
+ const cleanup=p.install({document:d.window.document,window:d.window},{mode:()=> 'paged',rtl:()=>false,blocked:()=>false,current:()=>true,annotationHit:()=>hit,tap:()=>taps++,turn:()=>{}});
19
+ function touch(target:Element){for(const type of ['touchstart','touchend']){const e=new d.window.Event(type,{bubbles:true});Object.defineProperties(e,{touches:{value:type==='touchend'?[]:[{clientX:30,clientY:40}]},changedTouches:{value:[{clientX:30,clientY:40}]}});target.dispatchEvent(e);}}
20
+ touch(d.window.document.querySelector('p')!);expect(taps).toBe(0);hit=false;touch(d.window.document.querySelector('a')!);expect(taps).toBe(0);touch(d.window.document.querySelector('p')!);expect(taps).toBe(1);cleanup();touch(d.window.document.querySelector('p')!);expect(taps).toBe(1);
21
+ });
22
+ });
@@ -0,0 +1,45 @@
1
+ import {readFileSync} from 'node:fs';
2
+ import {JSDOM} from 'jsdom';
3
+ import {describe,it,expect,vi} from 'vitest';
4
+
5
+ function runtime() {
6
+ const dom=new JSDOM('<div id="viewer"></div>',{runScripts:'outside-only'});
7
+ const messages:any[]=[], renditions:any[]=[];
8
+ (dom.window as any).ReactNativeWebView={postMessage:(raw:string)=>messages.push(JSON.parse(raw))};
9
+ const book:any={ready:Promise.resolve(),spine:{items:[{href:'a.xhtml'},{href:'b.xhtml'}]},loaded:{navigation:Promise.resolve({toc:[]})},destroy:vi.fn(),renderTo:vi.fn((_viewer,options)=>{
10
+ const events:any={};
11
+ const r:any={options,hooks:{content:{register:vi.fn()},unloaded:{register:vi.fn()}},on:(name:string,fn:any)=>events[name]=fn,themes:{fontSize:vi.fn()},destroy:vi.fn(),display:vi.fn(async()=>{}),next:vi.fn(async()=>{}),prev:vi.fn(async()=>{}),emit:(name:string,data:any)=>events[name]?.(data)};
12
+ renditions.push(r);return r;
13
+ })};
14
+ (dom.window as any).ePub=()=>book;
15
+ dom.window.eval(readFileSync('packages/ui-react-native/runtime/runtime.js','utf8'));
16
+ const send=async(id:string,kind:string,payload:any)=>{
17
+ dom.window.dispatchEvent(new dom.window.MessageEvent('message',{data:JSON.stringify({id,kind,payload})}));
18
+ for(let n=0;n<100;n++){const result=messages.find(m=>m.type==='response'&&m.id===id);if(result)return result;await new Promise(r=>setTimeout(r,1));}
19
+ throw new Error('No response: '+id);
20
+ };
21
+ return {send,messages,renditions,book,dom};
22
+ }
23
+ describe('EPUB runtime bridge',()=>{
24
+ it('loads paginated by default and emits visual location within the same chapter',async()=>{
25
+ const r=runtime();expect((await r.send('load','load',{type:'epub',documentSessionId:'s',source:{kind:'base64',data:'AA=='}})).ok).toBe(true);
26
+ expect(r.renditions[0].options.flow).toBe('paginated');
27
+ r.renditions[0].emit('relocated',{start:{index:0,href:'a.xhtml',cfi:'epubcfi(/6/2!/4/2:10)',displayed:{page:2,total:8}}});
28
+ expect(r.messages).toContainEqual(expect.objectContaining({name:'EPUB_LOCATION',payload:expect.objectContaining({location:expect.objectContaining({visualPage:2,chapter:1})})}));
29
+ });
30
+ it('rebuilds chapter scroll at the current CFI and rejects a stale session',async()=>{
31
+ const r=runtime();await r.send('load','load',{type:'epub',documentSessionId:'s',source:{kind:'base64',data:'AA=='}});
32
+ const cfi='epubcfi(/6/2!/4/2:10)';r.renditions[0].emit('relocated',{start:{index:0,cfi,displayed:{page:2,total:8}}});
33
+ expect((await r.send('mode','set-epub-view-mode',{epubMode:'chapter-scroll',documentSessionId:'s'})).ok).toBe(true);
34
+ expect(r.renditions[1].options.flow).toBe('scrolled-doc');expect(r.renditions[1].display).toHaveBeenCalledWith(cfi);
35
+ expect((await r.send('old','epub-go-to-location',{cfi,documentSessionId:'old'})).ok).toBe(false);
36
+ });
37
+ it('serializes rapid mode changes and restores on the rebuilt rendition',async()=>{
38
+ const r=runtime();await r.send('load','load',{type:'epub',documentSessionId:'s',source:{kind:'base64',data:'AA=='}});
39
+ const cfi='epubcfi(/6/2!/4/2:10)';
40
+ await Promise.all([r.send('scroll','set-epub-view-mode',{epubMode:'chapter-scroll',documentSessionId:'s'}),r.send('paged','set-epub-view-mode',{epubMode:'paged',documentSessionId:'s'}),r.send('restore','epub-go-to-location',{cfi,documentSessionId:'s'})]);
41
+ expect(r.renditions.at(-1).options.flow).toBe('paginated');
42
+ expect(r.renditions.at(-1).display).toHaveBeenCalledWith(cfi);
43
+ });
44
+ it('keeps TXT zoom independent of EPUB state',async()=>{const r=runtime();await r.send('load','load',{type:'text',source:{kind:'text',text:'sample'}});expect((await r.send('zoom','set-zoom',{zoom:1.2})).ok).toBe(true);});
45
+ });
@@ -20483,6 +20483,42 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20483
20483
  <!-- @papyrus-comic-runtime:end -->
20484
20484
  <script>
20485
20485
  (function () {
20486
+ /* @papyrus-epub-reader:start */
20487
+ (function(root){
20488
+ function mode(value){return value==='chapter-scroll'?'chapter-scroll':'paged';}
20489
+ function validCfi(value){return typeof value==='string' && value.length<=4096 && /^epubcfi\([^\n<>]+\)$/.test(value);}
20490
+ function options(value){return {manager:'default',flow:mode(value)==='paged'?'paginated':'scrolled-doc',spread:'none',width:'100%',height:'100%',allowScriptedContent:false};}
20491
+ function gesture(start,end,state){
20492
+ if(!start || !end || state.selected || state.interactive || state.multitouch)return null;
20493
+ var dx=end.x-start.x,dy=end.y-start.y,elapsed=end.time-start.time;
20494
+ 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';
20495
+ if(elapsed>=0 && elapsed<450 && Math.hypot(dx,dy)<=12)return 'tap';
20496
+ return null;
20497
+ }
20498
+ function location(raw,count,readingMode){
20499
+ var start=raw && raw.start;if(!start || !validCfi(start.cfi))return null;
20500
+ var displayed=start.displayed||{},chapter=Math.max(1,(Number(start.index)||0)+1);
20501
+ var visualPage=mode(readingMode)==='paged' && Number.isFinite(displayed.page)?displayed.page:null;
20502
+ var total=mode(readingMode)==='paged' && Number.isFinite(displayed.total)?displayed.total:null;
20503
+ var fraction=total?Math.max(0,(visualPage-1)/total):0;
20504
+ 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:Boolean(raw.atStart),atEnd:Boolean(raw.atEnd)};
20505
+ }
20506
+ function install(contents,config){
20507
+ var doc=contents.document,win=contents.window,start=null,blocked=false;
20508
+ 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;}
20509
+ function selected(){return Boolean(String(win.getSelection()||'').trim()) || config.blocked();}
20510
+ function interactive(target){return Boolean(target && target.closest && target.closest('a,button,input,textarea,select,[contenteditable="true"],[data-papyrus-overlay]'));}
20511
+ function begin(e){start=point(e);blocked=e.touches.length!==1 || selected() || interactive(e.target);}
20512
+ function move(e){if(e.touches.length!==1)blocked=true;}
20513
+ function end(e){var action=gesture(start,point(e),{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);}
20514
+ function cancel(){start=null;blocked=true;}
20515
+ doc.addEventListener('touchstart',begin,{passive:true});doc.addEventListener('touchmove',move,{passive:true});doc.addEventListener('touchend',end,{passive:true});doc.addEventListener('touchcancel',cancel,{passive:true});
20516
+ return function(){doc.removeEventListener('touchstart',begin);doc.removeEventListener('touchmove',move);doc.removeEventListener('touchend',end);doc.removeEventListener('touchcancel',cancel);};
20517
+ }
20518
+ root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,install:install};
20519
+ })(typeof window==='undefined'?globalThis:window);
20520
+
20521
+ /* @papyrus-epub-reader:end */
20486
20522
  /* @papyrus-epub-annotations:start */
20487
20523
  (function (root, factory) {
20488
20524
  if (typeof module === 'object' && module.exports) module.exports = factory();
@@ -20561,6 +20597,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20561
20597
  var color = /^#[0-9a-f]{6}$/i.test(annotation.color) ? annotation.color : '#FFC928';
20562
20598
  var style = annotation.markupStyle || annotation.type;
20563
20599
  var opacity = Number.isFinite(annotation.opacity) ? Math.max(0,Math.min(1,annotation.opacity)) : 0.35;
20600
+ if(style === 'highlight')opacity=Math.min(0.35,opacity);
20564
20601
  rects.forEach(function(rect) {
20565
20602
  var mark = doc.createElement('div');
20566
20603
  var bodyRect=doc.body.getBoundingClientRect();
@@ -20573,8 +20610,10 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20573
20610
  if (hasNote) {
20574
20611
  var last = rects[rects.length-1], button = doc.createElement('button');
20575
20612
  button.textContent = '●'; button.setAttribute('aria-label',options.labels().comment || '');
20576
- 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';
20577
- var key = Math.floor(last.top/44);
20613
+ var pageWidth=Math.max(44, options.pageWidth ? options.pageWidth() : win.innerWidth);
20614
+ var bodyRect=doc.body.getBoundingClientRect(), column=Math.max(0,Math.floor((last.left-bodyRect.left)/pageWidth));
20615
+ 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';
20616
+ var key = column+":"+Math.floor(last.top/44);
20578
20617
  var neighbors=noteGroups.get(key);
20579
20618
  if(!neighbors){neighbors={button:button,ids:[]};noteGroups.set(key,neighbors);group.appendChild(button);}
20580
20619
  neighbors.ids.push(annotation.id);
@@ -20595,10 +20634,10 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20595
20634
  doc.addEventListener('click',tapMarkup);
20596
20635
  var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
20597
20636
  if (observer) observer.observe(doc.body);
20598
- win.addEventListener('resize',schedule); doc.addEventListener('load',schedule,true);
20637
+ win.addEventListener('resize',schedule);win.addEventListener('scroll',schedule); doc.addEventListener('load',schedule,true);
20599
20638
  if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(schedule);
20600
20639
  schedule();
20601
- 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();}};
20640
+ return {schedule:schedule, 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;if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',schedule);win.removeEventListener('scroll',schedule);doc.removeEventListener('load',schedule,true);doc.removeEventListener('click',tapMarkup);overlay.remove();doc.body.style.position=originalPosition;registry.clear();}};
20602
20641
  }
20603
20642
  return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
20604
20643
  });
@@ -21679,26 +21718,55 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21679
21718
  pageCount,
21680
21719
  });
21681
21720
 
21721
+ if (book.locations?.generate) {
21722
+ const activeBook=book;
21723
+ activeBook.locations.generate(150).then(() => {
21724
+ if (isCurrentLoad() && activeBook===book && epubLocation) {
21725
+ epubLocation.progress=activeBook.locations.percentageFromCfi(epubLocation.cfi);
21726
+ sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
21727
+ }
21728
+ }).catch(() => {});
21729
+ }
21730
+ return renderEpubRendition(loadId,generation,null);
21731
+ };
21732
+
21733
+ const renderEpubRendition = async (loadId,generation,target) => {
21734
+ const diagnosticPayload={loadId};
21735
+ const isCurrentLoad=()=>generation===epubLoadGeneration;
21736
+ if(epubInteractionCleanup)epubInteractionCleanup();
21737
+ if(rendition)rendition.destroy();
21682
21738
  clearViewer();
21683
- rendition = book.renderTo(viewer, {
21684
- manager: 'continuous',
21685
- allowScriptedContent: false,
21686
- width: '100%',
21687
- height: '100%',
21688
- flow: 'scrolled-continuous',
21689
- spread: 'none',
21690
- });
21739
+ rendition = book.renderTo(viewer, window.PapyrusEpubReader.options(epubViewMode === 'single' ? 'paged' : 'chapter-scroll'));
21691
21740
  sendEpubDiagnostic('epub.rendition.created', diagnosticPayload);
21692
21741
 
21693
21742
  if (rendition && rendition.hooks && rendition.hooks.content) {
21694
21743
  const session = documentSessionId;
21695
21744
  const activeRendition = rendition;
21696
21745
  rendition.hooks.content.register((contents) => {
21746
+ const removeGestureListeners = window.PapyrusEpubReader.install(contents, {
21747
+ mode: () => epubViewMode === 'single' ? 'paged' : 'chapter-scroll',
21748
+ rtl: () => book.package?.metadata?.direction === 'rtl' || book.spine?.direction === 'rtl',
21749
+ blocked: () => Boolean(nativeMenuSelection && String(nativeMenuSelection.contents?.window?.getSelection() || '').trim()),
21750
+ annotationHit: point => epubAnnotationRenderers.get(contents)?.hit(point.x,point.y),
21751
+ current: () => session === documentSessionId && activeRendition === rendition,
21752
+ tap: () => sendEvent('EPUB_CONTENT_TAP',{documentSessionId:session}),
21753
+ turn: direction => {
21754
+ epubNavigationQueue = epubNavigationQueue.catch(()=>{}).then(() => {
21755
+ if(session !== documentSessionId || activeRendition !== rendition)return;
21756
+ return direction<0?activeRendition.prev():activeRendition.next();
21757
+ });
21758
+ epubNavigationQueue.catch(error => sendEvent('EPUB_NAVIGATION_ERROR',{message:String(error)}));
21759
+ },
21760
+ });
21761
+ const gestureCleanup = () => {removeGestureListeners();epubSelectionCleanups.delete(gestureCleanup);};
21762
+ epubSelectionCleanups.add(gestureCleanup);
21763
+ contents.__papyrusGestureCleanup=gestureCleanup;
21697
21764
  const href = spineItems[contents.sectionIndex]?.href;
21698
21765
  if (href && window.PapyrusEpubAnnotations) {
21699
21766
  const renderer = window.PapyrusEpubAnnotations.install(contents, {
21700
21767
  href, get documentId() { return annotationDocumentId; },
21701
21768
  isCurrent: () => session === documentSessionId && activeRendition === rendition,
21769
+ pageWidth: () => activeRendition.layout?.width || viewer.clientWidth,
21702
21770
  annotations: () => epubAnnotations, labels: () => annotationLabels,
21703
21771
  onTap: (id, ids) => sendEvent("EPUB_ANNOTATION_TAP", {documentSessionId: session, id, ids}),
21704
21772
  });
@@ -21717,8 +21785,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21717
21785
  typeof selectionWindow.getSelection === 'function'
21718
21786
  ) {
21719
21787
  let selectionTimer = null;
21720
- let lastSelectionKey = '';
21721
- let lastSelectionAt = 0;
21788
+
21722
21789
  const reportSelection = () => {
21723
21790
  selectionTimer = null;
21724
21791
  if (session !== documentSessionId || activeRendition !== rendition) return;
@@ -21733,11 +21800,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21733
21800
  try { cfi = contents.cfiFromRange(range); } catch (_) { return; }
21734
21801
  const anchor = window.PapyrusEpubAnnotations?.selection(contents, cfi, href, annotationDocumentId);
21735
21802
  if (!anchor) return;
21736
- const key = anchor.cfiRange + ':' + anchor.quote;
21737
- const now = Date.now();
21738
- if (key === lastSelectionKey && now - lastSelectionAt < 750) return;
21739
- lastSelectionKey = key;
21740
- lastSelectionAt = now;
21803
+ if (nativeMenuSelection?.payload?.anchor?.cfiRange === anchor.cfiRange && nativeMenuSelection?.payload?.anchor?.quote === anchor.quote) return;
21741
21804
  const payload = {documentSessionId: session, text: anchor.quote,
21742
21805
  pageIndex: contents.sectionIndex + 1, anchor};
21743
21806
  nativeMenuSelection = {contents, payload};
@@ -21752,6 +21815,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21752
21815
  // The content document still emits selectionchange, so resolve its
21753
21816
  // live Range to a CFI before the native menu action can fire.
21754
21817
  if (text) reportSelection();
21818
+ else if (nativeMenuSelection?.contents === contents) nativeMenuSelection = null;
21755
21819
  };
21756
21820
  selectionDocument.addEventListener('selectionchange', handleSelectionChange);
21757
21821
  const selectionCleanup = () => {
@@ -21774,6 +21838,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21774
21838
  typeof rendition.hooks.unloaded.register === 'function'
21775
21839
  ) {
21776
21840
  rendition.hooks.unloaded.register((view) => {
21841
+ view?.contents?.__papyrusGestureCleanup?.();
21777
21842
  const cleanup = view && view.contents && view.contents.__papyrusSelectionCleanup;
21778
21843
  if (typeof cleanup === 'function') cleanup();
21779
21844
  const renderer = epubAnnotationRenderers.get(view?.contents);
@@ -21789,13 +21854,13 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21789
21854
  if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
21790
21855
  const href = spineItems[contents.sectionIndex]?.href;
21791
21856
  const anchor = href && window.PapyrusEpubAnnotations?.selection(contents,cfiRange,href,annotationDocumentId);
21792
- if (!anchor) return;
21857
+ if (!anchor || nativeMenuSelection?.payload?.anchor?.cfiRange === anchor.cfiRange && nativeMenuSelection?.payload?.anchor?.quote === anchor.quote) return;
21793
21858
  epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(true);
21794
21859
  const payload = {documentSessionId: selectionSession, text: anchor.quote, pageIndex: contents.sectionIndex+1, anchor};
21795
21860
  nativeMenuSelection = {contents, payload};
21796
21861
  sendEvent('EPUB_TEXT_SELECTED', payload);
21797
21862
  });
21798
- ['rendered','resized','layout'].forEach(name => rendition.on(name, () => {
21863
+ ['rendered','resized','layout','relocated'].forEach(name => rendition.on(name, () => {
21799
21864
  if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => renderer.schedule());
21800
21865
  }));
21801
21866
  rendition.on('relocated', (location) => {
@@ -21805,6 +21870,11 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21805
21870
  if (index < 0 && start && start.href) index = getSpineIndexByHref(start.href);
21806
21871
  if (index < 0 || index >= spineItems.length) return;
21807
21872
  epubScrollDiagnostics && epubScrollDiagnostics.relocated(location);
21873
+ epubLocation=window.PapyrusEpubReader.location(location,spineItems.length,epubViewMode==='single'?'paged':'chapter-scroll');
21874
+ if(epubLocation){
21875
+ if(book.locations?.length?.()>0)epubLocation.progress=book.locations.percentageFromCfi(epubLocation.cfi);
21876
+ sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
21877
+ }
21808
21878
  const nextPage = index + 1;
21809
21879
  if (nextPage === currentPage) return;
21810
21880
  currentPage = nextPage;
@@ -21812,9 +21882,10 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21812
21882
  });
21813
21883
  }
21814
21884
 
21885
+ applyEpubZoom();
21815
21886
  sendEpubDiagnostic('epub.display.start', diagnosticPayload);
21816
21887
  await withEpubStageTimeout(
21817
- displayEpubPage(0),
21888
+ rendition.display(target || spineItems[0]?.href),
21818
21889
  'rendition.display',
21819
21890
  diagnosticPayload,
21820
21891
  );
@@ -21826,7 +21897,6 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21826
21897
  typeof rendition.manager.on === 'function' &&
21827
21898
  typeof rendition.on === 'function'
21828
21899
  ) {
21829
- let touchStart = null;
21830
21900
  let lastScrollOffset = -1;
21831
21901
  const manager = rendition.manager;
21832
21902
  epubScrollDiagnostics = createEpubScrollDiagnostics(manager);
@@ -21901,57 +21971,19 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21901
21971
  return result;
21902
21972
  };
21903
21973
  }
21904
- const getTouchPoint = (event) => {
21905
- const touch =
21906
- (event && event.changedTouches && event.changedTouches[0]) ||
21907
- (event && event.touches && event.touches[0]);
21908
- if (!touch) return null;
21909
- const x = Number(touch.screenX ?? touch.clientX);
21910
- const y = Number(touch.screenY ?? touch.clientY);
21911
- return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
21912
- };
21913
- const handleScroll = (event) => {
21974
+ const handleScroll = event => {
21914
21975
  const offsetY = Math.max(0, Number(event && event.top) || 0);
21915
21976
  epubScrollDiagnostics && epubScrollDiagnostics.scroll(event);
21916
- if (Math.abs(offsetY - lastScrollOffset) < 1) return;
21917
- lastScrollOffset = offsetY;
21918
- sendEvent('VIEWER_SCROLL', { offsetY });
21919
- };
21920
- const handleTouchStart = (event) => {
21921
- touchStart = event?.touches?.length > 1 ? null : getTouchPoint(event);
21922
- epubScrollDiagnostics && epubScrollDiagnostics.touchStart(event);
21977
+ if (Math.abs(offsetY-lastScrollOffset)<1) return;
21978
+ lastScrollOffset=offsetY;
21979
+ sendEvent('VIEWER_SCROLL',{offsetY});
21923
21980
  };
21924
- const handleTouchMove = (event) => {
21925
- if (event?.touches?.length > 1) touchStart = null;
21926
- epubScrollDiagnostics && epubScrollDiagnostics.touchMove(event);
21927
- };
21928
- const handleTouchEnd = (event) => {
21929
- const end = getTouchPoint(event);
21930
- const start = touchStart;
21931
- touchStart = null;
21932
- const selectedText = event?.target?.ownerDocument?.defaultView?.getSelection?.()?.toString().trim() || '';
21933
- if (!selectedText && start && end && epubViewMode === 'single' && Math.abs(end.x-start.x) >= 48 && Math.abs(end.x-start.x) > Math.abs(end.y-start.y)*1.5) {
21934
- const active = rendition;
21935
- const interactionSession = documentSessionId;
21936
- epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
21937
- if (active !== rendition || interactionSession !== documentSessionId) return;
21938
- return end.x < start.x ? active.next() : active.prev();
21939
- });
21940
- epubNavigationQueue.catch(error => sendEvent('EPUB_NAVIGATION_ERROR', {message:String(error)}));
21941
- } else if (!selectedText && start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
21942
- sendEvent('VIEWER_TAP', {});
21943
- }
21944
- epubScrollDiagnostics && epubScrollDiagnostics.touchEnd(event);
21945
- };
21946
-
21947
21981
  manager.on('scroll', handleScroll);
21948
21982
  const handleScrolled = (event) => {
21949
21983
  epubScrollDiagnostics && epubScrollDiagnostics.settled(event);
21950
21984
  };
21951
21985
  manager.on('scrolled', handleScrolled);
21952
- rendition.on('touchstart', handleTouchStart);
21953
- rendition.on('touchmove', handleTouchMove);
21954
- rendition.on('touchend', handleTouchEnd);
21986
+
21955
21987
  epubInteractionCleanup = () => {
21956
21988
  epubSelectionCleanups.forEach((cleanup) => cleanup());
21957
21989
  epubSelectionCleanups.clear();
@@ -21960,16 +21992,9 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21960
21992
  if (originalEnqueue) queue.enqueue = originalEnqueue;
21961
21993
  if (typeof manager.off === 'function') manager.off('scroll', handleScroll);
21962
21994
  if (typeof manager.off === 'function') manager.off('scrolled', handleScrolled);
21963
- if (typeof rendition.off === 'function') {
21964
- rendition.off('touchstart', handleTouchStart);
21965
- rendition.off('touchmove', handleTouchMove);
21966
- rendition.off('touchend', handleTouchEnd);
21967
- }
21968
21995
  epubScrollDiagnostics = null;
21969
21996
  };
21970
21997
  }
21971
- applyEpubZoom();
21972
-
21973
21998
  const outline = await withEpubStageTimeout(
21974
21999
  buildOutline(),
21975
22000
  'outline',
@@ -22107,7 +22132,8 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22107
22132
  };
22108
22133
 
22109
22134
  let nativeMenuSelection = null;
22110
- let epubViewMode = "continuous";
22135
+ let epubViewMode = "single";
22136
+ let epubLocation = null;
22111
22137
 
22112
22138
  const handleCommand = async (message) => {
22113
22139
  const { id, kind, payload } = message;
@@ -22120,7 +22146,9 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22120
22146
  epubAnnotations = [];
22121
22147
  annotationDocumentId = undefined;
22122
22148
  zoom = 1.0;
22123
- epubViewMode = "continuous";
22149
+ epubViewMode = payload.epubMode === "chapter-scroll" ? "continuous" : "single";
22150
+ epubLocation = null;
22151
+ nativeMenuSelection = null;
22124
22152
  epubNavigationRequest += 1;
22125
22153
  epubNavigationQueue = Promise.resolve();
22126
22154
 
@@ -22234,25 +22262,40 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22234
22262
 
22235
22263
  if (kind === 'set-epub-view-mode') {
22236
22264
  if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB layout session');
22237
- epubViewMode = payload.viewMode === 'single' ? 'single' : 'continuous';
22238
- const active = rendition;
22239
- epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
22240
- if (active !== rendition || payload.documentSessionId !== documentSessionId) return;
22241
- const flow = epubViewMode === 'single' ? 'paginated' : 'scrolled-continuous';
22242
- if (active.settings.flow === flow) return;
22243
- active.flow(flow);
22244
- return active.q.enqueue(() => Promise.resolve());
22245
- });
22246
- await epubNavigationQueue;
22247
- sendResponse(id,true,null); return;
22265
+ const next=payload.epubMode ? (payload.epubMode==='paged'?'single':'continuous') : (payload.viewMode==='single'?'single':'continuous');
22266
+ {
22267
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
22268
+ if(payload.documentSessionId!==documentSessionId || next===epubViewMode)return;
22269
+ const target=epubLocation?.cfi;
22270
+ epubViewMode=next;
22271
+ await renderEpubRendition('layout',epubLoadGeneration,target);
22272
+ });
22273
+ await epubNavigationQueue;
22274
+ }
22275
+ sendResponse(id,true,null);return;
22276
+ }
22277
+ if(kind === 'epub-go-to-location'){
22278
+ if(currentType!=='epub' || payload.documentSessionId!==documentSessionId || !window.PapyrusEpubReader.validCfi(payload.cfi))throw new Error('Invalid EPUB location');
22279
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(()=>payload.documentSessionId===documentSessionId?rendition.display(payload.cfi):undefined);
22280
+ await epubNavigationQueue;sendResponse(id,true,null);return;
22248
22281
  }
22249
22282
 
22250
22283
  if (kind === 'set-zoom') {
22251
- zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
22252
- applyZoom();
22253
- sendState();
22254
- sendResponse(id, true, { zoom });
22255
- return;
22284
+ const requestedZoom=Math.max(0.5,Math.min(4.0,payload.zoom || 1.0));
22285
+ if(currentType === 'epub') {
22286
+ const session=documentSessionId;
22287
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
22288
+ if(session!==documentSessionId)return;
22289
+ const active=rendition;
22290
+ const cfi=epubLocation?.cfi;
22291
+ zoom=requestedZoom;applyEpubZoom();
22292
+ // Let the content CSS update before redisplaying the logical anchor.
22293
+ await new Promise(resolve=>(window.requestAnimationFrame || (callback=>setTimeout(callback,0)))(resolve));
22294
+ if(cfi && active===rendition && session===documentSessionId)await active.display(cfi);
22295
+ });
22296
+ await epubNavigationQueue;
22297
+ } else {zoom=requestedZoom;applyZoom();}
22298
+ sendState();sendResponse(id,true,{zoom});return;
22256
22299
  }
22257
22300
 
22258
22301
  if (kind === 'set-rotation') {