@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.
- 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
|
+
});
|