@internetarchive/bookreader 5.0.0-63 → 5.0.0-64

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 (98) hide show
  1. package/.github/workflows/node.js.yml +1 -0
  2. package/BookReader/BookReader.css +45 -58
  3. package/BookReader/BookReader.js +1 -1
  4. package/BookReader/BookReader.js.LICENSE.txt +2 -0
  5. package/BookReader/BookReader.js.map +1 -1
  6. package/BookReader/ia-bookreader-bundle.js +95 -95
  7. package/BookReader/ia-bookreader-bundle.js.LICENSE.txt +2 -0
  8. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  9. package/BookReader/icons/1up.svg +1 -1
  10. package/BookReader/icons/2up.svg +1 -1
  11. package/BookReader/icons/advance.svg +1 -1
  12. package/BookReader/icons/chevron-right.svg +1 -1
  13. package/BookReader/icons/close-circle-dark.svg +1 -1
  14. package/BookReader/icons/close-circle.svg +1 -1
  15. package/BookReader/icons/fullscreen.svg +1 -1
  16. package/BookReader/icons/fullscreen_exit.svg +1 -1
  17. package/BookReader/icons/hamburger.svg +1 -1
  18. package/BookReader/icons/left-arrow.svg +1 -1
  19. package/BookReader/icons/magnify-minus.svg +1 -1
  20. package/BookReader/icons/magnify-plus.svg +1 -1
  21. package/BookReader/icons/magnify.svg +1 -1
  22. package/BookReader/icons/pause.svg +1 -1
  23. package/BookReader/icons/play.svg +1 -1
  24. package/BookReader/icons/playback-speed.svg +1 -1
  25. package/BookReader/icons/read-aloud.svg +1 -1
  26. package/BookReader/icons/review.svg +1 -1
  27. package/BookReader/icons/thumbnails.svg +1 -1
  28. package/BookReader/icons/voice.svg +1 -1
  29. package/BookReader/icons/volume-full.svg +1 -1
  30. package/BookReader/images/BRicons.svg +3 -3
  31. package/BookReader/images/books_graphic.svg +1 -1
  32. package/BookReader/images/icon_book.svg +1 -1
  33. package/BookReader/images/icon_bookmark.svg +1 -1
  34. package/BookReader/images/icon_gear.svg +1 -1
  35. package/BookReader/images/icon_hamburger.svg +1 -1
  36. package/BookReader/images/icon_home.svg +1 -1
  37. package/BookReader/images/icon_info.svg +1 -1
  38. package/BookReader/images/icon_one_page.svg +1 -1
  39. package/BookReader/images/icon_pause.svg +1 -1
  40. package/BookReader/images/icon_play.svg +1 -1
  41. package/BookReader/images/icon_search_button.svg +1 -1
  42. package/BookReader/images/icon_share.svg +1 -1
  43. package/BookReader/images/icon_skip-ahead.svg +1 -1
  44. package/BookReader/images/icon_skip-back.svg +1 -1
  45. package/BookReader/images/icon_speaker.svg +1 -1
  46. package/BookReader/images/icon_speaker_open.svg +1 -1
  47. package/BookReader/images/icon_thumbnails.svg +1 -1
  48. package/BookReader/images/icon_toc.svg +1 -1
  49. package/BookReader/images/icon_two_pages.svg +1 -1
  50. package/BookReader/images/marker_chap-off.svg +1 -1
  51. package/BookReader/images/marker_chap-on.svg +1 -1
  52. package/BookReader/images/marker_srch-on.svg +1 -1
  53. package/BookReader/jquery-3.js +1 -1
  54. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  55. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  56. package/BookReader/plugins/plugin.autoplay.js +1 -1
  57. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  58. package/BookReader/plugins/plugin.chapters.js +2 -1
  59. package/BookReader/plugins/plugin.chapters.js.LICENSE.txt +1 -0
  60. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  61. package/BookReader/plugins/plugin.iframe.js.map +1 -1
  62. package/BookReader/plugins/plugin.mobile_nav.js +1 -1
  63. package/BookReader/plugins/plugin.mobile_nav.js.map +1 -1
  64. package/BookReader/plugins/plugin.resume.js +1 -1
  65. package/BookReader/plugins/plugin.resume.js.map +1 -1
  66. package/BookReader/plugins/plugin.search.js +2 -1
  67. package/BookReader/plugins/plugin.search.js.LICENSE.txt +1 -0
  68. package/BookReader/plugins/plugin.search.js.map +1 -1
  69. package/BookReader/plugins/plugin.text_selection.js +2 -1
  70. package/BookReader/plugins/plugin.text_selection.js.LICENSE.txt +1 -0
  71. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  72. package/BookReader/plugins/plugin.tts.js +1 -1
  73. package/BookReader/plugins/plugin.tts.js.LICENSE.txt +2 -0
  74. package/BookReader/plugins/plugin.tts.js.map +1 -1
  75. package/BookReader/plugins/plugin.url.js +1 -1
  76. package/BookReader/plugins/plugin.url.js.map +1 -1
  77. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
  78. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  79. package/BookReader/webcomponents-bundle.js +1 -1
  80. package/BookReader/webcomponents-bundle.js.map +1 -1
  81. package/CHANGELOG.md +5 -0
  82. package/babel.config.js +5 -4
  83. package/netlify.toml +4 -0
  84. package/package.json +15 -15
  85. package/src/BookNavigator/search/search-results.js +1 -7
  86. package/src/BookReader/utils.js +10 -0
  87. package/src/css/_BRnav.scss +2 -2
  88. package/src/css/_BRsearch.scss +38 -10
  89. package/src/plugins/search/plugin.search.js +14 -21
  90. package/src/plugins/search/utils.js +43 -0
  91. package/src/plugins/search/view.js +11 -24
  92. package/tests/e2e/helpers/desktopSearch.js +3 -3
  93. package/tests/jest/BookNavigator/search/search-results.test.js +6 -1
  94. package/tests/jest/BookReader/utils.test.js +12 -0
  95. package/tests/jest/plugins/search/plugin.search.test.js +2 -39
  96. package/tests/jest/plugins/search/plugin.search.view.test.js +5 -0
  97. package/tests/jest/plugins/search/utils.js +25 -0
  98. package/tests/jest/plugins/search/utils.test.js +29 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 5.0.0-64
