From b0f6c41f5dc7f498f2948f846dd273bcb1bc1f0b Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 27 Sep 2021 19:07:28 -0400 Subject: [PATCH] Search query offset value (#1968) * Add type property to TextSource* classes * Use type property rather than instanceof * Expose a sentence offset value * Use offset added to URL * Improve fallback sentence for Anki note context --- ext/js/app/frontend.js | 3 +- ext/js/display/display-anki.js | 8 +++--- ext/js/display/display.js | 46 ++++++++++++++++++++++--------- ext/js/display/query-parser.js | 36 ++++++++++++++++++++++-- ext/js/dom/text-source-element.js | 4 +++ ext/js/dom/text-source-range.js | 4 +++ ext/js/language/text-scanner.js | 3 +- 7 files changed, 81 insertions(+), 23 deletions(-) diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js index ef53cecb..0d358639 100644 --- a/ext/js/app/frontend.js +++ b/ext/js/app/frontend.js @@ -18,7 +18,6 @@ /* global * DocumentUtil * TextScanner - * TextSourceElement * TextSourceRange */ @@ -536,7 +535,7 @@ class Frontend { } } }; - if (textSource instanceof TextSourceElement && textSource.fullContent !== query) { + if (textSource.type === 'element' && textSource.fullContent !== query) { details.params.full = textSource.fullContent; details.params['full-visible'] = 'true'; } diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js index d17a2f0f..216e3108 100644 --- a/ext/js/display/display-anki.js +++ b/ext/js/display/display-anki.js @@ -216,8 +216,8 @@ class DisplayAnki { if (typeof url !== 'string') { url = window.location.href; } - const {query, fullQuery} = this._display; - sentence = this._getValidSentenceData(sentence, query); + const {query, fullQuery, queryOffset} = this._display; + sentence = this._getValidSentenceData(sentence, fullQuery, queryOffset); return { url, sentence, @@ -565,11 +565,11 @@ class DisplayAnki { return isTerms ? ['term-kanji', 'term-kana'] : ['kanji']; } - _getValidSentenceData(sentence, fallback) { + _getValidSentenceData(sentence, fallback, fallbackOffset) { let {text, offset} = (isObject(sentence) ? sentence : {}); if (typeof text !== 'string') { text = fallback; - offset = 0; + offset = fallbackOffset; } else { if (typeof offset !== 'number') { offset = 0; } } diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 0509e22a..0e82af54 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -73,6 +73,7 @@ class Display extends EventDispatcher { this._titleMaxLength = 1000; this._query = ''; this._fullQuery = ''; + this._queryOffset = 0; this._documentUtil = new DocumentUtil(); this._progressIndicator = document.querySelector('#progress-indicator'); this._progressIndicatorTimer = null; @@ -208,6 +209,10 @@ class Display extends EventDispatcher { return this._fullQuery; } + get queryOffset() { + return this._queryOffset; + } + get frameVisible() { return this._frameVisible; } @@ -432,7 +437,7 @@ class Display extends EventDispatcher { const details = { focus: false, historyMode: 'clear', - params: this._createSearchParams(type, query, false), + params: this._createSearchParams(type, query, false, this._queryOffset), state, content: { dictionaryEntries: null, @@ -598,9 +603,14 @@ class Display extends EventDispatcher { const isTerms = (type === 'terms'); let queryFull = urlSearchParams.get('full'); queryFull = (queryFull !== null ? queryFull : query); + let queryOffset = urlSearchParams.get('offset'); + if (queryOffset !== null) { + queryOffset = Number.parseInt(queryOffset, 10); + if (!Number.isFinite(queryOffset)) { queryOffset = null; } + } const wildcardsEnabled = (urlSearchParams.get('wildcards') !== 'off'); const lookup = (urlSearchParams.get('lookup') !== 'false'); - await this._setContentTermsOrKanji(token, isTerms, query, queryFull, lookup, wildcardsEnabled, eventArgs); + await this._setContentTermsOrKanji(token, isTerms, query, queryFull, queryOffset, lookup, wildcardsEnabled, eventArgs); } break; case 'unloaded': @@ -633,7 +643,7 @@ class Display extends EventDispatcher { } } - _onQueryParserSearch({type, dictionaryEntries, sentence, inputInfo: {eventType}, textSource, optionsContext}) { + _onQueryParserSearch({type, dictionaryEntries, sentence, inputInfo: {eventType}, textSource, optionsContext, sentenceOffset}) { const query = textSource.text(); const historyState = this._history.state; const historyMode = ( @@ -644,7 +654,7 @@ class Display extends EventDispatcher { const details = { focus: false, historyMode, - params: this._createSearchParams(type, query, false), + params: this._createSearchParams(type, query, false, sentenceOffset), state: { sentence, optionsContext, @@ -724,7 +734,7 @@ class Display extends EventDispatcher { const details = { focus: false, historyMode: 'new', - params: this._createSearchParams('kanji', query, false), + params: this._createSearchParams('kanji', query, false, null), state: { focusEntry: 0, optionsContext, @@ -887,7 +897,7 @@ class Display extends EventDispatcher { } } - async _setContentTermsOrKanji(token, isTerms, query, queryFull, lookup, wildcardsEnabled, eventArgs) { + async _setContentTermsOrKanji(token, isTerms, query, queryFull, queryOffset, lookup, wildcardsEnabled, eventArgs) { let {state, content} = this._history; let changeHistory = false; if (!isObject(content)) { @@ -912,7 +922,11 @@ class Display extends EventDispatcher { changeHistory = true; } - this._setFullQuery(queryFull); + if (queryOffset !== null) { + queryOffset = Math.max(0, Math.min(queryFull.length - query.length, queryOffset)); + } + + this._setFullQuery(queryFull, queryOffset); this._setTitleText(query); let {dictionaryEntries} = content; @@ -1015,13 +1029,13 @@ class Display extends EventDispatcher { this._updateNavigation(false, false); this._setNoContentVisible(false); this._setTitleText(''); - this._setFullQuery(''); + this._setFullQuery('', 0); } _clearContent() { this._container.textContent = ''; this._setTitleText(''); - this._setFullQuery(''); + this._setFullQuery('', 0); } _setNoContentVisible(visible) { @@ -1032,8 +1046,9 @@ class Display extends EventDispatcher { } } - _setFullQuery(text) { + _setFullQuery(text, offset) { this._fullQuery = text; + this._queryOffset = offset; this._updateQueryParser(); } @@ -1200,12 +1215,17 @@ class Display extends EventDispatcher { } } - _createSearchParams(type, query, wildcards) { + _createSearchParams(type, query, wildcards, sentenceOffset) { const params = {}; - if (query.length < this._fullQuery.length) { - params.full = this._fullQuery; + const fullQuery = this._fullQuery; + const includeFull = (query.length < fullQuery.length); + if (includeFull) { + params.full = fullQuery; } params.query = query; + if (includeFull && sentenceOffset !== null) { + params.offset = `${sentenceOffset}`; + } if (typeof type === 'string') { params.type = type; } diff --git a/ext/js/display/query-parser.js b/ext/js/display/query-parser.js index cbcf7cff..e2578fcf 100644 --- a/ext/js/display/query-parser.js +++ b/ext/js/display/query-parser.js @@ -117,6 +117,8 @@ class QueryParser extends EventDispatcher { } if (e.type === null) { return; } + e.sentenceOffset = this._getSentenceOffset(e.textSource); + this.trigger('searched', e); } @@ -208,29 +210,33 @@ class QueryParser extends EventDispatcher { } _createParseResult(data) { + let offset = 0; const fragment = document.createDocumentFragment(); for (const term of data) { const termNode = document.createElement('span'); termNode.className = 'query-parser-term'; + termNode.dataset.offset = `${offset}`; for (const {text, reading} of term) { if (reading.length === 0) { termNode.appendChild(document.createTextNode(text)); } else { const reading2 = this._convertReading(text, reading); - termNode.appendChild(this._createSegment(text, reading2)); + termNode.appendChild(this._createSegment(text, reading2, offset)); } + offset += text.length; } fragment.appendChild(termNode); } return fragment; } - _createSegment(text, reading) { + _createSegment(text, reading, offset) { const segmentNode = document.createElement('ruby'); segmentNode.className = 'query-parser-segment'; const textNode = document.createElement('span'); textNode.className = 'query-parser-segment-text'; + textNode.dataset.offset = `${offset}`; const readingNode = document.createElement('rt'); readingNode.className = 'query-parser-segment-reading'; @@ -265,4 +271,30 @@ class QueryParser extends EventDispatcher { return reading; } } + + _getSentenceOffset(textSource) { + if (textSource.type === 'range') { + const {range} = textSource; + const node = this._getParentElement(range.startContainer); + if (node !== null) { + const {offset} = node.dataset; + if (typeof offset === 'string') { + const value = Number.parseInt(offset, 10); + if (Number.isFinite(value)) { + return Math.max(0, value) + range.startOffset; + } + } + } + } + return null; + } + + _getParentElement(node) { + const {ELEMENT_NODE} = Node; + while (true) { + node = node.parentNode; + if (node === null) { return null; } + if (node.nodeType === ELEMENT_NODE) { return node; } + } + } } diff --git a/ext/js/dom/text-source-element.js b/ext/js/dom/text-source-element.js index 499ee45d..11bf7eb0 100644 --- a/ext/js/dom/text-source-element.js +++ b/ext/js/dom/text-source-element.js @@ -24,6 +24,10 @@ class TextSourceElement { this._content = this._fullContent.substring(this._startOffset, this._endOffset); } + get type() { + return 'element'; + } + get element() { return this._element; } diff --git a/ext/js/dom/text-source-range.js b/ext/js/dom/text-source-range.js index 377016da..591429da 100644 --- a/ext/js/dom/text-source-range.js +++ b/ext/js/dom/text-source-range.js @@ -29,6 +29,10 @@ class TextSourceRange { this._imposterSourceElement = imposterSourceElement; } + get type() { + return 'range'; + } + get range() { return this._range; } diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js index c7cedd7e..5d37c84b 100644 --- a/ext/js/language/text-scanner.js +++ b/ext/js/language/text-scanner.js @@ -17,7 +17,6 @@ /* global * DocumentUtil - * TextSourceElement */ class TextScanner extends EventDispatcher { @@ -345,7 +344,7 @@ class TextScanner extends EventDispatcher { if (result !== null) { ({dictionaryEntries, sentence, type} = result); valid = true; - } else if (textSource instanceof TextSourceElement && await this._hasJapanese(textSource.fullContent)) { + } else if (textSource !== null && textSource.type === 'element' && await this._hasJapanese(textSource.fullContent)) { dictionaryEntries = []; sentence = {sentence: '', offset: 0}; type = 'terms';