@internetarchive/bookreader 5.0.0-7 → 5.0.0-70-a1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/.eslintrc.js +17 -15
  2. package/.github/workflows/node.js.yml +73 -10
  3. package/.github/workflows/npm-publish.yml +6 -20
  4. package/.testcaferc.js +10 -0
  5. package/BookReader/BookReader.css +396 -1129
  6. package/BookReader/BookReader.js +1 -1
  7. package/BookReader/BookReader.js.LICENSE.txt +20 -20
  8. package/BookReader/BookReader.js.map +1 -1
  9. package/BookReader/ia-bookreader-bundle.js +1788 -0
  10. package/BookReader/ia-bookreader-bundle.js.LICENSE.txt +19 -0
  11. package/BookReader/ia-bookreader-bundle.js.map +1 -0
  12. package/BookReader/icons/1up.svg +1 -1
  13. package/BookReader/icons/2up.svg +1 -1
  14. package/BookReader/icons/advance.svg +1 -1
  15. package/BookReader/icons/chevron-right.svg +1 -1
  16. package/BookReader/icons/close-circle-dark.svg +1 -1
  17. package/BookReader/icons/close-circle.svg +1 -1
  18. package/BookReader/icons/fullscreen.svg +1 -1
  19. package/BookReader/icons/fullscreen_exit.svg +1 -1
  20. package/BookReader/icons/hamburger.svg +1 -1
  21. package/BookReader/icons/left-arrow.svg +1 -1
  22. package/BookReader/icons/magnify-minus.svg +1 -1
  23. package/BookReader/icons/magnify-plus.svg +1 -1
  24. package/BookReader/icons/magnify.svg +1 -1
  25. package/BookReader/icons/pause.svg +1 -1
  26. package/BookReader/icons/play.svg +1 -1
  27. package/BookReader/icons/playback-speed.svg +1 -1
  28. package/BookReader/icons/read-aloud.svg +1 -1
  29. package/BookReader/icons/review.svg +1 -1
  30. package/BookReader/icons/thumbnails.svg +1 -1
  31. package/BookReader/icons/voice.svg +1 -0
  32. package/BookReader/icons/volume-full.svg +1 -1
  33. package/BookReader/images/BRicons.svg +3 -3
  34. package/BookReader/images/books_graphic.svg +1 -1
  35. package/BookReader/images/icon_book.svg +1 -1
  36. package/BookReader/images/icon_bookmark.svg +1 -1
  37. package/BookReader/images/icon_gear.svg +1 -1
  38. package/BookReader/images/icon_hamburger.svg +1 -1
  39. package/BookReader/images/icon_home.svg +1 -1
  40. package/BookReader/images/icon_info.svg +1 -1
  41. package/BookReader/images/icon_one_page.svg +1 -1
  42. package/BookReader/images/icon_pause.svg +1 -1
  43. package/BookReader/images/icon_play.svg +1 -1
  44. package/BookReader/images/icon_playback-rate.svg +1 -1
  45. package/BookReader/images/icon_search_button.svg +1 -1
  46. package/BookReader/images/icon_share.svg +1 -1
  47. package/BookReader/images/icon_skip-ahead.svg +1 -1
  48. package/BookReader/images/icon_skip-back.svg +1 -1
  49. package/BookReader/images/icon_speaker.svg +1 -1
  50. package/BookReader/images/icon_speaker_open.svg +1 -1
  51. package/BookReader/images/icon_thumbnails.svg +1 -1
  52. package/BookReader/images/icon_toc.svg +1 -1
  53. package/BookReader/images/icon_two_pages.svg +1 -1
  54. package/BookReader/images/marker_chap-off.svg +1 -1
  55. package/BookReader/images/marker_chap-on.svg +1 -1
  56. package/BookReader/images/marker_srch-on.svg +1 -1
  57. package/BookReader/jquery-3.js +2 -0
  58. package/BookReader/jquery-3.js.LICENSE.txt +24 -0
  59. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  60. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  61. package/BookReader/plugins/plugin.autoplay.js +1 -1
  62. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  63. package/BookReader/plugins/plugin.chapters.js +25 -1
  64. package/BookReader/plugins/plugin.chapters.js.LICENSE.txt +1 -0
  65. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  66. package/BookReader/plugins/plugin.iframe.js +1 -1
  67. package/BookReader/plugins/plugin.iframe.js.map +1 -1
  68. package/BookReader/plugins/plugin.resume.js +1 -1
  69. package/BookReader/plugins/plugin.resume.js.map +1 -1
  70. package/BookReader/plugins/plugin.search.js +2 -1
  71. package/BookReader/plugins/plugin.search.js.LICENSE.txt +1 -0
  72. package/BookReader/plugins/plugin.search.js.map +1 -1
  73. package/BookReader/plugins/plugin.text_selection.js +2 -1
  74. package/BookReader/plugins/plugin.text_selection.js.LICENSE.txt +1 -0
  75. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  76. package/BookReader/plugins/plugin.tts.js +1 -1
  77. package/BookReader/plugins/plugin.tts.js.LICENSE.txt +2 -0
  78. package/BookReader/plugins/plugin.tts.js.map +1 -1
  79. package/BookReader/plugins/plugin.url.js +1 -1
  80. package/BookReader/plugins/plugin.url.js.map +1 -1
  81. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
  82. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  83. package/BookReader/webcomponents-bundle.js +3 -0
  84. package/BookReader/webcomponents-bundle.js.LICENSE.txt +9 -0
  85. package/BookReader/webcomponents-bundle.js.map +1 -0
  86. package/BookReaderDemo/BookReaderDemo.css +16 -19
  87. package/BookReaderDemo/BookReaderJSAdvanced.js +0 -3
  88. package/BookReaderDemo/BookReaderJSAutoplay.js +4 -1
  89. package/BookReaderDemo/BookReaderJSSimple.js +1 -0
  90. package/BookReaderDemo/IADemoBr.js +147 -0
  91. package/BookReaderDemo/demo-advanced.html +2 -2
  92. package/BookReaderDemo/demo-autoplay.html +2 -3
  93. package/BookReaderDemo/demo-embed-iframe-src.html +2 -1
  94. package/BookReaderDemo/demo-fullscreen-mobile.html +3 -5
  95. package/BookReaderDemo/demo-fullscreen.html +2 -4
  96. package/BookReaderDemo/demo-iiif.html +2 -1
  97. package/BookReaderDemo/demo-iiif.js +0 -1
  98. package/BookReaderDemo/demo-internetarchive.html +213 -17
  99. package/BookReaderDemo/demo-multiple.html +2 -1
  100. package/BookReaderDemo/demo-preview-pages.html +2 -1
  101. package/BookReaderDemo/demo-simple.html +2 -1
  102. package/BookReaderDemo/demo-vendor-fullscreen.html +2 -4
  103. package/BookReaderDemo/ia-multiple-volumes-manifest.js +170 -0
  104. package/BookReaderDemo/immersion-1up.html +2 -2
  105. package/BookReaderDemo/immersion-mode.html +2 -4
  106. package/BookReaderDemo/toggle_controls.html +3 -2
  107. package/BookReaderDemo/view_mode.html +2 -1
  108. package/BookReaderDemo/viewmode-cycle.html +2 -3
  109. package/CHANGELOG.md +279 -0
  110. package/README.md +14 -1
  111. package/babel.config.js +20 -0
  112. package/codecov.yml +6 -0
  113. package/index.html +4 -1
  114. package/jsconfig.json +19 -0
  115. package/netlify.toml +9 -0
  116. package/package.json +69 -60
  117. package/renovate.json +52 -0
  118. package/scripts/preversion.js +4 -1
  119. package/src/BookNavigator/assets/bookmark-colors.js +1 -1
  120. package/src/BookNavigator/assets/button-base.js +4 -2
  121. package/src/BookNavigator/assets/ia-logo.js +17 -0
  122. package/src/BookNavigator/assets/icon_checkmark.js +1 -1
  123. package/src/BookNavigator/assets/icon_close.js +1 -1
  124. package/src/BookNavigator/book-navigator.js +586 -0
  125. package/src/BookNavigator/bookmarks/bookmark-button.js +3 -2
  126. package/src/BookNavigator/bookmarks/bookmark-edit.js +3 -4
  127. package/src/BookNavigator/bookmarks/bookmarks-list.js +2 -3
  128. package/src/BookNavigator/bookmarks/bookmarks-loginCTA.js +3 -8
  129. package/src/BookNavigator/bookmarks/bookmarks-provider.js +27 -17
  130. package/src/BookNavigator/bookmarks/ia-bookmarks.js +116 -67
  131. package/src/BookNavigator/delete-modal-actions.js +1 -1
  132. package/src/BookNavigator/downloads/downloads-provider.js +36 -21
  133. package/src/BookNavigator/downloads/downloads.js +41 -25
  134. package/src/BookNavigator/search/search-provider.js +49 -27
  135. package/src/BookNavigator/search/search-results.js +23 -9
  136. package/src/BookNavigator/sharing.js +27 -0
  137. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +11 -10
  138. package/src/BookNavigator/visual-adjustments/visual-adjustments.js +3 -3
  139. package/src/BookNavigator/volumes/volumes-provider.js +95 -0
  140. package/src/BookReader/BookModel.js +64 -34
  141. package/src/BookReader/DragScrollable.js +233 -0
  142. package/src/BookReader/Mode1Up.js +56 -351
  143. package/src/BookReader/Mode1UpLit.js +388 -0
  144. package/src/BookReader/Mode2Up.js +73 -1318
  145. package/src/BookReader/Mode2UpLit.js +776 -0
  146. package/src/BookReader/ModeCoordinateSpace.js +29 -0
  147. package/src/BookReader/ModeSmoothZoom.js +312 -0
  148. package/src/BookReader/ModeThumb.js +18 -12
  149. package/src/BookReader/Navbar/Navbar.js +12 -38
  150. package/src/BookReader/PageContainer.js +81 -6
  151. package/src/BookReader/ReduceSet.js +1 -1
  152. package/src/BookReader/Toolbar/Toolbar.js +10 -37
  153. package/src/BookReader/events.js +2 -3
  154. package/src/BookReader/options.js +24 -2
  155. package/src/BookReader/utils/HTMLDimensionsCacher.js +44 -0
  156. package/src/BookReader/utils/ScrollClassAdder.js +31 -0
  157. package/src/BookReader/utils/SelectionObserver.js +43 -0
  158. package/src/BookReader/utils.js +118 -13
  159. package/src/BookReader.js +423 -1056
  160. package/src/assets/icons/magnify-minus.svg +3 -7
  161. package/src/assets/icons/magnify-plus.svg +3 -7
  162. package/src/assets/icons/voice.svg +1 -0
  163. package/src/css/BookReader.scss +1 -5
  164. package/src/css/_BRBookmarks.scss +1 -1
  165. package/src/css/_BRComponent.scss +1 -1
  166. package/src/css/_BRmain.scss +16 -0
  167. package/src/css/_BRnav.scss +11 -38
  168. package/src/css/_BRpages.scss +149 -40
  169. package/src/css/_BRsearch.scss +67 -21
  170. package/src/css/_TextSelection.scss +87 -27
  171. package/src/css/_colorbox.scss +2 -2
  172. package/src/css/_controls.scss +20 -7
  173. package/src/css/_icons.scss +1 -1
  174. package/src/ia-bookreader/ia-bookreader.js +224 -0
  175. package/src/plugins/plugin.archive_analytics.js +3 -3
  176. package/src/plugins/plugin.autoplay.js +5 -11
  177. package/src/plugins/plugin.chapters.js +211 -186
  178. package/src/plugins/plugin.resume.js +3 -3
  179. package/src/plugins/plugin.text_selection.js +464 -134
  180. package/src/plugins/plugin.vendor-fullscreen.js +4 -4
  181. package/src/plugins/search/plugin.search.js +142 -125
  182. package/src/plugins/search/utils.js +43 -0
  183. package/src/plugins/search/view.js +33 -58
  184. package/src/plugins/tts/AbstractTTSEngine.js +46 -37
  185. package/src/plugins/tts/FestivalTTSEngine.js +13 -14
  186. package/src/plugins/tts/PageChunk.js +15 -21
  187. package/src/plugins/tts/PageChunkIterator.js +8 -12
  188. package/src/plugins/tts/WebTTSEngine.js +87 -71
  189. package/src/plugins/tts/plugin.tts.js +95 -126
  190. package/src/plugins/tts/utils.js +0 -25
  191. package/src/plugins/url/UrlPlugin.js +193 -0
  192. package/src/plugins/{plugin.url.js → url/plugin.url.js} +45 -16
  193. package/src/util/browserSniffing.js +22 -0
  194. package/src/util/docCookies.js +21 -2
  195. package/tests/e2e/README.md +37 -0
  196. package/tests/e2e/autoplay.test.js +2 -2
  197. package/tests/e2e/base.test.js +8 -16
  198. package/tests/e2e/helpers/base.js +53 -48
  199. package/tests/e2e/helpers/debug.js +1 -1
  200. package/tests/e2e/helpers/params.js +17 -0
  201. package/tests/e2e/helpers/rightToLeft.js +8 -14
  202. package/tests/e2e/helpers/search.js +73 -0
  203. package/tests/e2e/models/Navigation.js +20 -37
  204. package/tests/e2e/rightToLeft.test.js +4 -5
  205. package/tests/e2e/viewmode.test.js +40 -33
  206. package/tests/jest/BookNavigator/book-navigator.test.js +658 -0
  207. package/tests/jest/BookNavigator/bookmarks/bookmark-button.test.js +43 -0
  208. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmark-edit.test.js +25 -26
  209. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmarks-list.test.js +41 -42
  210. package/tests/jest/BookNavigator/bookmarks/ia-bookmarks.test.js +45 -0
  211. package/tests/jest/BookNavigator/downloads/downloads-provider.test.js +67 -0
  212. package/tests/jest/BookNavigator/downloads/downloads.test.js +53 -0
  213. package/tests/jest/BookNavigator/search/search-provider.test.js +167 -0
  214. package/tests/{karma → jest}/BookNavigator/search/search-results.test.js +109 -60
  215. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +49 -0
  216. package/tests/jest/BookNavigator/visual-adjustments.test.js +200 -0
  217. package/tests/jest/BookNavigator/volumes/volumes-provider.test.js +80 -0
  218. package/tests/{BookReader → jest/BookReader}/BookModel.test.js +74 -14
  219. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +193 -0
  220. package/tests/{BookReader → jest/BookReader}/ImageCache.test.js +4 -4
  221. package/tests/jest/BookReader/Mode1UpLit.test.js +73 -0
  222. package/tests/jest/BookReader/Mode2Up.test.js +98 -0
  223. package/tests/jest/BookReader/Mode2UpLit.test.js +190 -0
  224. package/tests/jest/BookReader/ModeCoordinateSpace.test.js +16 -0
  225. package/tests/jest/BookReader/ModeSmoothZoom.test.js +218 -0
  226. package/tests/jest/BookReader/ModeThumb.test.js +71 -0
  227. package/tests/{BookReader → jest/BookReader}/Navbar/Navbar.test.js +10 -10
  228. package/tests/{BookReader → jest/BookReader}/PageContainer.test.js +88 -6
  229. package/tests/{BookReader → jest/BookReader}/ReduceSet.test.js +1 -1
  230. package/tests/{BookReader → jest/BookReader}/Toolbar/Toolbar.test.js +2 -2
  231. package/tests/jest/BookReader/utils/HTMLDimensionsCacher.test.js +59 -0
  232. package/tests/jest/BookReader/utils/ScrollClassAdder.test.js +49 -0
  233. package/tests/jest/BookReader/utils/SelectionObserver.test.js +43 -0
  234. package/tests/{BookReader → jest/BookReader}/utils/classes.test.js +1 -1
  235. package/tests/jest/BookReader/utils.test.js +229 -0
  236. package/tests/jest/BookReader.keyboard.test.js +190 -0
  237. package/tests/{BookReader.options.test.js → jest/BookReader.options.test.js} +9 -1
  238. package/tests/{BookReader.test.js → jest/BookReader.test.js} +26 -37
  239. package/tests/{plugins → jest/plugins}/plugin.archive_analytics.test.js +2 -2
  240. package/tests/{plugins → jest/plugins}/plugin.autoplay.test.js +4 -4
  241. package/tests/jest/plugins/plugin.chapters.test.js +145 -0
  242. package/tests/{plugins → jest/plugins}/plugin.iframe.test.js +2 -2
  243. package/tests/{plugins → jest/plugins}/plugin.resume.test.js +3 -3
  244. package/tests/jest/plugins/plugin.text_selection.test.js +317 -0
  245. package/tests/{plugins → jest/plugins}/plugin.vendor-fullscreen.test.js +2 -2
  246. package/tests/{plugins → jest/plugins}/search/plugin.search.test.js +25 -47
  247. package/tests/{plugins → jest/plugins}/search/plugin.search.view.test.js +39 -6
  248. package/tests/jest/plugins/search/utils.js +25 -0
  249. package/tests/jest/plugins/search/utils.test.js +29 -0
  250. package/tests/{plugins → jest/plugins}/tts/AbstractTTSEngine.test.js +9 -9
  251. package/tests/{plugins → jest/plugins}/tts/FestivalTTSEngine.test.js +4 -4
  252. package/tests/{plugins → jest/plugins}/tts/PageChunk.test.js +1 -1
  253. package/tests/{plugins → jest/plugins}/tts/PageChunkIterator.test.js +3 -3
  254. package/tests/{plugins → jest/plugins}/tts/WebTTSEngine.test.js +47 -1
  255. package/tests/{plugins → jest/plugins}/tts/utils.test.js +1 -60
  256. package/tests/jest/plugins/url/UrlPlugin.test.js +190 -0
  257. package/tests/{plugins → jest/plugins/url}/plugin.url.test.js +53 -14
  258. package/tests/jest/setup.js +3 -0
  259. package/tests/{util → jest/util}/browserSniffing.test.js +1 -1
  260. package/tests/jest/util/docCookies.test.js +24 -0
  261. package/tests/{util → jest/util}/strings.test.js +1 -1
  262. package/tests/{utils.js → jest/utils.js} +38 -0
  263. package/webpack.config.js +11 -6
  264. package/.babelrc +0 -12
  265. package/.dependabot/config.yml +0 -6
  266. package/.testcaferc.json +0 -5
  267. package/BookReader/bookreader-component-bundle.js +0 -1450
  268. package/BookReader/bookreader-component-bundle.js.LICENSE.txt +0 -38
  269. package/BookReader/bookreader-component-bundle.js.map +0 -1
  270. package/BookReader/jquery-1.10.1.js +0 -2
  271. package/BookReader/jquery-1.10.1.js.LICENSE.txt +0 -24
  272. package/BookReader/plugins/plugin.menu_toggle.js +0 -2
  273. package/BookReader/plugins/plugin.menu_toggle.js.map +0 -1
  274. package/BookReader/plugins/plugin.mobile_nav.js +0 -2
  275. package/BookReader/plugins/plugin.mobile_nav.js.map +0 -1
  276. package/BookReaderDemo/bookreader-template-bundle.js +0 -7178
  277. package/BookReaderDemo/demo-plugin-menu-toggle.html +0 -34
  278. package/karma.conf.js +0 -23
  279. package/src/BookNavigator/BookModel.js +0 -14
  280. package/src/BookNavigator/BookNavigator.js +0 -446
  281. package/src/BookNavigator/assets/book-loader.js +0 -27
  282. package/src/BookNavigator/br-fullscreen-mgr.js +0 -83
  283. package/src/BookNavigator/search/a-search-result.js +0 -55
  284. package/src/BookReader/DebugConsole.js +0 -54
  285. package/src/BookReaderComponent/BookReaderComponent.js +0 -112
  286. package/src/ItemNavigator/ItemNavigator.js +0 -376
  287. package/src/ItemNavigator/providers/sharing.js +0 -29
  288. package/src/css/_MobileNav.scss +0 -194
  289. package/src/dragscrollable-br.js +0 -261
  290. package/src/plugins/menu_toggle/plugin.menu_toggle.js +0 -324
  291. package/src/plugins/plugin.mobile_nav.js +0 -287
  292. package/tests/BookReader/BookReaderPublicFunctions.test.js +0 -171
  293. package/tests/BookReader/DebugConsole.test.js +0 -25
  294. package/tests/BookReader/Mode1Up.test.js +0 -164
  295. package/tests/BookReader/Mode2Up.test.js +0 -247
  296. package/tests/BookReader/utils.test.js +0 -109
  297. package/tests/e2e/helpers/desktopSearch.js +0 -72
  298. package/tests/e2e/helpers/mobileSearch.js +0 -85
  299. package/tests/e2e/ia-production/ia-prod-base.js +0 -17
  300. package/tests/karma/BookNavigator/book-navigator.test.js +0 -132
  301. package/tests/karma/BookNavigator/search/search-provider.test.js +0 -23
  302. package/tests/karma/BookNavigator/visual-adjustments.test.js +0 -201
  303. package/tests/plugins/menu_toggle/plugin.menu_toggle.test.js +0 -68
  304. package/tests/plugins/plugin.chapters.test.js +0 -130
  305. package/tests/plugins/plugin.mobile_nav.test.js +0 -66
  306. package/tests/plugins/plugin.text_selection.test.js +0 -203
  307. package/tests/util/docCookies.test.js +0 -15