2
+ - Dev: update dependencies @renovate
3
+ - Dev: update build step @cdrini
4
+ - Fix: Search inside option for {{{/}}} + http support @cdrini
5
+
1
6
  # 5.0.0-63
2
7
  Fix: Don't limit autoFit zoom to real world size @cdrini
3
8
  Dev: Update test deps @cdrini
package/babel.config.js CHANGED
@@ -11,9 +11,10 @@ module.exports = {
11
11
  ]
12
12
  ],
13
13
  plugins: [
14
- ["@babel/plugin-proposal-decorators", {decoratorsBeforeExport: true}],
15
- ["@babel/plugin-proposal-class-properties", {loose: true}],
16
- ["@babel/plugin-proposal-private-property-in-object", { loose: true }],
17
- ["@babel/plugin-proposal-private-methods", { loose: true }],
14
+ ["@babel/plugin-proposal-decorators", {
15
+ version: "2018-09",
16
+ decoratorsBeforeExport: true
17
+ }],
18
+ ["@babel/plugin-proposal-class-properties"],
18
19
  ]
19
20
  };
package/netlify.toml CHANGED
@@ -1,3 +1,7 @@
1
+ [build.environment]
2
+ # Keep in sync with CI in .github/workflows/node.js.yml
3
+ NODE_VERSION = "16"
4
+
1
5
  [[headers]]
2
6
  # Define which paths this specific [[headers]] block will cover.
3
7
  for = "/BookReader/*"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/bookreader",
3
- "version": "5.0.0-63",
3
+ "version": "5.0.0-64",
4
4
  "description": "The Internet Archive BookReader.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,19 +41,19 @@
41
41
  "lit": "^2.5.0"
42
42
  },
