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

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,127 @@
109
109
  /* @papyrus-comic-runtime:end */
110
110
 
111
111
  (function () {
112
+ /* @papyrus-epub-annotations:start */
113
+ (function (root, factory) {
114
+ if (typeof module === 'object' && module.exports) module.exports = factory();
115
+ else root.PapyrusEpubAnnotations = factory();
116
+ })(typeof window !== 'undefined' ? window : globalThis, function () {
117
+ function validAnchor(anchor) {
118
+ return anchor && anchor.version === 1 && anchor.kind === 'epub-cfi' &&
119
+ typeof anchor.cfiRange === 'string' && anchor.cfiRange.length <= 4096 && /^epubcfi\([^\n<>]+\)$/.test(anchor.cfiRange) &&
120
+ typeof anchor.href === 'string' && !/^(javascript|data):/i.test(anchor.href) &&
121
+ typeof anchor.quote === 'string' && anchor.quote.length > 0 && anchor.quote.length <= 100000;
122
+ }
123
+ function textNodes(document) {
124
+ var walker = document.createTreeWalker(document.body, 4);
125
+ var nodes = [], text = '', node;
126
+ while ((node = walker.nextNode())) {
127
+ if (node.parentElement.closest('script,style,[data-papyrus-overlay]')) continue;
128
+ nodes.push({node:node, start:text.length}); text += node.textContent;
129
+ }
130
+ return {nodes:nodes,text:text};
131
+ }
132
+ function recoverRange(document, anchor) {
133
+ var source = textNodes(document), match = null, start = source.text.indexOf(anchor.quote);
134
+ while (start >= 0) {
135
+ var end = start + anchor.quote.length;
136
+ if ((!anchor.prefix || source.text.slice(Math.max(0,start-anchor.prefix.length),start) === anchor.prefix) &&
137
+ (!anchor.suffix || source.text.slice(end,end+anchor.suffix.length) === anchor.suffix)) {
138
+ if (match) return null; match = {start:start,end:end};
139
+ }
140
+ start = source.text.indexOf(anchor.quote,start+1);
141
+ }
142
+ if (!match) return null;
143
+ var range = document.createRange();
144
+ var first = source.nodes.find(function(n){return n.start+n.node.length > match.start;});
145
+ var last = source.nodes.find(function(n){return n.start+n.node.length >= match.end;});
146
+ if (!first || !last) return null;
147
+ range.setStart(first.node,match.start-first.start); range.setEnd(last.node,match.end-last.start);
148
+ return range;
149
+ }
150
+ function resolveRange(contents, anchor) {
151
+ if (!validAnchor(anchor)) return null;
152
+ try { var range = contents.range(anchor.cfiRange); if (range && range.toString() === anchor.quote) return range; } catch (_) {}
153
+ return recoverRange(contents.document,anchor);
154
+ }
155
+ function selection(contents, cfiRange, href, documentId) {
156
+ var range;
157
+ try { range = contents.range(cfiRange); } catch (_) { return null; }
158
+ if (!range || !range.toString()) return null;
159
+ var before = contents.document.createRange(); before.selectNodeContents(contents.document.body); before.setEnd(range.startContainer,range.startOffset);
160
+ var after = contents.document.createRange(); after.selectNodeContents(contents.document.body); after.setStart(range.endContainer,range.endOffset);
161
+ var anchor = {version:1,kind:'epub-cfi',cfiRange:cfiRange,href:href,spineIndex:contents.sectionIndex,documentId:documentId,quote:range.toString(),prefix:before.toString().slice(-48),suffix:after.toString().slice(0,48)};
162
+ return validAnchor(anchor) ? anchor : null;
163
+ }
164
+ function install(contents, options) {
165
+ var doc = contents.document, win = contents.window, alive = true, frame = 0;
166
+ var overlay = doc.createElement('div'); overlay.dataset.papyrusOverlay = 'annotations';
167
+ overlay.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:0;pointer-events:none;z-index:3';
168
+ var originalPosition=doc.body.style.position;
169
+ if(win.getComputedStyle(doc.body).position==='static')doc.body.style.position='relative';
170
+ doc.body.appendChild(overlay);
171
+ var registry = new Map(), hitRegions = [];
172
+ function render() {
173
+ frame = 0; if (!alive || !options.isCurrent()) return;
174
+ var keep = new Set(), noteGroups = new Map();
175
+ hitRegions = [];
176
+ options.annotations().forEach(function(annotation) {
177
+ var anchor = annotation.anchor;
178
+ if (!validAnchor(anchor) || anchor.href !== options.href || anchor.documentId && options.documentId && anchor.documentId !== options.documentId) return;
179
+ var range = resolveRange(contents,anchor); if (!range) return;
180
+ var rects = Array.from(range.getClientRects()).filter(function(r){return r.width>0 && r.height>0;});
181
+ if (!rects.length) return;
182
+ keep.add(annotation.id);
183
+ var group = registry.get(annotation.id);
184
+ if (!group) { group = doc.createElement('div'); group.dataset.annotationId = annotation.id; overlay.appendChild(group); registry.set(annotation.id,group); if (!doc.defaultView.matchMedia || !doc.defaultView.matchMedia('(prefers-reduced-motion: reduce)').matches) { if (typeof group.animate === 'function') group.animate([{opacity:0},{opacity:1}],{duration:180,easing:'ease-out'}); } }
185
+ group.replaceChildren();
186
+ hitRegions.push({id:annotation.id,rects:rects});
187
+ var color = /^#[0-9a-f]{6}$/i.test(annotation.color) ? annotation.color : '#FFC928';
188
+ var style = annotation.markupStyle || annotation.type;
189
+ var opacity = Number.isFinite(annotation.opacity) ? Math.max(0,Math.min(1,annotation.opacity)) : 0.35;
190
+ rects.forEach(function(rect) {
191
+ var mark = doc.createElement('div');
192
+ var bodyRect=doc.body.getBoundingClientRect();
193
+ var height = style === 'highlight' ? rect.height : 2;
194
+ var y = style === 'strikeout' ? rect.top+rect.height/2 : style === 'underline' ? rect.bottom-2 : rect.top;
195
+ mark.style.cssText = 'position:absolute;pointer-events:none;left:'+(rect.left-bodyRect.left)+'px;top:'+(y-bodyRect.top)+'px;width:'+rect.width+'px;height:'+height+'px;background:'+color+';opacity:'+opacity;
196
+ if (['highlight','underline','strikeout'].includes(style)) group.appendChild(mark);
197
+ });
198
+ var hasNote = annotation.noteContent !== undefined || annotation.type === 'comment' || annotation.type === 'text';
199
+ if (hasNote) {
200
+ var last = rects[rects.length-1], button = doc.createElement('button');
201
+ 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);
204
+ var neighbors=noteGroups.get(key);
205
+ if(!neighbors){neighbors={button:button,ids:[]};noteGroups.set(key,neighbors);group.appendChild(button);}
206
+ neighbors.ids.push(annotation.id);
207
+ }
208
+ });
209
+ noteGroups.forEach(function(group){
210
+ group.button.textContent=group.ids.length>1?String(group.ids.length):'●';
211
+ group.button.addEventListener('click',function(e){e.stopPropagation();if(options.isCurrent())options.onTap(group.ids[0],group.ids);});
212
+ });
213
+ registry.forEach(function(group,id){if (!keep.has(id)) {group.remove();registry.delete(id);}});
214
+ }
215
+ function schedule() { if (alive && !frame) frame = win.requestAnimationFrame(render); }
216
+ function tapMarkup(event) {
217
+ if(!alive || !options.isCurrent() || String(win.getSelection() || '').length || event.target.closest('[data-papyrus-overlay]'))return;
218
+ 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
+ if(hit) {event.stopPropagation();options.onTap(hit.id);}
220
+ }
221
+ doc.addEventListener('click',tapMarkup);
222
+ var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
223
+ 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);
226
+ 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();}};
228
+ }
229
+ return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
230
+ });
231
+
232
+ /* @papyrus-epub-annotations:end */
112
233
  const viewer = document.getElementById('viewer');
