aboutsummaryrefslogtreecommitdiff
path: root/ext/js/language/text-scanner.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/language/text-scanner.js')
-rw-r--r--ext/js/language/text-scanner.js982
1 files changed, 982 insertions, 0 deletions
diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js
new file mode 100644
index 00000000..7672b69d
--- /dev/null
+++ b/ext/js/language/text-scanner.js
@@ -0,0 +1,982 @@
+/*
+ * Copyright (C) 2019-2021 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/* global
+ * DocumentUtil
+ * api
+ */
+
+class TextScanner extends EventDispatcher {
+ constructor({
+ node,
+ documentUtil,
+ getSearchContext,
+ ignoreElements=null,
+ ignorePoint=null,
+ searchTerms=false,
+ searchKanji=false,
+ searchOnClick=false,
+ searchOnClickOnly=false
+ }) {
+ super();
+ this._node = node;
+ this._documentUtil = documentUtil;
+ this._getSearchContext = getSearchContext;
+ this._ignoreElements = ignoreElements;
+ this._ignorePoint = ignorePoint;
+ this._searchTerms = searchTerms;
+ this._searchKanji = searchKanji;
+ this._searchOnClick = searchOnClick;
+ this._searchOnClickOnly = searchOnClickOnly;
+
+ this._isPrepared = false;
+ this._includeSelector = null;
+ this._excludeSelector = null;
+
+ this._inputInfoCurrent = null;
+ this._scanTimerPromise = null;
+ this._textSourceCurrent = null;
+ this._textSourceCurrentSelected = false;
+ this._pendingLookup = false;
+
+ this._deepContentScan = false;
+ this._selectText = false;
+ this._delay = 0;
+ this._touchInputEnabled = false;
+ this._pointerEventsEnabled = false;
+ this._scanLength = 1;
+ this._layoutAwareScan = false;
+ this._preventMiddleMouse = false;
+ this._sentenceScanExtent = 0;
+ this._sentenceTerminatorMap = new Map();
+ this._sentenceForwardQuoteMap = new Map();
+ this._sentenceBackwardQuoteMap = new Map();
+ this._inputs = [];
+
+ this._enabled = false;
+ this._enabledValue = false;
+ this._eventListeners = new EventListenerCollection();
+
+ this._primaryTouchIdentifier = null;
+ this._preventNextContextMenu = false;
+ this._preventNextMouseDown = false;
+ this._preventNextClick = false;
+ this._preventScroll = false;
+ this._penPointerPressed = false;
+ this._penPointerReleased = false;
+ this._pointerIdTypeMap = new Map();
+
+ this._canClearSelection = true;
+ }
+
+ get canClearSelection() {
+ return this._canClearSelection;
+ }
+
+ set canClearSelection(value) {
+ this._canClearSelection = value;
+ }
+
+ get includeSelector() {
+ return this._includeSelector;
+ }
+
+ set includeSelector(value) {
+ this._includeSelector = value;
+ }
+
+ get excludeSelector() {
+ return this._excludeSelector;
+ }
+
+ set excludeSelector(value) {
+ this._excludeSelector = value;
+ }
+
+ prepare() {
+ this._isPrepared = true;
+ this.setEnabled(this._enabled);
+ }
+
+ setEnabled(enabled) {
+ this._enabled = enabled;
+
+ const value = enabled && this._isPrepared;
+ if (this._enabledValue === value) { return; }
+
+ this._eventListeners.removeAllEventListeners();
+ this._primaryTouchIdentifier = null;
+ this._preventNextContextMenu = false;
+ this._preventNextMouseDown = false;
+ this._preventNextClick = false;
+ this._preventScroll = false;
+ this._penPointerPressed = false;
+ this._penPointerReleased = false;
+ this._pointerIdTypeMap.clear();
+
+ this._enabledValue = value;
+
+ if (value) {
+ this._hookEvents();
+ } else {
+ this.clearSelection(true);
+ }
+ }
+
+ setOptions({
+ inputs,
+ deepContentScan,
+ selectText,
+ delay,
+ touchInputEnabled,
+ pointerEventsEnabled,
+ scanLength,
+ layoutAwareScan,
+ preventMiddleMouse,
+ sentenceParsingOptions
+ }) {
+ if (Array.isArray(inputs)) {
+ this._inputs = inputs.map(({
+ include,
+ exclude,
+ types,
+ options: {
+ searchTerms,
+ searchKanji,
+ scanOnTouchMove,
+ scanOnPenHover,
+ scanOnPenPress,
+ scanOnPenRelease,
+ preventTouchScrolling
+ }
+ }) => ({
+ include: this._getInputArray(include),
+ exclude: this._getInputArray(exclude),
+ types: this._getInputTypeSet(types),
+ options: {
+ searchTerms,
+ searchKanji,
+ scanOnTouchMove,
+ scanOnPenHover,
+ scanOnPenPress,
+ scanOnPenRelease,
+ preventTouchScrolling
+ }
+ }));
+ }
+ if (typeof deepContentScan === 'boolean') {
+ this._deepContentScan = deepContentScan;
+ }
+ if (typeof selectText === 'boolean') {
+ this._selectText = selectText;
+ }
+ if (typeof delay === 'number') {
+ this._delay = delay;
+ }
+ if (typeof touchInputEnabled === 'boolean') {
+ this._touchInputEnabled = touchInputEnabled;
+ }
+ if (typeof pointerEventsEnabled === 'boolean') {
+ this._pointerEventsEnabled = pointerEventsEnabled;
+ }
+ if (typeof scanLength === 'number') {
+ this._scanLength = scanLength;
+ }
+ if (typeof layoutAwareScan === 'boolean') {
+ this._layoutAwareScan = layoutAwareScan;
+ }
+ if (typeof preventMiddleMouse === 'boolean') {
+ this._preventMiddleMouse = preventMiddleMouse;
+ }
+ if (typeof sentenceParsingOptions === 'object' && sentenceParsingOptions !== null) {
+ const {scanExtent, enableTerminationCharacters, terminationCharacters} = sentenceParsingOptions;
+ const hasTerminationCharacters = (typeof terminationCharacters === 'object' && Array.isArray(terminationCharacters));
+ if (typeof scanExtent === 'number') {
+ this._sentenceScanExtent = sentenceParsingOptions.scanExtent;
+ }
+ if (typeof enableTerminationCharacters === 'boolean' || hasTerminationCharacters) {
+ const sentenceTerminatorMap = this._sentenceTerminatorMap;
+ const sentenceForwardQuoteMap = this._sentenceForwardQuoteMap;
+ const sentenceBackwardQuoteMap = this._sentenceBackwardQuoteMap;
+ sentenceTerminatorMap.clear();
+ sentenceForwardQuoteMap.clear();
+ sentenceBackwardQuoteMap.clear();
+ if (enableTerminationCharacters !== false && hasTerminationCharacters) {
+ for (const {enabled, character1, character2, includeCharacterAtStart, includeCharacterAtEnd} of terminationCharacters) {
+ if (!enabled) { continue; }
+ if (character2 === null) {
+ sentenceTerminatorMap.set(character1, [includeCharacterAtStart, includeCharacterAtEnd]);
+ } else {
+ sentenceForwardQuoteMap.set(character1, [character2, includeCharacterAtStart]);
+ sentenceBackwardQuoteMap.set(character2, [character1, includeCharacterAtEnd]);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ getTextSourceContent(textSource, length, layoutAwareScan) {
+ const clonedTextSource = textSource.clone();
+
+ clonedTextSource.setEndOffset(length, layoutAwareScan);
+
+ const includeSelector = this._includeSelector;
+ const excludeSelector = this._excludeSelector;
+ if (includeSelector !== null || excludeSelector !== null) {
+ this._constrainTextSource(clonedTextSource, includeSelector, excludeSelector, layoutAwareScan);
+ }
+
+ return clonedTextSource.text();
+ }
+
+ hasSelection() {
+ return (this._textSourceCurrent !== null);
+ }
+
+ clearSelection(passive) {
+ if (!this._canClearSelection) { return; }
+ if (this._textSourceCurrent !== null) {
+ if (this._textSourceCurrentSelected) {
+ this._textSourceCurrent.deselect();
+ }
+ this._textSourceCurrent = null;
+ this._textSourceCurrentSelected = false;
+ this._inputInfoCurrent = null;
+ }
+ this.trigger('clearSelection', {passive});
+ }
+
+ getCurrentTextSource() {
+ return this._textSourceCurrent;
+ }
+
+ setCurrentTextSource(textSource) {
+ this._textSourceCurrent = textSource;
+ if (this._selectText) {
+ this._textSourceCurrent.select();
+ this._textSourceCurrentSelected = true;
+ } else {
+ this._textSourceCurrentSelected = false;
+ }
+ }
+
+ async searchLast() {
+ if (this._textSourceCurrent !== null && this._inputInfoCurrent !== null) {
+ await this._search(this._textSourceCurrent, this._searchTerms, this._searchKanji, this._inputInfoCurrent);
+ return true;
+ }
+ return false;
+ }
+
+ async search(textSource, inputDetail) {
+ const inputInfo = this._createInputInfo(null, 'script', 'script', true, [], [], inputDetail);
+ return await this._search(textSource, this._searchTerms, this._searchKanji, inputInfo);
+ }
+
+ // Private
+
+ _createOptionsContextForInput(baseOptionsContext, inputInfo) {
+ const optionsContext = clone(baseOptionsContext);
+ const {modifiers, modifierKeys} = inputInfo;
+ optionsContext.modifiers = [...modifiers];
+ optionsContext.modifierKeys = [...modifierKeys];
+ return optionsContext;
+ }
+
+ async _search(textSource, searchTerms, searchKanji, inputInfo) {
+ let definitions = null;
+ let sentence = null;
+ let type = null;
+ let error = null;
+ let searched = false;
+ let optionsContext = null;
+ let detail = null;
+
+ try {
+ if (this._textSourceCurrent !== null && this._textSourceCurrent.hasSameStart(textSource)) {
+ return null;
+ }
+
+ ({optionsContext, detail} = await this._getSearchContext());
+ optionsContext = this._createOptionsContextForInput(optionsContext, inputInfo);
+
+ searched = true;
+
+ const result = await this._findDefinitions(textSource, searchTerms, searchKanji, optionsContext);
+ if (result !== null) {
+ ({definitions, sentence, type} = result);
+ this._inputInfoCurrent = inputInfo;
+ this.setCurrentTextSource(textSource);
+ }
+ } catch (e) {
+ error = e;
+ }
+
+ if (!searched) { return null; }
+
+ const results = {
+ textScanner: this,
+ type,
+ definitions,
+ sentence,
+ inputInfo,
+ textSource,
+ optionsContext,
+ detail,
+ error
+ };
+ this.trigger('searched', results);
+ return results;
+ }
+
+ _onMouseOver(e) {
+ if (this._ignoreElements !== null && this._ignoreElements().includes(e.target)) {
+ this._scanTimerClear();
+ }
+ }
+
+ _onMouseMove(e) {
+ this._scanTimerClear();
+
+ const inputInfo = this._getMatchingInputGroupFromEvent('mouse', 'mouseMove', e);
+ if (inputInfo === null) { return; }
+
+ this._searchAtFromMouseMove(e.clientX, e.clientY, inputInfo);
+ }
+
+ _onMouseDown(e) {
+ if (this._preventNextMouseDown) {
+ this._preventNextMouseDown = false;
+ this._preventNextClick = true;
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+
+ switch (e.button) {
+ case 0: // Primary
+ this._scanTimerClear();
+ this.clearSelection(false);
+ break;
+ case 1: // Middle
+ if (this._preventMiddleMouse) {
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+ break;
+ }
+ }
+
+ _onMouseOut() {
+ this._scanTimerClear();
+ }
+
+ _onClick(e) {
+ if (this._searchOnClick) {
+ const modifiers = DocumentUtil.getActiveModifiersAndButtons(e);
+ const modifierKeys = DocumentUtil.getActiveModifiers(e);
+ const inputInfo = this._createInputInfo(null, 'mouse', 'click', false, modifiers, modifierKeys);
+ this._searchAt(e.clientX, e.clientY, inputInfo);
+ }
+
+ if (this._preventNextClick) {
+ this._preventNextClick = false;
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+ }
+
+ _onAuxClick() {
+ this._preventNextContextMenu = false;
+ }
+
+ _onContextMenu(e) {
+ if (this._preventNextContextMenu) {
+ this._preventNextContextMenu = false;
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+ }
+
+ _onTouchStart(e) {
+ if (this._primaryTouchIdentifier !== null || e.changedTouches.length === 0) {
+ return;
+ }
+
+ const {clientX, clientY, identifier} = e.changedTouches[0];
+ this._onPrimaryTouchStart(e, clientX, clientY, identifier);
+ }
+
+ _onPrimaryTouchStart(e, x, y, identifier) {
+ this._preventScroll = false;
+ this._preventNextContextMenu = false;
+ this._preventNextMouseDown = false;
+ this._preventNextClick = false;
+
+ if (DocumentUtil.isPointInSelection(x, y, window.getSelection())) {
+ return;
+ }
+
+ this._primaryTouchIdentifier = identifier;
+
+ this._searchAtFromTouchStart(e, x, y);
+ }
+
+ _onTouchEnd(e) {
+ if (
+ this._primaryTouchIdentifier === null ||
+ this._getTouch(e.changedTouches, this._primaryTouchIdentifier) === null
+ ) {
+ return;
+ }
+
+ this._onPrimaryTouchEnd();
+ }
+
+ _onPrimaryTouchEnd() {
+ this._primaryTouchIdentifier = null;
+ this._preventScroll = false;
+ this._preventNextClick = false;
+ // Don't revert context menu and mouse down prevention, since these events can occur after the touch has ended.
+ // I.e. this._preventNextContextMenu and this._preventNextMouseDown should not be assigned to false.
+ }
+
+ _onTouchCancel(e) {
+ this._onTouchEnd(e);
+ }
+
+ _onTouchMove(e) {
+ if (!this._preventScroll || !e.cancelable || this._primaryTouchIdentifier === null) {
+ return;
+ }
+
+ const primaryTouch = this._getTouch(e.changedTouches, this._primaryTouchIdentifier);
+ if (primaryTouch === null) {
+ return;
+ }
+
+ const inputInfo = this._getMatchingInputGroupFromEvent('touch', 'touchMove', e);
+ if (inputInfo === null) { return; }
+
+ if (inputInfo.input.options.scanOnTouchMove) {
+ this._searchAt(primaryTouch.clientX, primaryTouch.clientY, inputInfo);
+ }
+
+ e.preventDefault(); // Disable scroll
+ }
+
+ _onPointerOver(e) {
+ const {pointerType, pointerId, isPrimary} = e;
+ if (pointerType === 'pen') {
+ this._pointerIdTypeMap.set(pointerId, pointerType);
+ }
+
+ if (!isPrimary) { return; }
+ switch (pointerType) {
+ case 'mouse': return this._onMousePointerOver(e);
+ case 'touch': return this._onTouchPointerOver(e);
+ case 'pen': return this._onPenPointerOver(e);
+ }
+ }
+
+ _onPointerDown(e) {
+ if (!e.isPrimary) { return; }
+ switch (this._getPointerEventType(e)) {
+ case 'mouse': return this._onMousePointerDown(e);
+ case 'touch': return this._onTouchPointerDown(e);
+ case 'pen': return this._onPenPointerDown(e);
+ }
+ }
+
+ _onPointerMove(e) {
+ if (!e.isPrimary) { return; }
+ switch (this._getPointerEventType(e)) {
+ case 'mouse': return this._onMousePointerMove(e);
+ case 'touch': return this._onTouchPointerMove(e);
+ case 'pen': return this._onPenPointerMove(e);
+ }
+ }
+
+ _onPointerUp(e) {
+ if (!e.isPrimary) { return; }
+ switch (this._getPointerEventType(e)) {
+ case 'mouse': return this._onMousePointerUp(e);
+ case 'touch': return this._onTouchPointerUp(e);
+ case 'pen': return this._onPenPointerUp(e);
+ }
+ }
+
+ _onPointerCancel(e) {
+ this._pointerIdTypeMap.delete(e.pointerId);
+ if (!e.isPrimary) { return; }
+ switch (e.pointerType) {
+ case 'mouse': return this._onMousePointerCancel(e);
+ case 'touch': return this._onTouchPointerCancel(e);
+ case 'pen': return this._onPenPointerCancel(e);
+ }
+ }
+
+ _onPointerOut(e) {
+ this._pointerIdTypeMap.delete(e.pointerId);
+ if (!e.isPrimary) { return; }
+ switch (e.pointerType) {
+ case 'mouse': return this._onMousePointerOut(e);
+ case 'touch': return this._onTouchPointerOut(e);
+ case 'pen': return this._onPenPointerOut(e);
+ }
+ }
+
+ _onMousePointerOver(e) {
+ return this._onMouseOver(e);
+ }
+
+ _onMousePointerDown(e) {
+ return this._onMouseDown(e);
+ }
+
+ _onMousePointerMove(e) {
+ return this._onMouseMove(e);
+ }
+
+ _onMousePointerUp() {
+ // NOP
+ }
+
+ _onMousePointerCancel(e) {
+ return this._onMouseOut(e);
+ }
+
+ _onMousePointerOut(e) {
+ return this._onMouseOut(e);
+ }
+
+ _onTouchPointerOver() {
+ // NOP
+ }
+
+ _onTouchPointerDown(e) {
+ const {clientX, clientY, pointerId} = e;
+ return this._onPrimaryTouchStart(e, clientX, clientY, pointerId);
+ }
+
+ _onTouchPointerMove(e) {
+ if (!this._preventScroll || !e.cancelable) {
+ return;
+ }
+
+ const inputInfo = this._getMatchingInputGroupFromEvent('touch', 'touchMove', e);
+ if (inputInfo === null || !inputInfo.input.options.scanOnTouchMove) { return; }
+
+ this._searchAt(e.clientX, e.clientY, inputInfo);
+ }
+
+ _onTouchPointerUp() {
+ return this._onPrimaryTouchEnd();
+ }
+
+ _onTouchPointerCancel() {
+ return this._onPrimaryTouchEnd();
+ }
+
+ _onTouchPointerOut() {
+ // NOP
+ }
+
+ _onTouchMovePreventScroll(e) {
+ if (!this._preventScroll) { return; }
+
+ if (e.cancelable) {
+ e.preventDefault();
+ } else {
+ this._preventScroll = false;
+ }
+ }
+
+ _onPenPointerOver(e) {
+ this._penPointerPressed = false;
+ this._penPointerReleased = false;
+ this._searchAtFromPen(e, e.clientX, e.clientY, 'pointerOver', false);
+ }
+
+ _onPenPointerDown(e) {
+ this._penPointerPressed = true;
+ this._searchAtFromPen(e, e.clientX, e.clientY, 'pointerDown', true);
+ }
+
+ _onPenPointerMove(e) {
+ if (this._penPointerPressed && (!this._preventScroll || !e.cancelable)) { return; }
+ this._searchAtFromPen(e, e.clientX, e.clientY, 'pointerMove', true);
+ }
+
+ _onPenPointerUp() {
+ this._penPointerPressed = false;
+ this._penPointerReleased = true;
+ this._preventScroll = false;
+ }
+
+ _onPenPointerCancel(e) {
+ this._onPenPointerOut(e);
+ }
+
+ _onPenPointerOut() {
+ this._penPointerPressed = false;
+ this._penPointerReleased = false;
+ this._preventScroll = false;
+ this._preventNextContextMenu = false;
+ this._preventNextMouseDown = false;
+ this._preventNextClick = false;
+ }
+
+ async _scanTimerWait() {
+ const delay = this._delay;
+ const promise = promiseTimeout(delay, true);
+ this._scanTimerPromise = promise;
+ try {
+ return await promise;
+ } finally {
+ if (this._scanTimerPromise === promise) {
+ this._scanTimerPromise = null;
+ }
+ }
+ }
+
+ _scanTimerClear() {
+ if (this._scanTimerPromise !== null) {
+ this._scanTimerPromise.resolve(false);
+ this._scanTimerPromise = null;
+ }
+ }
+
+ _arePointerEventsSupported() {
+ return (this._pointerEventsEnabled && typeof PointerEvent !== 'undefined');
+ }
+
+ _hookEvents() {
+ let eventListenerInfos;
+ if (this._searchOnClickOnly) {
+ eventListenerInfos = this._getMouseClickOnlyEventListeners();
+ } else if (this._arePointerEventsSupported()) {
+ eventListenerInfos = this._getPointerEventListeners();
+ } else {
+ eventListenerInfos = this._getMouseEventListeners();
+ if (this._touchInputEnabled) {
+ eventListenerInfos.push(...this._getTouchEventListeners());
+ }
+ }
+
+ for (const args of eventListenerInfos) {
+ this._eventListeners.addEventListener(...args);
+ }
+ }
+
+ _getPointerEventListeners() {
+ return [
+ [this._node, 'pointerover', this._onPointerOver.bind(this)],
+ [this._node, 'pointerdown', this._onPointerDown.bind(this)],
+ [this._node, 'pointermove', this._onPointerMove.bind(this)],
+ [this._node, 'pointerup', this._onPointerUp.bind(this)],
+ [this._node, 'pointercancel', this._onPointerCancel.bind(this)],
+ [this._node, 'pointerout', this._onPointerOut.bind(this)],
+ [this._node, 'touchmove', this._onTouchMovePreventScroll.bind(this), {passive: false}],
+ [this._node, 'mousedown', this._onMouseDown.bind(this)],
+ [this._node, 'click', this._onClick.bind(this)],
+ [this._node, 'auxclick', this._onAuxClick.bind(this)]
+ ];
+ }
+
+ _getMouseEventListeners() {
+ return [
+ [this._node, 'mousedown', this._onMouseDown.bind(this)],
+ [this._node, 'mousemove', this._onMouseMove.bind(this)],
+ [this._node, 'mouseover', this._onMouseOver.bind(this)],
+ [this._node, 'mouseout', this._onMouseOut.bind(this)],
+ [this._node, 'click', this._onClick.bind(this)]
+ ];
+ }
+
+ _getMouseClickOnlyEventListeners() {
+ return [
+ [this._node, 'click', this._onClick.bind(this)]
+ ];
+ }
+ _getTouchEventListeners() {
+ return [
+ [this._node, 'auxclick', this._onAuxClick.bind(this)],
+ [this._node, 'touchstart', this._onTouchStart.bind(this)],
+ [this._node, 'touchend', this._onTouchEnd.bind(this)],
+ [this._node, 'touchcancel', this._onTouchCancel.bind(this)],
+ [this._node, 'touchmove', this._onTouchMove.bind(this), {passive: false}],
+ [this._node, 'contextmenu', this._onContextMenu.bind(this)]
+ ];
+ }
+
+ _getTouch(touchList, identifier) {
+ for (const touch of touchList) {
+ if (touch.identifier === identifier) {
+ return touch;
+ }
+ }
+ return null;
+ }
+
+ async _findDefinitions(textSource, searchTerms, searchKanji, optionsContext) {
+ if (textSource === null) {
+ return null;
+ }
+ if (searchTerms) {
+ const results = await this._findTerms(textSource, optionsContext);
+ if (results !== null) { return results; }
+ }
+ if (searchKanji) {
+ const results = await this._findKanji(textSource, optionsContext);
+ if (results !== null) { return results; }
+ }
+ return null;
+ }
+
+ async _findTerms(textSource, optionsContext) {
+ const scanLength = this._scanLength;
+ const sentenceScanExtent = this._sentenceScanExtent;
+ const sentenceTerminatorMap = this._sentenceTerminatorMap;
+ const sentenceForwardQuoteMap = this._sentenceForwardQuoteMap;
+ const sentenceBackwardQuoteMap = this._sentenceBackwardQuoteMap;
+ const layoutAwareScan = this._layoutAwareScan;
+ const searchText = this.getTextSourceContent(textSource, scanLength, layoutAwareScan);
+ if (searchText.length === 0) { return null; }
+
+ const {definitions, length} = await api.termsFind(searchText, {}, optionsContext);
+ if (definitions.length === 0) { return null; }
+
+ textSource.setEndOffset(length, layoutAwareScan);
+ const sentence = this._documentUtil.extractSentence(
+ textSource,
+ layoutAwareScan,
+ sentenceScanExtent,
+ sentenceTerminatorMap,
+ sentenceForwardQuoteMap,
+ sentenceBackwardQuoteMap
+ );
+
+ return {definitions, sentence, type: 'terms'};
+ }
+
+ async _findKanji(textSource, optionsContext) {
+ const sentenceScanExtent = this._sentenceScanExtent;
+ const sentenceTerminatorMap = this._sentenceTerminatorMap;
+ const sentenceForwardQuoteMap = this._sentenceForwardQuoteMap;
+ const sentenceBackwardQuoteMap = this._sentenceBackwardQuoteMap;
+ const layoutAwareScan = this._layoutAwareScan;
+ const searchText = this.getTextSourceContent(textSource, 1, layoutAwareScan);
+ if (searchText.length === 0) { return null; }
+
+ const definitions = await api.kanjiFind(searchText, optionsContext);
+ if (definitions.length === 0) { return null; }
+
+ textSource.setEndOffset(1, layoutAwareScan);
+ const sentence = this._documentUtil.extractSentence(
+ textSource,
+ layoutAwareScan,
+ sentenceScanExtent,
+ sentenceTerminatorMap,
+ sentenceForwardQuoteMap,
+ sentenceBackwardQuoteMap
+ );
+
+ return {definitions, sentence, type: 'kanji'};
+ }
+
+ async _searchAt(x, y, inputInfo) {
+ if (this._pendingLookup) { return; }
+
+ try {
+ const sourceInput = inputInfo.input;
+ let searchTerms = this._searchTerms;
+ let searchKanji = this._searchKanji;
+ if (sourceInput !== null) {
+ if (searchTerms && !sourceInput.options.searchTerms) { searchTerms = false; }
+ if (searchKanji && !sourceInput.options.searchKanji) { searchKanji = false; }
+ }
+
+ this._pendingLookup = true;
+ this._scanTimerClear();
+
+ if (typeof this._ignorePoint === 'function' && await this._ignorePoint(x, y)) {
+ return;
+ }
+
+ const textSource = this._documentUtil.getRangeFromPoint(x, y, this._deepContentScan);
+ try {
+ await this._search(textSource, searchTerms, searchKanji, inputInfo);
+ } finally {
+ if (textSource !== null) {
+ textSource.cleanup();
+ }
+ }
+ } catch (e) {
+ yomichan.logError(e);
+ } finally {
+ this._pendingLookup = false;
+ }
+ }
+
+ async _searchAtFromMouseMove(x, y, inputInfo) {
+ if (this._pendingLookup) { return; }
+
+ if (inputInfo.passive) {
+ if (!await this._scanTimerWait()) {
+ // Aborted
+ return;
+ }
+ }
+
+ await this._searchAt(x, y, inputInfo);
+ }
+
+ async _searchAtFromTouchStart(e, x, y) {
+ if (this._pendingLookup) { return; }
+
+ const inputInfo = this._getMatchingInputGroupFromEvent('touch', 'touchStart', e);
+ if (inputInfo === null) { return; }
+
+ const textSourceCurrentPrevious = this._textSourceCurrent !== null ? this._textSourceCurrent.clone() : null;
+ const preventScroll = inputInfo.input.options.preventTouchScrolling;
+
+ await this._searchAt(x, y, inputInfo);
+
+ if (
+ this._textSourceCurrent !== null &&
+ !this._textSourceCurrent.hasSameStart(textSourceCurrentPrevious)
+ ) {
+ this._preventScroll = preventScroll;
+ this._preventNextContextMenu = true;
+ this._preventNextMouseDown = true;
+ }
+ }
+
+ async _searchAtFromPen(e, x, y, eventType, prevent) {
+ if (this._pendingLookup) { return; }
+
+ const inputInfo = this._getMatchingInputGroupFromEvent('pen', eventType, e);
+ if (inputInfo === null) { return; }
+
+ const {input: {options}} = inputInfo;
+ if (
+ (!options.scanOnPenRelease && this._penPointerReleased) ||
+ !(this._penPointerPressed ? options.scanOnPenPress : options.scanOnPenHover)
+ ) {
+ return;
+ }
+
+ const preventScroll = inputInfo.input.options.preventTouchScrolling;
+
+ await this._searchAt(x, y, inputInfo);
+
+ if (
+ prevent &&
+ this._textSourceCurrent !== null
+ ) {
+ this._preventScroll = preventScroll;
+ this._preventNextContextMenu = true;
+ this._preventNextMouseDown = true;
+ this._preventNextClick = true;
+ }
+ }
+
+ _getMatchingInputGroupFromEvent(pointerType, eventType, event) {
+ const modifiers = DocumentUtil.getActiveModifiersAndButtons(event);
+ const modifierKeys = DocumentUtil.getActiveModifiers(event);
+ return this._getMatchingInputGroup(pointerType, eventType, modifiers, modifierKeys);
+ }
+
+ _getMatchingInputGroup(pointerType, eventType, modifiers, modifierKeys) {
+ let fallbackIndex = -1;
+ const modifiersSet = new Set(modifiers);
+ for (let i = 0, ii = this._inputs.length; i < ii; ++i) {
+ const input = this._inputs[i];
+ const {include, exclude, types} = input;
+ if (!types.has(pointerType)) { continue; }
+ if (this._setHasAll(modifiersSet, include) && (exclude.length === 0 || !this._setHasAll(modifiersSet, exclude))) {
+ if (include.length > 0) {
+ return this._createInputInfo(input, pointerType, eventType, false, modifiers, modifierKeys);
+ } else if (fallbackIndex < 0) {
+ fallbackIndex = i;
+ }
+ }
+ }
+
+ return (
+ fallbackIndex >= 0 ?
+ this._createInputInfo(this._inputs[fallbackIndex], pointerType, eventType, true, modifiers, modifierKeys) :
+ null
+ );
+ }
+
+ _createInputInfo(input, pointerType, eventType, passive, modifiers, modifierKeys, detail) {
+ return {input, pointerType, eventType, passive, modifiers, modifierKeys, detail};
+ }
+
+ _setHasAll(set, values) {
+ for (const value of values) {
+ if (!set.has(value)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ _getInputArray(value) {
+ return (
+ typeof value === 'string' ?
+ value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0) :
+ []
+ );
+ }
+
+ _getInputTypeSet({mouse, touch, pen}) {
+ const set = new Set();
+ if (mouse) { set.add('mouse'); }
+ if (touch) { set.add('touch'); }
+ if (pen) { set.add('pen'); }
+ return set;
+ }
+
+ _getPointerEventType(e) {
+ // Workaround for Firefox bug not detecting certain 'touch' events as 'pen' events.
+ const cachedPointerType = this._pointerIdTypeMap.get(e.pointerId);
+ return (typeof cachedPointerType !== 'undefined' ? cachedPointerType : e.pointerType);
+ }
+
+ _constrainTextSource(textSource, includeSelector, excludeSelector, layoutAwareScan) {
+ let length = textSource.text().length;
+ while (length > 0) {
+ const nodes = textSource.getNodesInRange();
+ if (
+ (includeSelector !== null && !DocumentUtil.everyNodeMatchesSelector(nodes, includeSelector)) ||
+ (excludeSelector !== null && DocumentUtil.anyNodeMatchesSelector(nodes, excludeSelector))
+ ) {
+ --length;
+ textSource.setEndOffset(length, layoutAwareScan);
+ } else {
+ break;
+ }
+ }
+ }
+}