43
43
  "devDependencies": {
44
- "@babel/core": "7.17.9",
44
+ "@babel/core": "7.22.5",
45
45
  "@babel/eslint-parser": "7.21.8",
46
- "@babel/plugin-proposal-class-properties": "7.16.7",
47
- "@babel/plugin-proposal-decorators": "7.17.9",
48
- "@babel/preset-env": "7.16.11",
49
- "@open-wc/testing-helpers": "^2.2.1",
46
+ "@babel/plugin-proposal-class-properties": "7.18.6",
47
+ "@babel/plugin-proposal-decorators": "7.22.5",
48
+ "@babel/preset-env": "7.22.5",
49
+ "@open-wc/testing-helpers": "^2.3.0",
50
50
  "@types/jest": "29.5.2",
51
51
  "@webcomponents/webcomponentsjs": "^2.6.0",
52
- "babel-loader": "8.2.5",
52
+ "babel-loader": "9.1.2",
53
53
  "codecov": "^3.8.3",
54
54
  "concurrently": "7.4.0",
55
- "core-js": "3.22.3",
56
- "cpx2": "4.2.0",
55
+ "core-js": "3.30.2",
56
+ "cpx2": "4.2.3",
57
57
  "eslint": "^7.32.0",
58
58
  "eslint-plugin-no-jquery": "^2.7.0",
59
59
  "eslint-plugin-testcafe": "^0.2.1",
@@ -70,15 +70,15 @@
70
70
  "jquery.mmenu": "5.6.5",
71
71
  "live-server": "1.2.2",
72
72
  "node-fetch": "3.2.10",
73
- "regenerator-runtime": "0.13.9",
74
- "sass": "1.52.1",
73
+ "regenerator-runtime": "0.13.11",
74
+ "sass": "1.63.3",
75
75
  "sinon": "15.1.0",
76
76
  "soundmanager2": "2.97.20170602",
77
- "svgo": "2.8.0",
77
+ "svgo": "3.0.2",
78
78
  "testcafe": "2.6.2",
79
79
  "testcafe-browser-provider-browserstack": "^1.13.2-alpha.1",
80
- "webpack": "5.51.1",
81
- "webpack-cli": "4.9.2"
80
+ "webpack": "5.86.0",
81
+ "webpack-cli": "5.1.4"
82
82
  },
83
83
  "jest": {
84
84
  "testEnvironment": "jsdom",
@@ -121,7 +121,7 @@
121
121
  "DOCS:update:test-deps": "If CI succeeds, these should be good to update",
122
122
  "update:test-deps": "npm i @babel/eslint-parser@latest @open-wc/testing-helpers@latest @types/jest@latest codecov@latest eslint@7 eslint-plugin-testcafe@latest jest@latest sinon@latest testcafe@latest",
123
123
  "DOCS:update:build-deps": "These can cause strange changes, so do an npm run build + check file size (git diff --stat), and check the site is as expected",
124
- "update:build-deps": "npm i @babel/core@latest @babel/preset-env@latest babel-loader@latest core-js@latest regenerator-runtime@latest sass@latest svgo@latest webpack@latest webpack-cli@latest",
124
+ "update:build-deps": "npm i @babel/core@latest @babel/preset-env@latest @babel/plugin-proposal-class-properties@latest @babel/plugin-proposal-decorators@latest babel-loader@latest core-js@latest regenerator-runtime@latest sass@latest svgo@latest webpack@latest webpack-cli@latest",
125
125
  "codecov": "npx codecov"
126
126
  }
127
127
  }
@@ -1,5 +1,4 @@
1
1
  /* eslint-disable class-methods-use-this */
2
- import { escapeHTML } from '../../BookReader/utils.js';
3
2
  import { unsafeHTML } from 'lit/directives/unsafe-html.js';
4
3
  import { css, html, LitElement, nothing } from 'lit';
5
4
  import '@internetarchive/ia-activity-indicator/ia-activity-indicator';