113
234
  const DEFAULT_FONT_SIZE = 16;
114
235
  const TEXT_PAGE_CHUNK = 1600;
@@ -116,6 +237,11 @@
116
237
  let currentType = null;
117
238
  let book = null;
118
239
  let rendition = null;
240
+ let documentSessionId = null;
241
+ let annotationDocumentId;
242
+ let annotationLabels = {};
243
+ let epubAnnotations = [];
244
+ const epubAnnotationRenderers = new Map();
119
245
  let spineItems = [];
120
246
  let textPages = [];
121
247
  let textContainer = null;
@@ -523,6 +649,8 @@
523
649
  };
524
650
 
525
651
  const clearViewer = () => {
652
+ epubAnnotationRenderers.forEach(renderer => renderer.destroy());
653
+ epubAnnotationRenderers.clear();
526
654
  if (epubInteractionCleanup) {
527
655
  epubInteractionCleanup();
528
656
  epubInteractionCleanup = null;
@@ -1180,6 +1308,7 @@
1180
1308
  clearViewer();
1181
1309
  rendition = book.renderTo(viewer, {
1182
1310
  manager: 'continuous',
1311
+ allowScriptedContent: false,
1183
1312
  width: '100%',
1184
1313
  height: '100%',
1185
1314
  flow: 'scrolled-continuous',
@@ -1188,10 +1317,22 @@
1188
1317
  sendEpubDiagnostic('epub.rendition.created', diagnosticPayload);
1189
1318
 
1190
1319
  if (rendition && rendition.hooks && rendition.hooks.content) {
1320
+ const session = documentSessionId;
1321
+ const activeRendition = rendition;
1191
1322
  rendition.hooks.content.register((contents) => {
1323
+ const href = spineItems[contents.sectionIndex]?.href;
1324
+ if (href && window.PapyrusEpubAnnotations) {
1325
+ const renderer = window.PapyrusEpubAnnotations.install(contents, {
1326
+ href, get documentId() { return annotationDocumentId; },
1327
+ isCurrent: () => session === documentSessionId && activeRendition === rendition,
1328
+ annotations: () => epubAnnotations, labels: () => annotationLabels,
1329
+ onTap: (id, ids) => sendEvent("EPUB_ANNOTATION_TAP", {documentSessionId: session, id, ids}),
1330
+ });
1331
+ epubAnnotationRenderers.set(contents,renderer);
1332
+ }
1192
1333
  const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
1193
1334
  if (frame) {
1194
- frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
1335
+ frame.setAttribute('sandbox', 'allow-same-origin');
1195
1336
  }
1196
1337
  const selectionDocument = contents && contents.document;
1197
1338
  const selectionWindow = contents && contents.window;
@@ -1201,13 +1342,46 @@
1201
1342
  selectionWindow &&
1202
1343
  typeof selectionWindow.getSelection === 'function'
1203
1344
  ) {
1345
+ let selectionTimer = null;
1346
+ let lastSelectionKey = '';
1347
+ let lastSelectionAt = 0;
1348
+ const reportSelection = () => {
1349
+ selectionTimer = null;
1350
+ if (session !== documentSessionId || activeRendition !== rendition) return;
1351
+ const selected = selectionWindow.getSelection();
1352
+ if (!selected || selected.isCollapsed || selected.rangeCount === 0 ||
1353
+ typeof contents.cfiFromRange !== 'function') return;
1354
+ const range = selected.getRangeAt(0).cloneRange();
1355
+ if (!range.toString().trim()) return;
1356
+ const href = spineItems[contents.sectionIndex]?.href;
1357
+ if (!href) return;
1358
+ let cfi;
1359
+ try { cfi = contents.cfiFromRange(range); } catch (_) { return; }
1360
+ const anchor = window.PapyrusEpubAnnotations?.selection(contents, cfi, href, annotationDocumentId);
1361
+ 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;
1367
+ const payload = {documentSessionId: session, text: anchor.quote,
1368
+ pageIndex: contents.sectionIndex + 1, anchor};
1369
+ nativeMenuSelection = {contents, payload};
1370
+ sendEvent('EPUB_TEXT_SELECTED', payload);
1371
+ };
1204
1372
  const handleSelectionChange = () => {
1205
1373
  const selection = selectionWindow.getSelection();
1206
1374
  const text = selection ? selection.toString().trim() : '';
1207
1375
  epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
1376
+ if (selectionTimer !== null) clearTimeout(selectionTimer);
1377
+ // epub.js "selected" is not reliable on every iOS WKWebView path.
1378
+ // The content document still emits selectionchange, so resolve its
1379
+ // live Range to a CFI before the native menu action can fire.
1380
+ if (text) reportSelection();
1208
1381
  };
1209
1382
  selectionDocument.addEventListener('selectionchange', handleSelectionChange);
1210
1383
  const selectionCleanup = () => {
1384
+ if (selectionTimer !== null) clearTimeout(selectionTimer);
1211
1385
  selectionDocument.removeEventListener('selectionchange', handleSelectionChange);
1212
1386
  epubSelectionCleanups.delete(selectionCleanup);
1213
1387
  };
@@ -1228,21 +1402,28 @@
1228
1402
  rendition.hooks.unloaded.register((view) => {
1229
1403
  const cleanup = view && view.contents && view.contents.__papyrusSelectionCleanup;
1230
1404
  if (typeof cleanup === 'function') cleanup();
1405
+ const renderer = epubAnnotationRenderers.get(view?.contents);
1406
+ if (renderer) renderer.destroy();
1407
+ epubAnnotationRenderers.delete(view?.contents);
1231
1408
  });
1232
1409
  }
1233
1410
 
1234
1411
  if (rendition && typeof rendition.on === 'function') {
1412
+ const selectionSession = documentSessionId;
1413
+ const selectionRendition = rendition;
1235
1414
  rendition.on('selected', (cfiRange, contents) => {
1236
- const selection = contents && contents.window ? contents.window.getSelection() : null;
1237
- const text = selection ? selection.toString().trim() : '';
1238
- epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
1239
- if (text) {
1240
- sendEvent('TEXT_SELECTED', { text, pageIndex: Math.max(0, currentPage - 1) });
1241
- }
1242
- if (rendition && rendition.annotations && typeof rendition.annotations.remove === 'function') {
1243
- rendition.annotations.remove(cfiRange, 'highlight');
1244
- }
1415
+ if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
1416
+ const href = spineItems[contents.sectionIndex]?.href;
1417
+ const anchor = href && window.PapyrusEpubAnnotations?.selection(contents,cfiRange,href,annotationDocumentId);
1418
+ if (!anchor) return;
1419
+ epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(true);
1420
+ const payload = {documentSessionId: selectionSession, text: anchor.quote, pageIndex: contents.sectionIndex+1, anchor};
1421
+ nativeMenuSelection = {contents, payload};
1422
+ sendEvent('EPUB_TEXT_SELECTED', payload);
1245
1423
  });
1424
+ ['rendered','resized','layout'].forEach(name => rendition.on(name, () => {
1425
+ if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => renderer.schedule());
1426
+ }));
1246
1427
  rendition.on('relocated', (location) => {
1247
1428
  const start = location && location.start;
1248
1429
  let index = start && Number.isInteger(start.index) ? start.index : -1;
@@ -1372,7 +1553,8 @@
1372
1553
  const end = getTouchPoint(event);
1373
1554
  const start = touchStart;
1374
1555
  touchStart = null;
1375
- if (start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
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) {
1376
1558
  sendEvent('VIEWER_TAP', {});
1377
1559
  }
1378
1560
  epubScrollDiagnostics && epubScrollDiagnostics.touchEnd(event);
@@ -1529,6 +1711,8 @@
1529
1711
  }
1530
1712
  };
1531
1713
 
1714
+ let nativeMenuSelection = null;
1715
+
1532
1716
  const handleCommand = async (message) => {
1533
1717
  const { id, kind, payload } = message;
1534
1718
 
@@ -1536,6 +1720,9 @@
1536
1720
  if (kind === 'load') {
1537
1721
  const generation = (epubLoadGeneration += 1);
1538
1722
  currentType = payload.type;
1723
+ documentSessionId = payload.documentSessionId || null;
1724
+ epubAnnotations = [];
1725
+ annotationDocumentId = undefined;
1539
1726
  zoom = 1.0;
1540
1727
 
1541
1728
  if (currentType === 'text') {
@@ -1576,6 +1763,36 @@
1576
1763
  throw new Error('Unsupported document type');
1577
1764
  }
1578
1765
 
1766
+ if (kind === 'epub-annotations' || kind === 'epub-annotation-navigate') {
1767
+ if (currentType !== 'epub' || !documentSessionId || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB annotation session');
1768
+ if (kind === 'epub-annotations') {
1769
+ const incoming = payload.annotations;
1770
+ if (!Array.isArray(incoming) || incoming.length > 10000 || incoming.some(a => !a || typeof a.id !== 'string' || a.id.length > 256)) throw new Error('Invalid EPUB annotations');
1771
+ const ids = new Set();
1772
+ epubAnnotations = incoming.filter(a => {
1773
+ if (ids.has(a.id) || !window.PapyrusEpubAnnotations.validAnchor(a.anchor)) return false;
1774
+ ids.add(a.id); return true;
1775
+ });
1776
+ annotationDocumentId = typeof payload.documentId === 'string' ? payload.documentId : undefined;
1777
+ annotationLabels = {comment: typeof payload.labels?.comment === 'string' ? payload.labels.comment.slice(0,128) : ''};
1778
+ epubAnnotationRenderers.forEach(renderer => renderer.schedule());
1779
+ } else {
1780
+ const annotation = epubAnnotations.find(a => a.id === payload.annotationId);
1781
+ if (!annotation) throw new Error('Annotation not available');
1782
+ // Navigation uses the CFI, never a synthetic percentage/page rectangle.
1783
+ const navigationSession = documentSessionId;
1784
+ const activeRendition = rendition;
1785
+ // 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));
1792
+ }
1793
+ sendResponse(id,true,null); return;
1794
+ }
1795
+
1579
1796
  if (kind === 'go-to-page') {
1580
1797
  const page = Math.max(1, payload.page || 1);
1581
1798
  if (currentType === 'text') {
@@ -1745,6 +1962,26 @@
1745
1962
  return;
1746
1963
  }
1747
1964
 
1965
+ if (message.type === 'epub-selection-action') {
1966
+ const selected = nativeMenuSelection;
1967
+ if (!selected || message.documentSessionId !== documentSessionId ||
1968
+ selected.payload.documentSessionId !== documentSessionId) return;
1969
+ if (message.key === 'selectAll') {
1970
+ const doc = selected.contents.document;
1971
+ const range = doc.createRange();
1972
+ range.selectNodeContents(doc.body);
1973
+ const selection = selected.contents.window.getSelection();
1974
+ selection.removeAllRanges();
1975
+ selection.addRange(range);
1976
+ return;
1977
+ }
1978
+ if (!['copy','define','highlight','underline','strikeout','comment'].includes(message.key)) return;
1979
+ sendEvent('EPUB_SELECTION_ACTION', {...selected.payload, action: message.key});
1980
+ selected.contents.window.getSelection()?.removeAllRanges();
1981
+ nativeMenuSelection = null;
1982
+ return;
1983
+ }
1984
+
1748
1985
  if (!message.kind) return;
1749
1986
  handleCommand(message);
1750
1987
  };
@@ -17,7 +17,8 @@ const replaceMarkedSource = (content, start, end, replacement) => {
17
17
  };
18
18
 
19
19
  const runtimePath = resolve(runtimeDirectory, "runtime.js");
20
- const runtime = await readFile(runtimePath, "utf8");
20
+ const epubSource = await readFile(resolve(runtimeDirectory,"epubAnnotations.js"),"utf8");
21
+ const runtime = replaceMarkedSource(await readFile(runtimePath, "utf8"), "/* @papyrus-epub-annotations:start */", "/* @papyrus-epub-annotations:end */", `\n${epubSource}\n`);
21
22
  const comicRuntimeEnd = "/* @papyrus-comic-runtime:end */";
22
23
  const runtimeBodyStart = runtime.indexOf("\n(function () {", runtime.indexOf(comicRuntimeEnd));
23
24
  if (runtimeBodyStart < 0) {