Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions src/common/components/view-popup/find-popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import IconChevronUp from '../../../../res/icons/20/chevron-up.svg';
import IconChevronDown from '../../../../res/icons/20/chevron-down.svg';
import IconClose from '../../../../res/icons/20/x.svg';
import { getCodeCombination, getKeyCombination } from '../../lib/utilities';
import { compileFindRegExp } from '../../lib/find-pattern';

function FindPopup({ params, onChange, onFindNext, onFindPrevious, onAddAnnotation, tools }) {
const { l10n } = useLocalization();
Expand All @@ -18,15 +19,23 @@ function FindPopup({ params, onChange, onFindNext, onFindPrevious, onAddAnnotati

currentParamsRef.current = params;

let invalid = !!(params.useRegex && query && !compileFindRegExp(query));

const debounceInputChange = useCallback(debounce(value => {
if (!inputRef.current) {
return;
}
let query = inputRef.current.value;
if (query !== currentParamsRef.current.query && !(query.length === 1 && RegExp(/^\p{Script=Latin}/, 'u').test(query))) {
if (currentParamsRef.current.useRegex && !compileFindRegExp(query)) {
// Record the query but don't search until the pattern is a valid regular expression
onChange({ ...currentParamsRef.current, query, active: false, result: null });
return;
}
onChange({ ...currentParamsRef.current, query, active: true, result: null });
}
}, DEBOUNCE_FIND_POPUP_INPUT), [onChange]);
// Wait longer in regex mode -- half-typed patterns can be very broad
}, params.useRegex ? DEBOUNCE_FIND_POPUP_INPUT * 3 : DEBOUNCE_FIND_POPUP_INPUT), [onChange, params.useRegex]);

