@internetarchive/bookreader 5.0.0-63 → 5.0.0-64

Sign up to get free protection for your applications and to get access to all the features.
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
+ });