@@ -1,7 +1,9 @@
1
1
  //@ts-check
2
- import { isFirefox, isSafari } from '../util/browserSniffing.js';
2
+ import { createDIVPageLayer } from '../BookReader/PageContainer.js';
3
+ import { SelectionObserver } from '../BookReader/utils/SelectionObserver.js';
3
4
  import { applyVariables } from '../util/strings.js';
4
5
  /** @typedef {import('../util/strings.js').StringWithVars} StringWithVars */
6
+ /** @typedef {import('../BookReader/PageContainer.js').PageContainer} PageContainer */
5
7
 
6
8
  const BookReader = /** @type {typeof import('../BookReader').default} */(window.BookReader);
7
9
 
@@ -11,6 +13,8 @@ export const DEFAULT_OPTIONS = {
11
13
  fullDjvuXmlUrl: null,
12
14
  /** @type {StringWithVars} The URL to fetch a single page of the DJVU xml. Supports options.vars. Also has {{pageIndex}} */
13
15
  singlePageDjvuXmlUrl: null,
16
+ /** Whether to fetch the XML as a jsonp */
17
+ jsonp: false,
14
18
  };
15
19
  /** @typedef {typeof DEFAULT_OPTIONS} TextSelectionPluginOptions */
16
20
 
@@ -36,25 +40,18 @@ export class Cache {
36
40
  }