@@ -146,12 +145,7 @@ export class IABookSearchResults extends LitElement {
146
145
  ${match.cover ? html`<img src="${match.cover}" />` : nothing}
147
146
  <h4>${match.title || nothing}</h4>
148
147
  <p class="page-num">Page ${match.displayPageNumber}</p>
149
- <p>
150
- ${
151
- // [^] matches any character, including line breaks
152
- unsafeHTML(escapeHTML(match.text).replace(/{{{([^]+?)}}}/g, '<mark>$1</mark>'))
153
- }
154
- </p>
148
+ <p>${unsafeHTML(match.html)}</p>
155
149
  </li>
156
150
  `)}
157
151
  </ul>
@@ -278,3 +278,13 @@ export function promisifyEvent(target, eventType) {
278
278
  target.addEventListener(eventType, resolver);
279
279
  });
280
280
  }
281
+
282
+ /**
283
+ * Escapes regex special characters in a string. Allows for safe usage in regexes.
284
+ * Src: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions
285
+ * @param {string} string
286
+ * @returns {string}
287
+ */
288
+ export function escapeRegExp(string) {
289
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
290
+ }
@@ -77,8 +77,8 @@
77
77
  }
78
78
 
79
79
  @keyframes fadeUp {
80
- from { opacity: 0; transform: translateY(10px); }
81
- to { opacity: 1; transform: translateY(0); }
80
+ from { opacity: 0; translate: 0 10px; }
81
+ to { opacity: 1; translate: 0 0; }
82
82
  }
83
83
 
84
84
  .BRfooter {
@@ -1,26 +1,45 @@
1
+ @mixin ellipsis-lines($lines: 4) {
2
+ display: -webkit-box;
3
+ -webkit-line-clamp: $lines;
4
+ -webkit-box-orient: vertical;
5
+ overflow: hidden;
6
+ }
7
+
1
8
  %timeline-tooltip {
2
9
  display: none;
3
10
  position: absolute;
4
11
  bottom: calc(100% + 5px);
5
12
  left: 50%;
6
13
  transform: translateX(-50%);
7
- width: 230px;
14
+ width: 350px;
15
+ max-width: 100vw;
8
16
  padding: 12px 14px;
17
+ padding-bottom: 10px;
9
18
  color: $tooltipText;
10
- font-weight: bold;
11
19
  background: $tooltipBG;
12
20
  box-shadow: 0 2px 4px rgba(0, 0, 0, .5);
21
+ border-radius: 4px;
22
+ animation: fadeUp 0.2s;
23
+
24
+ // Disable text selection
25
+ -webkit-user-select: none;
26
+ -moz-user-select: none;
27
+ -ms-user-select: none;
28
+ -o-user-select: none;
29
+ user-select: none;
30
+
31
+ // Create a triangle under the tooltip using clip-path
32
+ // This makes it possible to move the mouse onto the tooltip
13
33
  &:after {
14
- display: none;
15
34
  position: absolute;
16
- top: 100%;
35
+ content: "";
36
+ bottom: -9px;
17
37
  left: 50%;
38
+ margin-left: -1px;
18
39
  transform: translateX(-50%);
19
- content: "";
20
- border: 7px solid transparent;
21
- border-width: 7px 4px;
22
- border-bottom: none;
23
- border-top-color: $tooltipBG;
40
+ width: 30px;
41
+ height: 10px;
42
+ clip-path: polygon(0 0, 100% 0, 50% 100%);
24
43
  }
25
44
  }
26
45
 
@@ -105,7 +124,16 @@
105
124
  }
106
125
  .BRquery {
107
126
  @extend %timeline-tooltip;
108
- b {
127
+ main {
128
+ @include ellipsis-lines(4);
129
+ margin-bottom: 6px;
130
+ }
131
+ footer {
132
+ text-align: center;
133
+ font-weight: bold;
134
+ font-size: 0.9em;
135
+ }
136
+ mark {
109
137
  color: $searchResultText;
110
138
  font-weight: bold;
111
139
  background-color: $searchResultBG;
@@ -27,6 +27,7 @@
27
27
  import { poll } from '../../BookReader/utils.js';
28
28
  import { renderBoxesInPageContainerLayer } from '../../BookReader/PageContainer.js';
29
29
  import SearchView from './view.js';
30
+ import { marshallSearchResults } from './utils.js';
30
31
  /** @typedef {import('../../BookReader/PageContainer').PageContainer} PageContainer */
31
32
  /** @typedef {import('../../BookReader/BookModel').PageIndex} PageIndex */
32
33
  /** @typedef {import('../../BookReader/BookModel').LeafNum} LeafNum */
@@ -38,7 +39,10 @@ jQuery.extend(BookReader.defaultOptions, {
38
39
  subPrefix: '',
39
40
  bookPath: '',
40
41
  enableSearch: true,
42
+ searchInsideProtocol: 'https',
41
43
  searchInsideUrl: '/fulltext/inside.php',
44
+ searchInsidePreTag: '{{{',
45
+ searchInsidePostTag: '}}}',
42
46
  initialSearchTerm: null,
43
47
  });
44
48
 
@@ -170,7 +174,7 @@ BookReader.prototype.search = async function(term = '', overrides = {}) {
170
174
 
171
175
  // Remove the port and userdir
172
176
  const serverPath = this.server.replace(/:.+/, '');
173
- const baseUrl = `https://${serverPath}${this.searchInsideUrl}?`;
177
+ const baseUrl = `${this.options.searchInsideProtocol}://${serverPath}${this.searchInsideUrl}?`;
174
178
 
175
179
  // Remove subPrefix from end of path
176
180
  let path = this.bookPath;
@@ -184,6 +188,8 @@ BookReader.prototype.search = async function(term = '', overrides = {}) {
184
188
  doc: this.subPrefix,
185
189
  path,
186
190
  q: term,
191
+ pre_tag: this.options.searchInsidePreTag,
192
+ post_tag: this.options.searchInsidePostTag,
187
193
  };
188
194
 
189
195
  // NOTE that the API does not expect / (slashes) to be encoded. (%2F) won't work
@@ -261,6 +267,7 @@ BookReader.prototype.cancelSearchRequest = function () {
261
267
  * @typedef {object} SearchInsideMatch
262
268
  * @property {number} matchIndex This is a fake field! Not part of the API response. It is added by the JS.
263
269
  * @property {string} displayPageNumber (fake field) The page number as it should be displayed in the UI.
270
+ * @property {string} html (computed field) The html-escaped raw html to display in the UI.
264
271
  * @property {string} text
265
272
  * @property {Array<{ page: number, boxes: SearchInsideMatchBox[] }>} par
266
273
  */
@@ -272,25 +279,6 @@ BookReader.prototype.cancelSearchRequest = function () {
272
279
  * @property {boolean} indexed
273
280
  */
274
281
 
275
- /**
276
- * Attach some fields to search inside results
277
- * @param {SearchInsideResults} results
278
- * @param {(pageNum: LeafNum) => PageNumString} displayPageNumberFn
279
- */
280
- export function marshallSearchResults(results, displayPageNumberFn) {
281
- // Attach matchIndex to a few things to make it easier to identify
282
- // an active/selected match
283
- for (const [index, match] of results.matches.entries()) {
284
- match.matchIndex = index;
285
- match.displayPageNumber = displayPageNumberFn(match.par[0].page);
286
- for (const par of match.par) {
287
- for (const box of par.boxes) {
288
- box.matchIndex = index;
289
- }
290
- }
291
- }
292
- }
293
-
294
282
  /**
295
283
  * Search Results return handler
296
284
  * @param {SearchInsideResults} results
@@ -298,7 +286,12 @@ export function marshallSearchResults(results, displayPageNumberFn) {
298
286
  * @param {boolean} options.goToFirstResult
299
287
  */
300
288
  BookReader.prototype.BRSearchCallback = function(results, options) {
301
- marshallSearchResults(results, pageNum => this.book.getPageNum(this.book.leafNumToIndex(pageNum)));
289
+ marshallSearchResults(
290
+ results,
291
+ pageNum => this.book.getPageNum(this.book.leafNumToIndex(pageNum)),
292
+ this.options.searchInsidePreTag,
293
+ this.options.searchInsidePostTag,
294
+ );
302
295
  this.searchResults = results || [];
303
296
 
304
297
  this.updateSearchHilites();
@@ -0,0 +1,43 @@
1
+ import { escapeHTML, escapeRegExp } from '../../BookReader/utils.js';
2
+
3
+ /**
4
+ * @param {string} match
5
+ * @param {string} preTag
6
+ * @param {string} postTag
7
+ * @returns {string}
8
+ */
9
+ export function renderMatch(match, preTag, postTag) {
10
+ // Search results are returned as a text blob with the hits wrapped in
11
+ // triple mustaches. Hits occasionally include text beyond the search
12
+ // term, so everything within the staches is captured and wrapped.
13
+ const preTagRe = escapeRegExp(escapeHTML(preTag));
14
+ const postTagRe = escapeRegExp(escapeHTML(postTag));
15
+ // [^] matches any character, including line breaks
16
+ const regex = new RegExp(`${preTagRe}([^]+?)${postTagRe}`, 'g');
17
+ return escapeHTML(match)
18
+ .replace(regex, '<mark>$1</mark>')
19
+ // Fix trailing hyphens. This over-corrects but is net useful.
20
+ .replace(/(\b)- /g, '$1');
21
+ }
22
+
23
+ /**
24
+ * Attach some fields to search inside results
25
+ * @param {SearchInsideResults} results
26
+ * @param {(pageNum: LeafNum) => PageNumString} displayPageNumberFn
27
+ * @param {string} preTag
28
+ * @param {string} postTag
29
+ */
30
+ export function marshallSearchResults(results, displayPageNumberFn, preTag, postTag) {
31
+ // Attach matchIndex to a few things to make it easier to identify
32
+ // an active/selected match
33
+ for (const [index, match] of results.matches.entries()) {
34
+ match.matchIndex = index;
35
+ match.displayPageNumber = displayPageNumberFn(match.par[0].page);
36
+ match.html = renderMatch(match.text, preTag, postTag);
37
+ for (const par of match.par) {
38
+ for (const box of par.boxes) {
39
+ box.matchIndex = index;
40
+ }
41
+ }
42
+ }
43
+ }
@@ -1,5 +1,3 @@
1
- import { escapeHTML } from "../../BookReader/utils.js";
2
-
3
1
  class SearchView {
4
2
  /**
5
3
  * @param {object} params
@@ -11,11 +9,6 @@ class SearchView {
11
9
  */
12
10
  constructor({ br, searchCancelledCallback = () => {} }) {
13
11
  this.br = br;
14
-
15
- // Search results are returned as a text blob with the hits wrapped in
16
- // triple mustaches. Hits occasionally include text beyond the search
17
- // term, so everything within the staches is captured and wrapped.
18
- this.matcher = new RegExp('{{{([^]+?)}}}', 'g'); // [^] matches any character, including line breaks
19
12
  this.matches = [];
20
13
  this.cacheDOMElements();
21
14
  this.bindEvents();
@@ -230,26 +223,20 @@ class SearchView {
230
223
  */
231
224
  renderPins(matches) {
232
225
  matches.forEach((match) => {
233
- const queryString = match.text;
234
226
  const pageIndex = this.br.book.leafNumToIndex(match.par[0].page);
235
227
  const uiStringSearch = "Search result"; // i18n
236
-
237
228
  const percentThrough = this.br.constructor.util.cssPercentage(pageIndex, this.br.book.getNumLeafs() - 1);
238
229
 
239
- const escapedQueryString = escapeHTML(queryString);
240
- const queryStringWithB = escapedQueryString.replace(this.matcher, '<b>$1</b>');
241
-
242
- let queryStringWithBTruncated = '';
243
-
244
- if (queryString.length > 100) {
245
- queryStringWithBTruncated = queryString.replace(/^(.{100}[^\s]*).*/, "$1");
246
-
247
- // If truncating, we must escape *after* truncation occurs (but before wrapping in <b>)
248
- queryStringWithBTruncated = escapeHTML(queryStringWithBTruncated)
249
- .replace(this.matcher, '<b>$1</b>')
250
- + '...';
230
+ let html = match.html;
231
+ if (html.length > 200) {
232
+ const start = Math.max(0, html.indexOf('<mark>') - 100);
233
+ if (start != 0) {
234
+ html = '…' + match.html
235
+ .substring(start)
236
+ // Make sure at word boundary though
237
+ .replace(/^\S+/, '');
238
+ }
251
239
  }
252
-
253
240
  // draw marker
254
241
  $('<div>')
255
242
  .addClass('BRsearch')
@@ -259,8 +246,8 @@ class SearchView {
259
246
  .attr('title', uiStringSearch)
260
247
  .append(`
261
248
  <div class="BRquery">
262
- <div>${queryStringWithBTruncated || queryStringWithB}</div>
263
- <div>Page ${match.displayPageNumber}</div>
249
+ <main>${html}</main>
250
+ <footer>Page ${match.displayPageNumber}</footer>
264
251
  </div>
265
252
  `)
266
253
  .appendTo(this.br.$('.BRnavline'))
@@ -29,8 +29,8 @@ export function runDesktopSearchTests(br) {
29
29
  await t.pressKey('enter');
30
30
 
31
31
  await t.expect(nav.desktop.searchPin.exists).ok();
32
- await t.expect(nav.desktop.searchPin.child('.BRquery').child('div').exists).ok();
33
- await t.expect(nav.desktop.searchPin.child('.BRquery').child('div').innerText).contains(TEST_TEXT_FOUND);
32
+ await t.expect(nav.desktop.searchPin.child('.BRquery').child('main').exists).ok();
33
+ await t.expect(nav.desktop.searchPin.child('.BRquery').child('main').innerText).contains(TEST_TEXT_FOUND);
34
34
  await t.expect(nav.desktop.searchNavigation.exists).ok();
35
35
  await t.expect(nav.desktop.searchNavigation.find('[data-id="resultsCount"]').exists).ok();
36
36
  await t.expect(nav.desktop.searchNavigation.find('[data-id="resultsCount"]').innerText).contains(SEARCH_MATCHES_LENGTH);
@@ -63,7 +63,7 @@ export function runDesktopSearchTests(br) {
63
63
  // FIXME: Why is it only typing every other letter?!?!
64
64
  await t.typeText(nav.desktop.searchBox, TEST_TEXT_NOT_FOUND.split('').join('_'));
65
65
  await t.pressKey('enter');
66
- await t.expect(nav.desktop.searchPin.child('.BRquery').child('div').withText(TEST_TEXT_NOT_FOUND).exists).notOk();
66
+ await t.expect(nav.desktop.searchPin.child('.BRquery').child('main').withText(TEST_TEXT_NOT_FOUND).exists).notOk();
67
67
 
68
68
  const getPageUrl = ClientFunction(() => window.location.href.toString());
69
69
  await t.expect(getPageUrl()).contains(TEST_TEXT_NOT_FOUND);
@@ -5,6 +5,7 @@ import {
5
5
  } from '@open-wc/testing-helpers';
6
6
  import sinon from 'sinon';
7
7
  import { IABookSearchResults } from '@/src/BookNavigator/search/search-results.js';
8
+ import { marshallSearchResults } from '@/src/plugins/search/utils.js';
8
9
 
9
10
  const container = (results = [], query = '') => (
10
11
  html`<ia-book-search-results .results=${results} .query=${query}></ia-book-search-results>`
@@ -43,9 +44,11 @@ const results = [{
43
44
  l: 432,
44
45
  page_height: 5357,
45
46
  page: 86,
46
- }],
47
+ }]
47
48
  }];
48
49
 
50
+ marshallSearchResults({ matches: results }, () => '', '{{{', '}}}');
51
+
49
52
  const resultWithScript = [{
50
53
  text: `foo bar <script>const msg = 'test' + ' failure'; document.write(msg);</script> {{{${searchQuery}}}} baz`,
51
54
  cover: '//placehold.it/30x44',
@@ -65,6 +68,8 @@ const resultWithScript = [{
65
68
  }],
66
69
  }];
67
70
 
71
+ marshallSearchResults({ matches: resultWithScript }, () => '', '{{{', '}}}');
72
+
68
73
  describe('<ia-book-search-results>', () => {
69
74
  afterEach(() => {
70
75
  sinon.restore();
@@ -7,6 +7,7 @@ import {
7
7
  decodeURIComponentPlus,
8
8
  encodeURIComponentPlus,
9
9
  escapeHTML,
10
+ escapeRegExp,
10
11
  getActiveElement,
11
12
  isInputActive,
12
13
  poll,
@@ -215,3 +216,14 @@ describe('promisifyEvent', () => {
215
216
  expect(resolveSpy.callCount).toBe(1);
216
217
  });
217
218
  });
219
+
220
+ describe('escapeRegex', () => {
221
+ test('Escapes regex', () => {
222
+ expect(escapeRegExp('.*')).toBe('\\.\\*');
223
+ expect(escapeRegExp('foo')).toBe('foo');
224
+ expect(escapeRegExp('foo.bar')).toBe('foo\\.bar');
225
+ expect(escapeRegExp('{{{')).toBe('\\{\\{\\{');
226
+ expect(escapeRegExp('')).toBe('');
227
+ expect(escapeRegExp('https://example.com')).toBe('https://example\\.com');
228
+ });
229
+ });
@@ -1,7 +1,7 @@
1
1
  import BookReader from '@/src/BookReader.js';
2
- import '@/src/plugins/plugin.mobile_nav.js';
3
- import { marshallSearchResults } from '@/src/plugins/search/plugin.search.js';
2
+ import '@/src/plugins/search/plugin.search.js';
4
3
  import { deepCopy } from '../../utils.js';
4
+ import { DUMMY_RESULTS } from './utils.js';
5
5
 
6
6
  jest.mock('@/src/plugins/search/view.js');
7
7
 
@@ -14,28 +14,6 @@ const triggeredEvents = () => {
14
14
  });
15
15
  };
16
16
 
17
- const DUMMY_RESULTS = {
18
- ia: "adventuresofoli00dick",
19
- q: "child",
20
- indexed: true,
21
- page_count: 644,
22
- body_length: 666,
23
- leaf0_missing: false,
24
- matches: [{
25
- text: 'For a long; time after it was ushered into this world of sorrow and trouble, by the parish surgeon, it remained a matter of considerable doubt wliether the {{{child}}} Avould survi^ e to bear any name at all; in which case it is somewhat more than probable that these memoirs would never have appeared; or, if they had, that being comprised within a couple of pages, they would have possessed the inestimable meiit of being the most concise and faithful specimen of biography, extant in the literature of any age or country.',
26
- par: [{
27
- boxes: [{r: 1221, b: 2121, t: 2075, page: 37, l: 1107}],
28
- b: 2535,
29
- t: 1942,
30
- page_width: 1790,
31
- r: 1598,
32
- l: 50,
33
- page_height: 2940,
34
- page: 37
35
- }]
36
- }]
37
- };
38
-
39
17
  beforeEach(() => {
40
18
  $.ajax = jest.fn().mockImplementation(() => {
41
19
  // return from:
@@ -165,18 +143,3 @@ describe('Plugin: Search', () => {
165
143
  expect(triggeredEvents()).toContain(`${namespace}SearchCallbackEmpty`);
166
144
  });
167
145
  });
168
-
169
- describe('marshallSearchResults', () => {
170
- test('Adds match index', () => {
171
- const results = deepCopy(DUMMY_RESULTS);
172
- marshallSearchResults(results, x => x.toString());
173
- expect(results.matches[0]).toHaveProperty('matchIndex', 0);
174
- expect(results.matches[0].par[0].boxes[0]).toHaveProperty('matchIndex', 0);
175
- });
176
-
177
- test('Adds display page number', () => {
178
- const results = deepCopy(DUMMY_RESULTS);
179
- marshallSearchResults(results, x => `n${x}`);
180
- expect(results.matches[0]).toHaveProperty('displayPageNumber', 'n37');
181
- });
182
- });
@@ -2,6 +2,7 @@
2
2
  import BookReader from '@/src/BookReader.js';
3
3
  import '@/src/plugins/plugin.mobile_nav.js';
4
4
  import '@/src/plugins/search/plugin.search.js';
5
+ import { marshallSearchResults } from '@/src/plugins/search/utils.js';
5
6
  import '@/src/plugins/search/view.js';
6
7
 
7
8
  let br;
@@ -27,6 +28,8 @@ const results = {
27
28
  }]
28
29
  }]
29
30
  };
31
+
32
+ marshallSearchResults(results, () => '', '{{{', '}}}');
30
33
  const resultWithScript = {
31
34
  ia: "adventuresofoli00dick",
32
35
  q: "child",
@@ -48,6 +51,8 @@ const resultWithScript = {
48
51
  }]
49
52
  }]
50
53
  };
54
+
55
+ marshallSearchResults(resultWithScript, () => '', '{{{', '}}}');
51
56
  beforeEach(() => {
52
57
  $.ajax = jest.fn().mockImplementation(() => {
53
58
  // return from:
@@ -0,0 +1,25 @@
1
+ import { marshallSearchResults } from "@/src/plugins/search/utils";
2
+
3
+ export const DUMMY_RESULTS = {
4
+ ia: "adventuresofoli00dick",
5
+ q: "child",
6
+ indexed: true,
7
+ page_count: 644,
8
+ body_length: 666,
9
+ leaf0_missing: false,
10
+ matches: [{
11
+ text: 'For a long; time after it was ushered into this world of sorrow and trouble, by the parish surgeon, it remained a matter of considerable doubt wliether the {{{child}}} Avould survi^ e to bear any name at all; in which case it is somewhat more than probable that these memoirs would never have appeared; or, if they had, that being comprised within a couple of pages, they would have possessed the inestimable meiit of being the most concise and faithful specimen of biography, extant in the literature of any age or country.',
12
+ par: [{
13
+ boxes: [{r: 1221, b: 2121, t: 2075, page: 37, l: 1107}],
14
+ b: 2535,
15
+ t: 1942,
16
+ page_width: 1790,
17
+ r: 1598,
18
+ l: 50,
19
+ page_height: 2940,
20
+ page: 37
21
+ }]
22
+ }]
23
+ };
24
+
25
+ marshallSearchResults(DUMMY_RESULTS, () => '', '{{{', '}}}');
@@ -0,0 +1,29 @@
1
+ import { marshallSearchResults, renderMatch } from '@/src/plugins/search/utils.js';
2
+ import { deepCopy } from '@/tests/jest/utils.js';
3
+ import { DUMMY_RESULTS } from './utils.js';
4
+
5
+ describe('renderMatch', () => {
6
+ test('Supports custom pre/post tags', () => {
7
+ const matchText = DUMMY_RESULTS.matches[0].text
8
+ .replace(/\{\{\{/g, '<IA_FTS_MATCH>')
9
+ .replace(/\}\}\}/g, '</IA_FTS_MATCH>');
10
+ const html = renderMatch(matchText, '<IA_FTS_MATCH>', '</IA_FTS_MATCH>');
11
+ expect(html).toContain('<mark>');
12
+ expect(html).toContain('</mark>');
13
+ });
14
+ });
15
+
16
+ describe('marshallSearchResults', () => {
17
+ test('Adds match index', () => {
18
+ const results = deepCopy(DUMMY_RESULTS);
19
+ marshallSearchResults(results, x => x.toString(), '{{{', '}}}');
20
+ expect(results.matches[0]).toHaveProperty('matchIndex', 0);
21
+ expect(results.matches[0].par[0].boxes[0]).toHaveProperty('matchIndex', 0);
22
+ });
23
+
24
+ test('Adds display page number', () => {
25
+ const results = deepCopy(DUMMY_RESULTS);
26
+ marshallSearchResults(results, x => `n${x}`, '{{{', '}}}');
27
+ expect(results.matches[0]).toHaveProperty('displayPageNumber', 'n37');
28
+ });
29
+ });