@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.
- package/.github/workflows/node.js.yml +1 -0
- package/BookReader/BookReader.css +45 -58
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.LICENSE.txt +2 -0
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/ia-bookreader-bundle.js +95 -95
- package/BookReader/ia-bookreader-bundle.js.LICENSE.txt +2 -0
- package/BookReader/ia-bookreader-bundle.js.map +1 -1
- package/BookReader/icons/1up.svg +1 -1
- package/BookReader/icons/2up.svg +1 -1
- package/BookReader/icons/advance.svg +1 -1
- package/BookReader/icons/chevron-right.svg +1 -1
- package/BookReader/icons/close-circle-dark.svg +1 -1
- package/BookReader/icons/close-circle.svg +1 -1
- package/BookReader/icons/fullscreen.svg +1 -1
- package/BookReader/icons/fullscreen_exit.svg +1 -1
- package/BookReader/icons/hamburger.svg +1 -1
- package/BookReader/icons/left-arrow.svg +1 -1
- package/BookReader/icons/magnify-minus.svg +1 -1
- package/BookReader/icons/magnify-plus.svg +1 -1
- package/BookReader/icons/magnify.svg +1 -1
- package/BookReader/icons/pause.svg +1 -1
- package/BookReader/icons/play.svg +1 -1
- package/BookReader/icons/playback-speed.svg +1 -1
- package/BookReader/icons/read-aloud.svg +1 -1
- package/BookReader/icons/review.svg +1 -1
- package/BookReader/icons/thumbnails.svg +1 -1
- package/BookReader/icons/voice.svg +1 -1
- package/BookReader/icons/volume-full.svg +1 -1
- package/BookReader/images/BRicons.svg +3 -3
- package/BookReader/images/books_graphic.svg +1 -1
- package/BookReader/images/icon_book.svg +1 -1
- package/BookReader/images/icon_bookmark.svg +1 -1
- package/BookReader/images/icon_gear.svg +1 -1
- package/BookReader/images/icon_hamburger.svg +1 -1
- package/BookReader/images/icon_home.svg +1 -1
- package/BookReader/images/icon_info.svg +1 -1
- package/BookReader/images/icon_one_page.svg +1 -1
- package/BookReader/images/icon_pause.svg +1 -1
- package/BookReader/images/icon_play.svg +1 -1
- package/BookReader/images/icon_search_button.svg +1 -1
- package/BookReader/images/icon_share.svg +1 -1
- package/BookReader/images/icon_skip-ahead.svg +1 -1
- package/BookReader/images/icon_skip-back.svg +1 -1
- package/BookReader/images/icon_speaker.svg +1 -1
- package/BookReader/images/icon_speaker_open.svg +1 -1
- package/BookReader/images/icon_thumbnails.svg +1 -1
- package/BookReader/images/icon_toc.svg +1 -1
- package/BookReader/images/icon_two_pages.svg +1 -1
- package/BookReader/images/marker_chap-off.svg +1 -1
- package/BookReader/images/marker_chap-on.svg +1 -1
- package/BookReader/images/marker_srch-on.svg +1 -1
- package/BookReader/jquery-3.js +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
- package/BookReader/plugins/plugin.autoplay.js +1 -1
- package/BookReader/plugins/plugin.autoplay.js.map +1 -1
- package/BookReader/plugins/plugin.chapters.js +2 -1
- package/BookReader/plugins/plugin.chapters.js.LICENSE.txt +1 -0
- package/BookReader/plugins/plugin.chapters.js.map +1 -1
- package/BookReader/plugins/plugin.iframe.js.map +1 -1
- package/BookReader/plugins/plugin.mobile_nav.js +1 -1
- package/BookReader/plugins/plugin.mobile_nav.js.map +1 -1
- package/BookReader/plugins/plugin.resume.js +1 -1
- package/BookReader/plugins/plugin.resume.js.map +1 -1
- package/BookReader/plugins/plugin.search.js +2 -1
- package/BookReader/plugins/plugin.search.js.LICENSE.txt +1 -0
- package/BookReader/plugins/plugin.search.js.map +1 -1
- package/BookReader/plugins/plugin.text_selection.js +2 -1
- package/BookReader/plugins/plugin.text_selection.js.LICENSE.txt +1 -0
- package/BookReader/plugins/plugin.text_selection.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.LICENSE.txt +2 -0
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReader/plugins/plugin.url.js +1 -1
- package/BookReader/plugins/plugin.url.js.map +1 -1
- package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
- package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
- package/BookReader/webcomponents-bundle.js +1 -1
- package/BookReader/webcomponents-bundle.js.map +1 -1
- package/CHANGELOG.md +5 -0
- package/babel.config.js +5 -4
- package/netlify.toml +4 -0
- package/package.json +15 -15
- package/src/BookNavigator/search/search-results.js +1 -7
- package/src/BookReader/utils.js +10 -0
- package/src/css/_BRnav.scss +2 -2
- package/src/css/_BRsearch.scss +38 -10
- package/src/plugins/search/plugin.search.js +14 -21
- package/src/plugins/search/utils.js +43 -0
- package/src/plugins/search/view.js +11 -24
- package/tests/e2e/helpers/desktopSearch.js +3 -3
- package/tests/jest/BookNavigator/search/search-results.test.js +6 -1
- package/tests/jest/BookReader/utils.test.js +12 -0
- package/tests/jest/plugins/search/plugin.search.test.js +2 -39
- package/tests/jest/plugins/search/plugin.search.view.test.js +5 -0
- package/tests/jest/plugins/search/utils.js +25 -0
- package/tests/jest/plugins/search/utils.test.js +29 -0
package/CHANGELOG.md
CHANGED
package/babel.config.js
CHANGED
@@ -11,9 +11,10 @@ module.exports = {
|
|
11
11
|
]
|
12
12
|
],
|
13
13
|
plugins: [
|
14
|
-
["@babel/plugin-proposal-decorators", {
|
15
|
-
|
16
|
-
|
17
|
-
|
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
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@internetarchive/bookreader",
|
3
|
-
"version": "5.0.0-
|
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.
|
44
|
+
"@babel/core": "7.22.5",
|
45
45
|
"@babel/eslint-parser": "7.21.8",
|
46
|
-
"@babel/plugin-proposal-class-properties": "7.
|
47
|
-
"@babel/plugin-proposal-decorators": "7.
|
48
|
-
"@babel/preset-env": "7.
|
49
|
-
"@open-wc/testing-helpers": "^2.
|
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": "
|
52
|
+
"babel-loader": "9.1.2",
|
53
53
|
"codecov": "^3.8.3",
|
54
54
|
"concurrently": "7.4.0",
|
55
|
-
"core-js": "3.
|
56
|
-
"cpx2": "4.2.
|
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.
|
74
|
-
"sass": "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": "
|
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.
|
81
|
-
"webpack-cli": "
|
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>
|
package/src/BookReader/utils.js
CHANGED
@@ -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
|
+
}
|
package/src/css/_BRnav.scss
CHANGED
package/src/css/_BRsearch.scss
CHANGED
@@ -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:
|
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
|
-
|
35
|
+
content: "";
|
36
|
+
bottom: -9px;
|
17
37
|
left: 50%;
|
38
|
+
margin-left: -1px;
|
18
39
|
transform: translateX(-50%);
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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 =
|
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(
|
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
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
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
|
-
<
|
263
|
-
<
|
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('
|
33
|
-
await t.expect(nav.desktop.searchPin.child('.BRquery').child('
|
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('
|
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.
|
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
|
+
});
|