useLayoutEffect(() => {
if (params.popupOpen) {
Expand Down Expand Up @@ -56,6 +65,9 @@ function FindPopup({ params, onChange, onFindNext, onFindPrevious, onAddAnnotati
let key = getKeyCombination(event);
let code = getCodeCombination(event);
if (key === 'Enter') {
if (invalid) {
return;
}
if (params.active) {
onFindNext();
}
Expand All @@ -64,6 +76,9 @@ function FindPopup({ params, onChange, onFindNext, onFindPrevious, onAddAnnotati
}
}
else if (key === 'Shift-Enter') {
if (invalid) {
return;
}
if (params.active) {
onFindPrevious();
}
Expand Down Expand Up @@ -110,6 +125,18 @@ function FindPopup({ params, onChange, onFindNext, onFindPrevious, onAddAnnotati
onChange({ ...params, entireWord: event.currentTarget.checked, result: null });
}

function handleUseRegexChange(event) {
let useRegex = event.currentTarget.checked;
if (useRegex && query && !compileFindRegExp(query)) {
// Deactivate the search until the pattern is a valid regular expression
onChange({ ...params, useRegex, active: false, result: null });
return;
}
// Reactivate the search if it was deactivated because of an invalid pattern
let wasInvalid = params.useRegex && !!params.query && !compileFindRegExp(params.query);
onChange({ ...params, useRegex, active: params.active || wasInvalid, result: null });
}

return (
<div className="find-popup" role="application">
<div className="row input">
Expand All @@ -118,7 +145,7 @@ function FindPopup({ params, onChange, onFindNext, onFindPrevious, onAddAnnotati
<input
ref={inputRef}
type="text"
className="toolbar-text-input"
className={cx('toolbar-text-input', { invalid })}
value={query !== null ? query : params.query}
tabIndex="-1"
data-tabstop={1}
Expand Down Expand Up @@ -185,6 +212,16 @@ function FindPopup({ params, onChange, onFindNext, onFindPrevious, onAddAnnotati
/>
<label htmlFor="entire-word">{l10n.getString('reader-whole-words')}</label>
</div>
<div className="option">
<input
id="use-regex"
type="checkbox"
tabIndex="-1"
checked={params.useRegex}
onChange={handleUseRegexChange}
/>
<label htmlFor="use-regex">{l10n.getString('reader-use-regex')}</label>
</div>
</div>
{params.result &&
<div className="row result">
Expand Down
3 changes: 3 additions & 0 deletions src/common/defines.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export const FIND_RESULT_COLOR_ALL_LIGHT = 'rgba(180,0,170,0.3)';
export const FIND_RESULT_COLOR_CURRENT_LIGHT = 'rgba(0,100,0,0.3)';
export const FIND_RESULT_COLOR_ALL_DARK = 'rgba(180,0,170,0.6)';
export const FIND_RESULT_COLOR_CURRENT_DARK = 'rgba(0,100,0,0.6)';
// Stop collecting find matches beyond this total -- highlighting and snippet
// generation for unbounded match counts (e.g. a regex like '.') freezes the UI
export const FIND_MAX_TOTAL_MATCHES = 5000;

export const ANNOTATION_POSITION_MAX_SIZE = 65000;

Expand Down
10 changes: 10 additions & 0 deletions src/common/lib/find-pattern.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Find patterns use the strict Unicode-mode regex grammar, both for validation
// in the find popup and for matching in the view-specific find implementations
export function compileFindRegExp(source, flags = '') {
try {
return new RegExp(source, flags + 'u');
}
catch (e) {
return null;
}
}
2 changes: 2 additions & 0 deletions src/common/reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ class Reader {
highlightAll: true,
caseSensitive: false,
entireWord: false,
useRegex: false,
index: null,
result: null,
},
Expand All @@ -260,6 +261,7 @@ class Reader {
highlightAll: true,
caseSensitive: false,
entireWord: false,
useRegex: false,
index: null,
result: null
},
Expand Down
17 changes: 13 additions & 4 deletions src/common/stylesheets/components/_view-popup.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
.row.input {
gap: 8px;

.toolbar-text-input.invalid {
border-color: var(--accent-red);

&:focus {
box-shadow: 0 0 0 var(--width-focus-border) var(--accent-red);
}
}

.input-box {
width: 100%;
display: flex;
Expand Down Expand Up @@ -56,10 +64,11 @@
}

.row.options {
height: 16px;
display: flex;
align-items: flex-start;
gap: 12px;
display: grid;
grid-template-columns: auto auto;
justify-content: start;
align-items: center;
gap: 6px 12px;

.option {
display: flex;
Expand Down
1 change: 1 addition & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export type FindState = {
highlightAll: boolean;
caseSensitive: boolean;
entireWord: boolean;
useRegex: boolean;
// For mobile app to focus specific result
index: number | null,
result: {
Expand Down
3 changes: 3 additions & 0 deletions src/common/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class View {
highlightAll: true,
caseSensitive: false,
entireWord: false,
useRegex: false,
index: null,
result: null,
// View can be created with an active search
Expand Down Expand Up @@ -165,6 +166,7 @@ class View {
* @param {String} [params.highlightAll]
* @param {String} [params.caseSensitive]
* @param {String} [params.entireWord]
* @param {String} [params.useRegex]
* @param {String} [params.index] Focus specific result
*/
find(params) {
Expand All @@ -182,6 +184,7 @@ class View {
highlightAll: true,
caseSensitive: false,
entireWord: false,
useRegex: false,
index: null,
result: null,
...(params || {})
Expand Down
4 changes: 3 additions & 1 deletion src/dom/common/lib/find/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ class DefaultFindProcessor implements FindProcessor {
this.findState.query,
{
caseSensitive: this.findState.caseSensitive,
entireWord: this.findState.entireWord
entireWord: this.findState.entireWord,
useRegex: this.findState.useRegex
}
);
for (let internalOutputRange of ranges) {
Expand Down Expand Up @@ -231,6 +232,7 @@ class DefaultFindProcessor implements FindProcessor {
options: {
caseSensitive: boolean,
entireWord: boolean,
useRegex?: boolean,
}
): Promise<InternalOutputRange[]> {
if (this._worker) {
Expand Down
30 changes: 27 additions & 3 deletions src/dom/common/lib/find/worker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { InternalCharDataRange, InternalOutputRange, InternalSearchContext } from "./internal-types";
import { FIND_MAX_TOTAL_MATCHES } from "../../../../common/defines";
import { compileFindRegExp } from "../../../../common/lib/find-pattern";

onmessage = async (event) => {
let { context, term, options } = event.data;
Expand All @@ -11,6 +13,7 @@ export function executeSearch(
options: {
caseSensitive: boolean,
entireWord: boolean,
useRegex?: boolean,
}
): InternalOutputRange[] {
if (!term) {
Expand All @@ -21,14 +24,32 @@ export function executeSearch(
let ranges: InternalOutputRange[] = [];

// https://stackoverflow.com/a/6969486
let termRe = normalize(term).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
let termRe = options.useRegex
? normalize(term)
: normalize(term).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
if (options.entireWord) {
termRe = '\\b' + termRe + '\\b';
termRe = '\\b(?:' + termRe + ')\\b';
}
let flags = 'g' + (options.caseSensitive ? '' : 'i');
let re;
if (options.useRegex) {
re = compileFindRegExp(termRe, flags);
if (!re) {
return [];
}
}
else {
re = new RegExp(termRe, flags);
}
let re = new RegExp(termRe, 'g' + (options.caseSensitive ? '' : 'i'));
let matches;
while ((matches = re.exec(text))) {
let [match] = matches;
if (!match.length) {
// A pattern like 'a*' can match an empty string, which would
// otherwise loop forever
re.lastIndex++;
continue;
}
let { charDataID: startCharDataID, start: startOffset } = binarySearch(internalCharDataRanges, matches.index)!;
let { charDataID: endCharDataID, start: endOffset } = binarySearch(internalCharDataRanges, matches.index + match.length)!;
ranges.push({
Expand All @@ -37,6 +58,9 @@ export function executeSearch(
endCharDataID,
endIndex: matches.index + match.length - endOffset,
});
if (ranges.length >= FIND_MAX_TOTAL_MATCHES) {
break;
}
}

return ranges;
Expand Down
1 change: 1 addition & 0 deletions src/dom/epub/epub-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,7 @@ class EPUBView extends DOMView<EPUBViewState, EPUBViewData> {
|| previousState.query !== state.query
|| previousState.caseSensitive !== state.caseSensitive
|| previousState.entireWord !== state.entireWord
|| previousState.useRegex !== state.useRegex
|| previousState.active !== state.active) {
console.log('Initiating new search', state);
this._find?.cancel();
Expand Down
1 change: 1 addition & 0 deletions src/dom/snapshot/snapshot-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ class SnapshotView extends DOMView<SnapshotViewState, SnapshotViewData> {
|| previousState.query !== state.query
|| previousState.caseSensitive !== state.caseSensitive
|| previousState.entireWord !== state.entireWord
|| previousState.useRegex !== state.useRegex
|| previousState.active !== state.active) {
console.log('Initiating new search', state);
this._find = new DefaultFindProcessor({
Expand Down
Loading