@internetarchive/bookreader 5.0.0-8 → 5.0.0-80

Sign up to get free protection for your applications and to get access to all the features.
Files changed (313) 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 +398 -1133
  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 +1782 -0
  10. package/BookReader/ia-bookreader-bundle.js.LICENSE.txt +7 -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.iiif.js +2 -0
  69. package/BookReader/plugins/plugin.iiif.js.map +1 -0
  70. package/BookReader/plugins/plugin.resume.js +1 -1
  71. package/BookReader/plugins/plugin.resume.js.map +1 -1
  72. package/BookReader/plugins/plugin.search.js +2 -1
  73. package/BookReader/plugins/plugin.search.js.LICENSE.txt +1 -0
  74. package/BookReader/plugins/plugin.search.js.map +1 -1
  75. package/BookReader/plugins/plugin.text_selection.js +2 -1
  76. package/BookReader/plugins/plugin.text_selection.js.LICENSE.txt +1 -0
  77. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  78. package/BookReader/plugins/plugin.tts.js +1 -1
  79. package/BookReader/plugins/plugin.tts.js.LICENSE.txt +2 -0
  80. package/BookReader/plugins/plugin.tts.js.map +1 -1
  81. package/BookReader/plugins/plugin.url.js +1 -1
  82. package/BookReader/plugins/plugin.url.js.map +1 -1
  83. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
  84. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  85. package/BookReader/webcomponents-bundle.js +3 -0
  86. package/BookReader/webcomponents-bundle.js.LICENSE.txt +9 -0
  87. package/BookReader/webcomponents-bundle.js.map +1 -0
  88. package/BookReaderDemo/BookReaderDemo.css +18 -19
  89. package/BookReaderDemo/BookReaderJSAdvanced.js +0 -3
  90. package/BookReaderDemo/BookReaderJSAutoplay.js +4 -1
  91. package/BookReaderDemo/BookReaderJSSimple.js +1 -0
  92. package/BookReaderDemo/IADemoBr.js +147 -0
  93. package/BookReaderDemo/demo-advanced.html +2 -2
  94. package/BookReaderDemo/demo-autoplay.html +2 -3
  95. package/BookReaderDemo/demo-embed-iframe-src.html +2 -1
  96. package/BookReaderDemo/demo-fullscreen-mobile.html +3 -5
  97. package/BookReaderDemo/demo-fullscreen.html +2 -4
  98. package/BookReaderDemo/demo-iiif.html +99 -12
  99. package/BookReaderDemo/demo-internetarchive.html +214 -18
  100. package/BookReaderDemo/demo-multiple.html +2 -1
  101. package/BookReaderDemo/demo-preview-pages.html +2 -1
  102. package/BookReaderDemo/demo-simple.html +2 -1
  103. package/BookReaderDemo/demo-vendor-fullscreen.html +2 -4
  104. package/BookReaderDemo/ia-multiple-volumes-manifest.js +170 -0
  105. package/BookReaderDemo/immersion-1up.html +2 -2
  106. package/BookReaderDemo/immersion-mode.html +2 -4
  107. package/BookReaderDemo/toggle_controls.html +3 -2
  108. package/BookReaderDemo/view_mode.html +2 -1
  109. package/BookReaderDemo/viewmode-cycle.html +2 -3
  110. package/CHANGELOG.md +536 -33
  111. package/README.md +14 -1
  112. package/babel.config.js +20 -0
  113. package/codecov.yml +6 -0
  114. package/index.html +4 -1
  115. package/jsconfig.json +19 -0
  116. package/netlify.toml +9 -0
  117. package/package.json +70 -60
  118. package/renovate.json +52 -0
  119. package/scripts/preversion.js +0 -1
  120. package/src/BookNavigator/assets/bookmark-colors.js +1 -1
  121. package/src/BookNavigator/assets/button-base.js +4 -2
  122. package/src/BookNavigator/assets/ia-logo.js +17 -0
  123. package/src/BookNavigator/assets/icon_checkmark.js +1 -1
  124. package/src/BookNavigator/assets/icon_close.js +1 -1
  125. package/src/BookNavigator/book-navigator.js +590 -0
  126. package/src/BookNavigator/bookmarks/bookmark-button.js +3 -2
  127. package/src/BookNavigator/bookmarks/bookmark-edit.js +3 -4
  128. package/src/BookNavigator/bookmarks/bookmarks-list.js +2 -3
  129. package/src/BookNavigator/bookmarks/bookmarks-loginCTA.js +3 -8
  130. package/src/BookNavigator/bookmarks/bookmarks-provider.js +27 -17
  131. package/src/BookNavigator/bookmarks/ia-bookmarks.js +116 -67
  132. package/src/BookNavigator/delete-modal-actions.js +1 -1
  133. package/src/BookNavigator/downloads/downloads-provider.js +36 -21
  134. package/src/BookNavigator/downloads/downloads.js +41 -25
  135. package/src/BookNavigator/search/search-provider.js +49 -27
  136. package/src/BookNavigator/search/search-results.js +23 -9
  137. package/src/BookNavigator/sharing.js +27 -0
  138. package/src/BookNavigator/viewable-files.js +95 -0
  139. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +11 -10
  140. package/src/BookNavigator/visual-adjustments/visual-adjustments.js +3 -3
  141. package/src/BookReader/BookModel.js +64 -34
  142. package/src/BookReader/DragScrollable.js +233 -0
  143. package/src/BookReader/Mode1Up.js +56 -351
  144. package/src/BookReader/Mode1UpLit.js +388 -0
  145. package/src/BookReader/Mode2Up.js +73 -1318
  146. package/src/BookReader/Mode2UpLit.js +776 -0
  147. package/src/BookReader/ModeCoordinateSpace.js +29 -0
  148. package/src/BookReader/ModeSmoothZoom.js +312 -0
  149. package/src/BookReader/ModeThumb.js +18 -12
  150. package/src/BookReader/Navbar/Navbar.js +14 -40
  151. package/src/BookReader/PageContainer.js +81 -6
  152. package/src/BookReader/ReduceSet.js +1 -1
  153. package/src/BookReader/Toolbar/Toolbar.js +10 -37
  154. package/src/BookReader/events.js +2 -3
  155. package/src/BookReader/options.js +27 -2
  156. package/src/BookReader/utils/HTMLDimensionsCacher.js +44 -0
  157. package/src/BookReader/utils/ScrollClassAdder.js +31 -0
  158. package/src/BookReader/utils/SelectionObserver.js +45 -0
  159. package/src/BookReader/utils.js +118 -13
  160. package/src/BookReader.js +427 -1061
  161. package/src/assets/icons/magnify-minus.svg +3 -7
  162. package/src/assets/icons/magnify-plus.svg +3 -7
  163. package/src/assets/icons/voice.svg +1 -0
  164. package/src/css/BookReader.scss +1 -5
  165. package/src/css/_BRBookmarks.scss +1 -1
  166. package/src/css/_BRComponent.scss +1 -1
  167. package/src/css/_BRmain.scss +16 -3
  168. package/src/css/_BRnav.scss +12 -39
  169. package/src/css/_BRpages.scss +149 -40
  170. package/src/css/_BRsearch.scss +68 -25
  171. package/src/css/_BRtoolbar.scss +5 -5
  172. package/src/css/_TextSelection.scss +87 -27
  173. package/src/css/_colorbox.scss +2 -2
  174. package/src/css/_controls.scss +20 -7
  175. package/src/css/_icons.scss +1 -1
  176. package/src/ia-bookreader/ia-bookreader.js +224 -0
  177. package/src/plugins/plugin.archive_analytics.js +3 -3
  178. package/src/plugins/plugin.autoplay.js +5 -11
  179. package/src/plugins/plugin.chapters.js +237 -191
  180. package/src/plugins/plugin.iiif.js +151 -0
  181. package/src/plugins/plugin.resume.js +3 -3
  182. package/src/plugins/plugin.text_selection.js +464 -134
  183. package/src/plugins/plugin.vendor-fullscreen.js +4 -4
  184. package/src/plugins/search/plugin.search.js +142 -125
  185. package/src/plugins/search/utils.js +43 -0
  186. package/src/plugins/search/view.js +33 -58
  187. package/src/plugins/tts/AbstractTTSEngine.js +71 -40
  188. package/src/plugins/tts/FestivalTTSEngine.js +13 -14
  189. package/src/plugins/tts/PageChunk.js +15 -21
  190. package/src/plugins/tts/PageChunkIterator.js +8 -12
  191. package/src/plugins/tts/WebTTSEngine.js +87 -71
  192. package/src/plugins/tts/plugin.tts.js +96 -127
  193. package/src/plugins/tts/utils.js +15 -25
  194. package/src/plugins/url/UrlPlugin.js +191 -0
  195. package/src/plugins/{plugin.url.js → url/plugin.url.js} +45 -16
  196. package/src/util/browserSniffing.js +22 -0
  197. package/src/util/docCookies.js +21 -2
  198. package/tests/e2e/README.md +37 -0
  199. package/tests/e2e/autoplay.test.js +2 -2
  200. package/tests/e2e/base.test.js +8 -16
  201. package/tests/e2e/helpers/base.js +53 -48
  202. package/tests/e2e/helpers/debug.js +1 -1
  203. package/tests/e2e/helpers/params.js +17 -0
  204. package/tests/e2e/helpers/rightToLeft.js +8 -14
  205. package/tests/e2e/helpers/search.js +73 -0
  206. package/tests/e2e/models/Navigation.js +20 -37
  207. package/tests/e2e/rightToLeft.test.js +4 -5
  208. package/tests/e2e/viewmode.test.js +40 -33
  209. package/tests/jest/BookNavigator/book-navigator.test.js +661 -0
  210. package/tests/jest/BookNavigator/bookmarks/bookmark-button.test.js +43 -0
  211. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmark-edit.test.js +25 -26
  212. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmarks-list.test.js +41 -42
  213. package/tests/jest/BookNavigator/bookmarks/ia-bookmarks.test.js +45 -0
  214. package/tests/jest/BookNavigator/downloads/downloads-provider.test.js +67 -0
  215. package/tests/jest/BookNavigator/downloads/downloads.test.js +53 -0
  216. package/tests/jest/BookNavigator/search/search-provider.test.js +167 -0
  217. package/tests/{karma → jest}/BookNavigator/search/search-results.test.js +109 -60
  218. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +49 -0
  219. package/tests/jest/BookNavigator/viewable-files/viewable-files-provider.test.js +80 -0
  220. package/tests/jest/BookNavigator/visual-adjustments.test.js +200 -0
  221. package/tests/{BookReader → jest/BookReader}/BookModel.test.js +74 -14
  222. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +193 -0
  223. package/tests/{BookReader → jest/BookReader}/ImageCache.test.js +4 -4
  224. package/tests/jest/BookReader/Mode1UpLit.test.js +73 -0
  225. package/tests/jest/BookReader/Mode2Up.test.js +98 -0
  226. package/tests/jest/BookReader/Mode2UpLit.test.js +190 -0
  227. package/tests/jest/BookReader/ModeCoordinateSpace.test.js +16 -0
  228. package/tests/jest/BookReader/ModeSmoothZoom.test.js +218 -0
  229. package/tests/jest/BookReader/ModeThumb.test.js +71 -0
  230. package/tests/{BookReader → jest/BookReader}/Navbar/Navbar.test.js +10 -10
  231. package/tests/{BookReader → jest/BookReader}/PageContainer.test.js +88 -6
  232. package/tests/{BookReader → jest/BookReader}/ReduceSet.test.js +1 -1
  233. package/tests/{BookReader → jest/BookReader}/Toolbar/Toolbar.test.js +2 -2
  234. package/tests/jest/BookReader/utils/HTMLDimensionsCacher.test.js +59 -0
  235. package/tests/jest/BookReader/utils/ScrollClassAdder.test.js +49 -0
  236. package/tests/jest/BookReader/utils/SelectionObserver.test.js +57 -0
  237. package/tests/{BookReader → jest/BookReader}/utils/classes.test.js +1 -1
  238. package/tests/jest/BookReader/utils.test.js +229 -0
  239. package/tests/jest/BookReader.keyboard.test.js +190 -0
  240. package/tests/{BookReader.options.test.js → jest/BookReader.options.test.js} +9 -1
  241. package/tests/{BookReader.test.js → jest/BookReader.test.js} +26 -37
  242. package/tests/{plugins → jest/plugins}/plugin.archive_analytics.test.js +2 -2
  243. package/tests/{plugins → jest/plugins}/plugin.autoplay.test.js +4 -4
  244. package/tests/jest/plugins/plugin.chapters.test.js +195 -0
  245. package/tests/{plugins → jest/plugins}/plugin.iframe.test.js +2 -2
  246. package/tests/{plugins → jest/plugins}/plugin.resume.test.js +3 -3
  247. package/tests/jest/plugins/plugin.text_selection.test.js +317 -0
  248. package/tests/{plugins → jest/plugins}/plugin.vendor-fullscreen.test.js +2 -2
  249. package/tests/{plugins → jest/plugins}/search/plugin.search.test.js +25 -47
  250. package/tests/{plugins → jest/plugins}/search/plugin.search.view.test.js +39 -6
  251. package/tests/jest/plugins/search/utils.js +25 -0
  252. package/tests/jest/plugins/search/utils.test.js +29 -0
  253. package/tests/{plugins → jest/plugins}/tts/AbstractTTSEngine.test.js +29 -9
  254. package/tests/{plugins → jest/plugins}/tts/FestivalTTSEngine.test.js +4 -4
  255. package/tests/{plugins → jest/plugins}/tts/PageChunk.test.js +1 -1
  256. package/tests/{plugins → jest/plugins}/tts/PageChunkIterator.test.js +3 -3
  257. package/tests/{plugins → jest/plugins}/tts/WebTTSEngine.test.js +47 -1
  258. package/tests/{plugins → jest/plugins}/tts/utils.test.js +1 -60
  259. package/tests/jest/plugins/url/UrlPlugin.test.js +198 -0
  260. package/tests/{plugins → jest/plugins/url}/plugin.url.test.js +53 -14
  261. package/tests/jest/setup.js +3 -0
  262. package/tests/{util → jest/util}/browserSniffing.test.js +1 -1
  263. package/tests/jest/util/docCookies.test.js +24 -0
  264. package/tests/{util → jest/util}/strings.test.js +1 -1
  265. package/tests/{utils.js → jest/utils.js} +38 -0
  266. package/webpack.config.js +12 -6
  267. package/.babelrc +0 -12
  268. package/.dependabot/config.yml +0 -6
  269. package/.testcaferc.json +0 -5
  270. package/BookReader/bookreader-component-bundle.js +0 -1450
  271. package/BookReader/bookreader-component-bundle.js.LICENSE.txt +0 -38
  272. package/BookReader/bookreader-component-bundle.js.map +0 -1
  273. package/BookReader/jquery-1.10.1.js +0 -2
  274. package/BookReader/jquery-1.10.1.js.LICENSE.txt +0 -24
  275. package/BookReader/plugins/plugin.menu_toggle.js +0 -2
  276. package/BookReader/plugins/plugin.menu_toggle.js.map +0 -1
  277. package/BookReader/plugins/plugin.mobile_nav.js +0 -2
  278. package/BookReader/plugins/plugin.mobile_nav.js.map +0 -1
  279. package/BookReaderDemo/IIIFBookReader.js +0 -207
  280. package/BookReaderDemo/bookreader-template-bundle.js +0 -7178
  281. package/BookReaderDemo/demo-iiif.js +0 -26
  282. package/BookReaderDemo/demo-plugin-menu-toggle.html +0 -34
  283. package/karma.conf.js +0 -23
  284. package/src/BookNavigator/BookModel.js +0 -14
  285. package/src/BookNavigator/BookNavigator.js +0 -446
  286. package/src/BookNavigator/assets/book-loader.js +0 -27
  287. package/src/BookNavigator/br-fullscreen-mgr.js +0 -83
  288. package/src/BookNavigator/search/a-search-result.js +0 -55
  289. package/src/BookReader/DebugConsole.js +0 -54
  290. package/src/BookReaderComponent/BookReaderComponent.js +0 -112
  291. package/src/ItemNavigator/ItemNavigator.js +0 -376
  292. package/src/ItemNavigator/providers/sharing.js +0 -29
  293. package/src/css/_MobileNav.scss +0 -194
  294. package/src/dragscrollable-br.js +0 -261
  295. package/src/lit-wrapper.js +0 -2
  296. package/src/plugins/menu_toggle/plugin.menu_toggle.js +0 -324
  297. package/src/plugins/plugin.mobile_nav.js +0 -287
  298. package/tests/BookReader/BookReaderPublicFunctions.test.js +0 -171
  299. package/tests/BookReader/DebugConsole.test.js +0 -25
  300. package/tests/BookReader/Mode1Up.test.js +0 -164
  301. package/tests/BookReader/Mode2Up.test.js +0 -247
  302. package/tests/BookReader/utils.test.js +0 -109
  303. package/tests/e2e/helpers/desktopSearch.js +0 -72
  304. package/tests/e2e/helpers/mobileSearch.js +0 -85
  305. package/tests/e2e/ia-production/ia-prod-base.js +0 -17
  306. package/tests/karma/BookNavigator/book-navigator.test.js +0 -132
  307. package/tests/karma/BookNavigator/search/search-provider.test.js +0 -23
  308. package/tests/karma/BookNavigator/visual-adjustments.test.js +0 -201
  309. package/tests/plugins/menu_toggle/plugin.menu_toggle.test.js +0 -68
  310. package/tests/plugins/plugin.chapters.test.js +0 -130
  311. package/tests/plugins/plugin.mobile_nav.test.js +0 -66
  312. package/tests/plugins/plugin.text_selection.test.js +0 -203
  313. 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
+ }