37
41
 
38
42
  export class TextSelectionPlugin {
39
-
40
- constructor(options = DEFAULT_OPTIONS, optionVariables, avoidTspans = isFirefox(), pointerEventsOnParagraph = isSafari()) {
43
+ /**
44
+ * @param {'lr' | 'rl'} pageProgression In the future this should be in the ocr file
45
+ * since a book being right to left doesn't mean the ocr is right to left. But for
46
+ * now we do make that assumption.
47
+ */
48
+ constructor(options = DEFAULT_OPTIONS, optionVariables, pageProgression = 'lr') {
41
49
  this.options = options;
42
50
  this.optionVariables = optionVariables;
43
51
  /**@type {PromiseLike<JQuery<HTMLElement>|undefined>} */
44
52
  this.djvuPagesPromise = null;
45
- // Using text elements instead of tspans for words because Firefox does not allow svg tspan stretch.
46
- // Tspans are necessary on Chrome because they prevent newline character after every word when copying
47
- this.svgParagraphElement = "text";
48
- this.svgWordElement = "tspan";
49
- this.insertNewlines = avoidTspans
50
- // Safari has a bug where `pointer-events` doesn't work on `<tspans>`. So
51
- // there we will set `pointer-events: all` on the paragraph element. We don't
52
- // do this everywhere, because it's a worse experience. Thanks Safari :/
53
- this.pointerEventsOnParagraph = pointerEventsOnParagraph;
54
- if (avoidTspans) {
55
- this.svgParagraphElement = "g";
56
- this.svgWordElement = "text";
57
- }
53
+ /** Whether the book is right-to-left */
54
+ this.rtl = pageProgression === 'rl';
58
55
 
59
56
  /** @type {Cache<{index: number, response: any}>} */
60
57
  this.pageTextCache = new Cache();
@@ -64,15 +61,34 @@ export class TextSelectionPlugin {
64
61
  * unusable. For now don't render text layer for pages with too many words.
65
62
  */
66
63
  this.maxWordRendered = 2500;
64
+
65
+ this.selectionObserver = new SelectionObserver('.BRtextLayer', this._onSelectionChange);
66
+ }
67
+
68
+ /**
69
+ * @param {'started' | 'cleared'} type
70
+ * @param {HTMLElement} target
71
+ */
72
+ _onSelectionChange = (type, target) => {
73
+ if (type === 'started') {
74
+ this.textSelectingMode(target);
75
+ } else if (type === 'cleared') {
76
+ this.defaultMode(target);
77
+ } else {
78
+ throw new Error(`Unknown type ${type}`);
79
+ }
67
80
  }
68
81
 
69
82
  init() {
83
+ this.selectionObserver.attach();
84
+
70
85
  // Only fetch the full djvu xml if the single page url isn't there
71
86
  if (this.options.singlePageDjvuXmlUrl) return;
72
87
  this.djvuPagesPromise = $.ajax({
73
88
  type: "GET",
74
89
  url: applyVariables(this.options.fullDjvuXmlUrl, this.optionVariables),
75
- dataType: "html",
90
+ dataType: this.options.jsonp ? "jsonp" : "html",
91
+ cache: true,
76
92
  error: (e) => undefined
77
93
  }).then((res) => {
78
94
  try {
@@ -94,21 +110,21 @@ export class TextSelectionPlugin {
94
110
  if (cachedEntry) {
95
111
  return cachedEntry.response;
96
112
  }
97
- return $.ajax({
113
+ const res = await $.ajax({
98
114
  type: "GET",
99
115
  url: applyVariables(this.options.singlePageDjvuXmlUrl, this.optionVariables, { pageIndex: index }),
100
- dataType: "html",
116
+ dataType: this.options.jsonp ? "jsonp" : "html",
117
+ cache: true,
101
118
  error: (e) => undefined,
102
- }).then((res) => {
103
- try {
104
- const xmlDoc = $.parseXML(res);
105
- const result = xmlDoc && $(xmlDoc).find("OBJECT")[0];
106
- this.pageTextCache.add({ index, response: result });
107
- return result;
108
- } catch (e) {
109
- return undefined;
110
- }
111
119
  });
120
+ try {
121
+ const xmlDoc = $.parseXML(res);
122
+ const result = xmlDoc && $(xmlDoc).find("OBJECT")[0];
123
+ this.pageTextCache.add({ index, response: result });
124
+ return result;
125
+ } catch (e) {
126
+ return undefined;
127
+ }
112
128
  } else {
113
129
  const XMLpagesArr = await this.djvuPagesPromise;
114
130
  if (XMLpagesArr) return XMLpagesArr[index];
@@ -129,67 +145,87 @@ export class TextSelectionPlugin {
129
145
 
130
146
  /**
131
147
  * Applies mouse events when in default mode
132
- * @param {SVGElement} svg
148
+ * @param {HTMLElement} textLayer
133
149
  */
134
- defaultMode(svg) {
135
- svg.classList.remove("selectingSVG");
136
- $(svg).on("mousedown.textSelectPluginHandler", (event) => {
137
- if (!$(event.target).is(".BRwordElement")) return;
138
- event.stopPropagation();
139
- svg.classList.add("selectingSVG");
140
- $(svg).one("mouseup.textSelectPluginHandler", (event) => {
141
- if (window.getSelection().toString() != "") {
142
- event.stopPropagation();
143
- $(svg).off(".textSelectPluginHandler");
144
- this.textSelectingMode(svg);
145
- }
146
- else svg.classList.remove("selectingSVG");
147
- })
148
- })
150
+ defaultMode(textLayer) {
151
+ const $pageContainer = $(textLayer).closest('.BRpagecontainer');
152
+ textLayer.style.pointerEvents = "none";
153
+ $pageContainer.find("img").css("pointer-events", "auto");
154
+
155
+ $(textLayer).off(".textSelectPluginHandler");
156
+ const startedMouseDown = this.mouseIsDown;
157
+ let skipNextMouseup = this.mouseIsDown;
158
+ if (startedMouseDown) {
159
+ textLayer.style.pointerEvents = "auto";
160
+ }
161
+
162
+ // Need to stop propagation to prevent DragScrollable from
163
+ // blocking selection
164
+ $(textLayer).on("mousedown.textSelectPluginHandler", (event) => {
165
+ this.mouseIsDown = true;
166
+ if ($(event.target).is(".BRwordElement, .BRspace")) {
167
+ event.stopPropagation();
168
+ }
169
+ });
170
+
171
+ $(textLayer).on("mouseup.textSelectPluginHandler", (event) => {
172
+ this.mouseIsDown = false;
173
+ textLayer.style.pointerEvents = "none";
174
+ if (skipNextMouseup) {
175
+ skipNextMouseup = false;
176
+ event.stopPropagation();
177
+ }
178
+ });
149
179
  }
150
180
 
151
181
  /**
152
- * Applies mouse events when in textSelecting mode
153
- * @param {SVGElement} svg
182
+ * This mode is active while there is a selection on the given textLayer
183
+ * @param {HTMLElement} textLayer
154
184
  */
155
- textSelectingMode(svg) {
156
- $(svg).on('mousedown.textSelectPluginHandler', (event) => {
157
- if (!$(event.target).is(".BRwordElement")) {
158
- if (window.getSelection().toString() != "") window.getSelection().removeAllRanges();
159
- }
185
+ textSelectingMode(textLayer) {
186
+ const $pageContainer = $(textLayer).closest('.BRpagecontainer');
187
+ // Make text layer consume all events
188
+ textLayer.style.pointerEvents = "all";
189
+ // Block img from getting long-press to save while selecting
190
+ $pageContainer.find("img").css("pointer-events", "none");
191
+
192
+ $(textLayer).off(".textSelectPluginHandler");
193
+
194
+ $(textLayer).on('mousedown.textSelectPluginHandler', (event) => {
195
+ this.mouseIsDown = true;
160
196
  event.stopPropagation();
161
- })
162
- $(svg).on('mouseup.textSelectPluginHandler', (event) => {
197
+ });
198
+
199
+ // Prevent page flip on click
200
+ $(textLayer).on('mouseup.textSelectPluginHandler', (event) => {
201
+ this.mouseIsDown = false;
163
202
  event.stopPropagation();
164
- if (window.getSelection().toString() == "") {
165
- $(svg).off(".textSelectPluginHandler");
166
- this.defaultMode(svg); }
167
- })
203
+ });
168
204
  }
169
205
 
170
206
  /**
171
- * Initializes text selection modes if there is an svg on the page
207
+ * Initializes text selection modes if there is a text layer on the page
172
208
  * @param {JQuery} $container
173
209
  */
174
210
  stopPageFlip($container) {
175
- /** @type {JQuery<SVGElement>} */
176
- const $svg = $container.find('svg.textSelectionSVG');
177
- if (!$svg.length) return;
178
- $svg.each((i, s) => this.defaultMode(s))
211
+ /** @type {JQuery<HTMLElement>} */
212
+ const $textLayer = $container.find('.BRtextLayer');
213
+ if (!$textLayer.length) return;
214
+ $textLayer.each((i, s) => this.defaultMode(s));
179
215
  this.interceptCopy($container);
180
216
  }
181
217
 
182
218
  /**
183
- * @param {number} pageIndex
184
- * @param {JQuery} $container
219
+ * @param {PageContainer} pageContainer
185
220
  */
186
- async createTextLayer(pageIndex, $container) {
187
- const $svgLayers = $container.find('.textSelectionSVG');
188
- if ($svgLayers.length) return;
221
+ async createTextLayer(pageContainer) {
222
+ const pageIndex = pageContainer.page.index;
223
+ const $container = pageContainer.$container;
224
+ const $textLayers = $container.find('.BRtextLayer');
225
+ if ($textLayers.length) return;
189
226
  const XMLpage = await this.getPageText(pageIndex);
190
227
  if (!XMLpage) return;
191
- const XMLwidth = $(XMLpage).attr("width");
192
- const XMLheight = $(XMLpage).attr("height");
228
+ recursivelyAddCoords(XMLpage);
193
229
 
194
230
  const totalWords = $(XMLpage).find("WORD").length;
195
231
  if (totalWords > this.maxWordRendered) {
@@ -197,77 +233,174 @@ export class TextSelectionPlugin {
197
233
  return;
198
234
  }
199
235
 
200
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
201
- svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
202
- svg.setAttribute("viewBox", `0 0 ${XMLwidth} ${XMLheight}`);
203
- $container.append(svg);
204
- svg.setAttribute('class', 'textSelectionSVG');
205
- svg.setAttribute('preserveAspectRatio', 'none');
206
- $(svg).css({
207
- "width": "100%",
208
- "position": "absolute",
209
- "height": "100%",
210
- "top": "0",
211
- "left": "0",
236
+ const textLayer = createDIVPageLayer(pageContainer.page, 'BRtextLayer');
237
+ const ratioW = parseFloat(pageContainer.$container[0].style.width) / pageContainer.page.width;
238
+ const ratioH = parseFloat(pageContainer.$container[0].style.height) / pageContainer.page.height;
239
+ textLayer.style.transform = `scale(${ratioW}, ${ratioH})`;
240
+ textLayer.setAttribute("dir", this.rtl ? "rtl" : "ltr");
241
+
242
+ const ocrParagraphs = $(XMLpage).find("PARAGRAPH[coords]").toArray();
243
+ const paragEls = ocrParagraphs.map(p => {
244
+ const el = this.renderParagraph(p);
245
+ textLayer.appendChild(el);
246
+ return el;
212
247
  });
213
248
 
214
- $(XMLpage).find("PARAGRAPH").each((i, paragraph) => {
215
- // Adding text element for each paragraph in the page
216
- const words = $(paragraph).find("WORD");
217
- if (!words.length) return;
218
- const paragSvg = document.createElementNS("http://www.w3.org/2000/svg", this.svgParagraphElement);
219
- paragSvg.setAttribute("class", "BRparagElement");
220
- if (this.pointerEventsOnParagraph) {
221
- paragSvg.style.pointerEvents = "all";
222
- }
249
+ // Fix up paragraph positions
250
+ const paragraphRects = determineRealRects(textLayer, '.BRparagraphElement');
251
+ let yAdded = 0;
252
+ for (const [ocrParagraph, paragEl] of zip(ocrParagraphs, paragEls)) {
253
+ const ocrParagBounds = $(ocrParagraph).attr("coords").split(",").map(parseFloat);
254
+ const realRect = paragraphRects.get(paragEl);
255
+ const [ocrLeft, , ocrRight, ocrTop] = ocrParagBounds;
256
+ const newStartMargin = this.rtl ? (realRect.right - ocrRight) : (ocrLeft - realRect.left);
257
+ const newTop = ocrTop - (realRect.top + yAdded);
223
258
 
224
- const wordHeightArr = [];
259
+ paragEl.style[this.rtl ? 'marginRight' : 'marginLeft'] = `${newStartMargin}px`;
260
+ paragEl.style.marginTop = `${newTop}px`;
261
+ yAdded += newTop;
262
+ textLayer.appendChild(paragEl);
263
+ }
264
+ $container.append(textLayer);
265
+ this.stopPageFlip($container);
266
+ }
225
267
 
226
- for (let i = 0; i < words.length; i++) {
227
- // Adding tspan for each word in paragraph
228
- const currWord = words[i];
229
- // eslint-disable-next-line no-unused-vars
230
- const [left, bottom, right, top] = $(currWord).attr("coords").split(',').map(parseFloat);
268
+ /**
269
+ * @param {HTMLElement} ocrParagraph
270
+ * @returns {HTMLParagraphElement}
271
+ */
272
+ renderParagraph(ocrParagraph) {
273
+ const paragEl = document.createElement('p');
274
+ paragEl.classList.add('BRparagraphElement');
275
+ const [paragLeft, paragBottom, paragRight, paragTop] = $(ocrParagraph).attr("coords").split(",").map(parseFloat);
276
+ const wordHeightArr = [];
277
+ const lines = $(ocrParagraph).find("LINE[coords]").toArray();
278
+ if (!lines.length) return paragEl;
279
+
280
+
281
+ for (const [prevLine, line, nextLine] of lookAroundWindow(genMap(lines, augmentLine))) {
282
+ const isLastLineOfParagraph = line.ocrElement == lines[lines.length - 1];
283
+ const lineEl = document.createElement('span');
284
+ lineEl.classList.add('BRlineElement');
285
+
286
+ for (const [wordIndex, currWord] of line.words.entries()) {
287
+ const [, bottom, right, top] = $(currWord).attr("coords").split(',').map(parseFloat);
231
288
  const wordHeight = bottom - top;
232
289
  wordHeightArr.push(wordHeight);
233
290
 
234
- const wordTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
235
- wordTspan.setAttribute("class", "BRwordElement");
236
- wordTspan.setAttribute("x", left.toString());
237
- wordTspan.setAttribute("y", bottom.toString());
238
- wordTspan.setAttribute("textLength", (right - left).toString());
239
- wordTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
240
- wordTspan.textContent = currWord.textContent;
241
- paragSvg.appendChild(wordTspan);
242
-
243
- // Adding spaces after words except at the end of the paragraph
244
- // TODO: assumes left-to-right text
245
- if (i < words.length - 1) {
246
- const nextWord = words[i + 1];
247
- // eslint-disable-next-line no-unused-vars
248
- const [leftNext, bottomNext, rightNext, topNext] = $(nextWord).attr("coords").split(',').map(parseFloat);
249
- const spaceTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
250
- spaceTspan.setAttribute("class", "BRwordElement");
251
- spaceTspan.setAttribute("x", right.toString());
252
- spaceTspan.setAttribute("y", bottom.toString());
253
- if ((leftNext - right) > 0) spaceTspan.setAttribute("textLength", (leftNext - right).toString());
254
- spaceTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
255
- spaceTspan.textContent = " ";
256
- paragSvg.appendChild(spaceTspan);
291
+ if (wordIndex == 0 && prevLine?.lastWord.textContent.trim().endsWith('-')) {
292
+ // ideally prefer the next line to determine the left position,
293
+ // since the previous line could be the first line of the paragraph
294
+ // and hence have an incorrectly indented first word.
295
+ // E.g. https://archive.org/details/driitaleofdaring00bachuoft/page/360/mode/2up
296
+ const [newLeft, , , ] = $((nextLine || prevLine).firstWord).attr("coords").split(',').map(parseFloat);
297
+ $(currWord).attr("coords", `${newLeft},${bottom},${right},${top}`);
257
298
  }
258
299
 
259
- // Adds newline at the end of paragraph on Firefox
260
- if ((i == words.length - 1 && (this.insertNewlines))) {
261
- paragSvg.appendChild(document.createTextNode("\n"));
300
+ const wordEl = document.createElement('span');
301
+ wordEl.setAttribute("class", "BRwordElement");
302
+ wordEl.textContent = currWord.textContent.trim();
303
+
304
+ if (wordIndex > 0) {
305
+ const space = document.createElement('span');
306
+ space.classList.add('BRspace');
307
+ space.textContent = ' ';
308
+ lineEl.append(space);
309
+
310
+ // Edge ignores empty elements (like BRspace), so add another
311
+ // space to ensure Edge's ReadAloud works correctly.
312
+ lineEl.appendChild(document.createTextNode(' '));
262
313
  }
314
+
315
+ lineEl.appendChild(wordEl);
263
316
  }
264
317
 
265
- wordHeightArr.sort();
266
- const paragWordHeight = wordHeightArr[Math.floor(wordHeightArr.length * 0.85)];
267
- paragSvg.setAttribute("font-size", paragWordHeight.toString());
268
- svg.appendChild(paragSvg);
269
- })
270
- this.stopPageFlip($container);
318
+ const hasHyphen = line.lastWord.textContent.trim().endsWith('-');
319
+ const lastWordEl = lineEl.children[lineEl.children.length - 1];
320
+ if (hasHyphen && !isLastLineOfParagraph) {
321
+ lastWordEl.textContent = lastWordEl.textContent.trim().slice(0, -1);
322
+ lastWordEl.classList.add('BRwordElement--hyphen');
323
+ }
324
+
325
+ paragEl.appendChild(lineEl);
326
+ if (!isLastLineOfParagraph && !hasHyphen) {
327
+ // Edge does not correctly have spaces between the lines.
328
+ paragEl.appendChild(document.createTextNode(' '));
329
+ }
330
+ }
331
+
332
+ wordHeightArr.sort((a, b) => a - b);
333
+ const paragWordHeight = wordHeightArr[Math.floor(wordHeightArr.length * 0.85)] + 4;
334
+ paragEl.style.left = `${paragLeft}px`;
335
+ paragEl.style.top = `${paragTop}px`;
336
+ paragEl.style.width = `${paragRight - paragLeft}px`;
337
+ paragEl.style.height = `${paragBottom - paragTop}px`;
338
+ paragEl.style.fontSize = `${paragWordHeight}px`;
339
+
340
+ // Fix up sizes - stretch/crush words as necessary using letter spacing
341
+ let wordRects = determineRealRects(paragEl, '.BRwordElement');
342
+ const ocrWords = $(ocrParagraph).find("WORD").toArray();
343
+ const wordEls = paragEl.querySelectorAll('.BRwordElement');
344
+ for (const [ocrWord, wordEl] of zip(ocrWords, wordEls)) {
345
+ const realRect = wordRects.get(wordEl);
346
+ const [left, , right ] = $(ocrWord).attr("coords").split(',').map(parseFloat);
347
+ let ocrWidth = right - left;
348
+ // Some books (eg theworksofplato01platiala) have a space _inside_ the <WORD>
349
+ // element. That makes it impossible to determine the correct positining
350
+ // of everything, but to avoid the BRspace's being width 0, which makes selection
351
+ // janky on Chrome Android, assume the space is the same width as one of the
352
+ // letters.
353
+ if (ocrWord.textContent.endsWith(' ')) {
354
+ ocrWidth = ocrWidth * (ocrWord.textContent.length - 1) / ocrWord.textContent.length;
355
+ }
356
+ const diff = ocrWidth - realRect.width;
357
+ wordEl.style.letterSpacing = `${diff / (ocrWord.textContent.length - 1)}px`;
358
+ }
359
+
360
+ // Stretch/crush lines as necessary using line spacing
361
+ // Recompute rects after letter spacing
362
+ wordRects = determineRealRects(paragEl, '.BRwordElement');
363
+ const spaceRects = determineRealRects(paragEl, '.BRspace');
364
+
365
+ const ocrLines = $(ocrParagraph).find("LINE[coords]").toArray();
366
+ const lineEls = Array.from(paragEl.querySelectorAll('.BRlineElement'));
367
+
368
+ let ySoFar = paragTop;
369
+ for (const [ocrLine, lineEl] of zip(ocrLines, lineEls)) {
370
+ // shift words using marginLeft to align with the correct x position
371
+ const words = $(ocrLine).find("WORD").toArray();
372
+ // const ocrLineLeft = Math.min(...words.map(w => parseFloat($(w).attr("coords").split(',')[0])));
373
+ let xSoFar = this.rtl ? paragRight : paragLeft;
374
+ for (const [ocrWord, wordEl] of zip(words, lineEl.querySelectorAll('.BRwordElement'))) {
375
+ // start of line, need to compute the offset relative to the OCR words
376
+ const wordRect = wordRects.get(wordEl);
377
+ const [ocrLeft, , ocrRight ] = $(ocrWord).attr("coords").split(',').map(parseFloat);
378
+ const diff = (this.rtl ? -(ocrRight - xSoFar) : ocrLeft - xSoFar);
379
+
380
+ if (wordEl.previousElementSibling) {
381
+ const space = wordEl.previousElementSibling;
382
+ space.style.letterSpacing = `${diff - spaceRects.get(space).width}px`;
383
+ } else {
384
+ wordEl.style[this.rtl ? 'paddingRight' : 'paddingLeft'] = `${diff}px`;
385
+ }
386
+ if (this.rtl) xSoFar -= diff + wordRect.width;
387
+ else xSoFar += diff + wordRect.width;
388
+ }
389
+ // And also fix y position
390
+ const ocrLineTop = Math.min(...words.map(w => parseFloat($(w).attr("coords").split(',')[3])));
391
+ const diff = ocrLineTop - ySoFar;
392
+ if (lineEl.previousElementSibling) {
393
+ lineEl.previousElementSibling.style.lineHeight = `${diff}px`;
394
+ ySoFar += diff;
395
+ }
396
+ }
397
+
398
+ // The last line will have a line height subtracting from the paragraph height
399
+ lineEls[lineEls.length - 1].style.lineHeight = `${paragBottom - ySoFar}px`;
400
+
401
+ // Edge does not include a newline for some reason when copying/pasting the <p> els
402
+ paragEl.appendChild(document.createElement('br'));
403
+ return paragEl;
271
404
  }
272
405
  }
273
406
 
@@ -275,12 +408,24 @@ export class BookreaderWithTextSelection extends BookReader {
275
408
  init() {
276
409
  const options = Object.assign({}, DEFAULT_OPTIONS, this.options.plugins.textSelection);
277
410
  if (options.enabled) {
278
- this.textSelectionPlugin = new TextSelectionPlugin(options, this.options.vars);
411
+ this.textSelectionPlugin = new TextSelectionPlugin(options, this.options.vars, this.pageProgression);
279
412
  // Write this back; this way the plugin is the source of truth, and BR just
280
413
  // contains a reference to it.
281
414
  this.options.plugins.textSelection = options;
282
415
  this.textSelectionPlugin.init();
416
+
417
+ new SelectionObserver('.BRtextLayer', (selectEvent) => {
418
+ // Track how often selection is used
419
+ if (selectEvent == 'started') {
420
+ this.archiveAnalyticsSendEvent?.('BookReader', 'SelectStart');
421
+
422
+ // Set a class on the page to avoid hiding it when zooming/etc
423
+ this.refs.$br.find('.BRpagecontainer--hasSelection').removeClass('BRpagecontainer--hasSelection');
424
+ $(window.getSelection().anchorNode).closest('.BRpagecontainer').addClass('BRpagecontainer--hasSelection');
425
+ }
426
+ }).attach();
283
427
  }
428
+
284
429
  super.init();
285
430
  }
286
431
 
@@ -290,14 +435,199 @@ export class BookreaderWithTextSelection extends BookReader {
290
435
  _createPageContainer(index) {
291
436
  const pageContainer = super._createPageContainer(index);
292
437
  // Disable if thumb mode; it's too janky
293
- // index can be -1 for "pre-cover" region
294
- // Added checking of lastPageIndex to avoid loop around index value
295
- const lastPageIndex = this.getNumLeafs() - 1;
296
- if (this.mode !== this.constModeThumb && (index >= 0 && index <= lastPageIndex)) {
297
- this.textSelectionPlugin?.createTextLayer(index, pageContainer.$container);
438
+ // .page can be null for "pre-cover" region
439
+ if (this.mode !== this.constModeThumb && pageContainer.page) {
440
+ this.textSelectionPlugin?.createTextLayer(pageContainer);
298
441
  }
299
442
  return pageContainer;
300
443
  }
301
444
  }
302
445
  window.BookReader = BookreaderWithTextSelection;
303
446
  export default BookreaderWithTextSelection;
447
+
448
+
449
+ /**
450
+ * @param {HTMLElement} parentEl
451
+ * @param {string} selector
452
+ * @returns {Map<Element, Rect>}
453
+ */
454
+ function determineRealRects(parentEl, selector) {
455
+ const initals = {
456
+ position: parentEl.style.position,
457
+ visibility: parentEl.style.visibility,
458
+ top: parentEl.style.top,
459
+ left: parentEl.style.left,
460
+ transform: parentEl.style.transform,
461
+ };
462
+ parentEl.style.position = 'absolute';
463
+ parentEl.style.visibility = 'hidden';
464
+ parentEl.style.top = '0';
465
+ parentEl.style.left = '0';
466
+ parentEl.style.transform = 'none';
467
+ document.body.appendChild(parentEl);
468
+ const rects = new Map(
469
+ Array.from(parentEl.querySelectorAll(selector))
470
+ .map(wordEl => {
471
+ const origRect = wordEl.getBoundingClientRect();
472
+ return [wordEl, new Rect(
473
+ origRect.left + window.scrollX,
474
+ origRect.top + window.scrollY,
475
+ origRect.width,
476
+ origRect.height,
477
+ )];
478
+ })
479
+ );
480
+ document.body.removeChild(parentEl);
481
+ Object.assign(parentEl.style, initals);
482
+ return rects;
483
+ }
484
+
485
+ /**
486
+ * @param {HTMLElement} line
487
+ */
488
+ function augmentLine(line) {
489
+ const words = $(line).find("WORD").toArray();
490
+ return {
491
+ ocrElement: line,
492
+ words,
493
+ firstWord: words[0],
494
+ lastWord: words[words.length - 1],
495
+ };
496
+ }
497
+
498
+ /**
499
+ * @template TFrom, TTo
500
+ * Generator version of map
501
+ * @param {Iterable<TFrom>} gen
502
+ * @param {function(TFrom): TTo} fn
503
+ * @returns {Iterable<TTo>}
504
+ */
505
+ export function* genMap(gen, fn) {
506
+ for (const x of gen) yield fn(x);
507
+ }
508
+
509
+ /**
510
+ * @template T
511
+ * Generator that provides a sliding window of 3 elements,
512
+ * prev, current, and next.
513
+ * @param {Iterable<T>} gen
514
+ * @returns {Iterable<[T | undefined, T, T | undefined]>}
515
+ */
516
+ export function* lookAroundWindow(gen) {
517
+ let prev = undefined;
518
+ let cur = undefined;
519
+ let next = undefined;
520
+ for (const x of gen) {
521
+ if (typeof cur !== 'undefined') {
522
+ next = x;
523
+ yield [prev, cur, next];
524
+ }
525
+ prev = cur;
526
+ cur = x;
527
+ next = undefined;
528
+ }
529
+
530
+ if (typeof cur !== 'undefined') {
531
+ yield [prev, cur, next];
532
+ }
533
+ }
534
+
535
+ /**
536
+ * @template T1, T2
537
+ * Lazy zip implementation to avoid importing lodash
538
+ * Expects iterators to be of the same length
539
+ * @param {Iterable<T1>} gen1
540
+ * @param {Iterable<T2>} gen2
541
+ * @returns {Iterable<[T1, T2]>}
542
+ */
543
+ export function* zip(gen1, gen2) {
544
+ const it1 = gen1[Symbol.iterator]();
545
+ const it2 = gen2[Symbol.iterator]();
546
+ while (true) {
547
+ const r1 = it1.next();
548
+ const r2 = it2.next();
549
+ if (r1.done && r2.done) {
550
+ return;
551
+ }
552
+ if (r1.done || r2.done) {
553
+ throw new Error('zip: one of the iterators is done');
554
+ }
555
+ yield [r1.value, r2.value];
556
+ }
557
+ }
558
+
559
+ /**
560
+ * [left, bottom, right, top]
561
+ * @param {Array<[number, number, number, number]>} bounds
562
+ * @returns {[number, number, number, number]}
563
+ */
564
+ function determineBounds(bounds) {
565
+ let leftMost = Infinity;
566
+ let bottomMost = -Infinity;
567
+ let rightMost = -Infinity;
568
+ let topMost = Infinity;
569
+
570
+ for (const [left, bottom, right, top] of bounds) {
571
+ leftMost = Math.min(leftMost, left);
572
+ bottomMost = Math.max(bottomMost, bottom);
573
+ rightMost = Math.max(rightMost, right);
574
+ topMost = Math.min(topMost, top);
575
+ }
576
+
577
+ return [leftMost, bottomMost, rightMost, topMost];
578
+ }
579
+
580
+ /**
581
+ * Recursively traverses the XML tree and adds coords
582
+ * which are the bounding box of all child coords
583
+ * @param {Element} xmlEl
584
+ */
585
+ function recursivelyAddCoords(xmlEl) {
586
+ if ($(xmlEl).attr('coords') || !xmlEl.children) {
587
+ return;
588
+ }
589
+
590
+ const children = $(xmlEl).children().toArray();
591
+ if (children.length === 0) {
592
+ return;
593
+ }
594
+
595
+ for (const child of children) {
596
+ recursivelyAddCoords(child);
597
+ }
598
+
599
+ const childCoords = [];
600
+
601
+ for (const child of children) {
602
+ if (!$(child).attr('coords')) continue;
603
+ childCoords.push($(child).attr('coords').split(',').map(parseFloat));
604
+ }
605
+
606
+ const boundingCoords = determineBounds(childCoords);
607
+ if (Math.abs(boundingCoords[0]) != Infinity) {
608
+ $(xmlEl).attr('coords', boundingCoords.join(','));
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Basically a polyfill for the native DOMRect class
614
+ */
615
+ class Rect {
616
+ /**
617
+ * @param {number} x
618
+ * @param {number} y
619
+ * @param {number} width
620
+ * @param {number} height
621
+ */
622
+ constructor(x, y, width, height) {
623
+ this.x = x;
624
+ this.y = y;
625
+ this.width = width;
626
+ this.height = height;
627
+ }
628
+
629
+ get right() { return this.x + this.width; }
630
+ get bottom() { return this.y + this.height; }
631
+ get top() { return this.y; }
632
+ get left() { return this.x; }
633
+ }