diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2020-08-09 13:27:21 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-09 13:27:21 -0400 |
commit | 6da02c6eee803756d9a9075bfde333eeb31ce64b (patch) | |
tree | 158d81b89ca99095c06db31c1ebc7bb46a57c33a | |
parent | 480e0e15e3109165d077c18985893d7cca79959e (diff) |
document.js refactor (#719)
* Refactor document.js into a class
* Move public functions first
* Rename private functions
* Rename
* Rename argument
* Use instance of DocumentUtil
* Update tests
* Refactor
* Rename (test-)document.js to (test-)document-util.js
-rw-r--r-- | dev/data/manifest-variants.json | 2 | ||||
-rw-r--r-- | ext/bg/js/query-parser.js | 9 | ||||
-rw-r--r-- | ext/bg/search.html | 2 | ||||
-rw-r--r-- | ext/bg/settings-popup-preview.html | 2 | ||||
-rw-r--r-- | ext/fg/float.html | 2 | ||||
-rw-r--r-- | ext/fg/js/document-util.js | 381 | ||||
-rw-r--r-- | ext/fg/js/document.js | 374 | ||||
-rw-r--r-- | ext/fg/js/frontend.js | 8 | ||||
-rw-r--r-- | ext/manifest.json | 2 | ||||
-rw-r--r-- | ext/mixed/js/display.js | 11 | ||||
-rw-r--r-- | ext/mixed/js/text-scanner.js | 6 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | test/test-document-util.js (renamed from test/test-document.js) | 16 |
13 files changed, 414 insertions, 403 deletions
diff --git a/dev/data/manifest-variants.json b/dev/data/manifest-variants.json index bac781da..c8135baf 100644 --- a/dev/data/manifest-variants.json +++ b/dev/data/manifest-variants.json @@ -47,7 +47,7 @@ "mixed/js/dynamic-loader.js", "mixed/js/frame-client.js", "mixed/js/text-scanner.js", - "fg/js/document.js", + "fg/js/document-util.js", "fg/js/dom-text-scanner.js", "fg/js/popup.js", "fg/js/source.js", diff --git a/ext/bg/js/query-parser.js b/ext/bg/js/query-parser.js index 6ca09a82..2ad88601 100644 --- a/ext/bg/js/query-parser.js +++ b/ext/bg/js/query-parser.js @@ -19,11 +19,10 @@ * QueryParserGenerator * TextScanner * api - * docSentenceExtract */ class QueryParser extends EventDispatcher { - constructor({getOptionsContext, setSpinnerVisible}) { + constructor({getOptionsContext, setSpinnerVisible, documentUtil}) { super(); this._getOptionsContext = getOptionsContext; this._setSpinnerVisible = setSpinnerVisible; @@ -31,6 +30,7 @@ class QueryParser extends EventDispatcher { this._scanLength = 1; this._sentenceExtent = 1; this._layoutAwareScan = false; + this._documentUtil = documentUtil; this._parseResults = []; this._queryParser = document.querySelector('#query-parser-content'); this._queryParserSelect = document.querySelector('#query-parser-select-container'); @@ -39,7 +39,8 @@ class QueryParser extends EventDispatcher { node: this._queryParser, ignoreElements: () => [], ignorePoint: null, - search: this._search.bind(this) + search: this._search.bind(this), + documentUtil }); } @@ -104,7 +105,7 @@ class QueryParser extends EventDispatcher { const {definitions, length} = await api.termsFind(searchText, {}, optionsContext); if (definitions.length === 0) { return null; } - const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); + const sentence = this._documentUtil.extractSentence(textSource, sentenceExtent, layoutAwareScan); textSource.setEndOffset(length, layoutAwareScan); diff --git a/ext/bg/search.html b/ext/bg/search.html index eb85e368..aff71835 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -79,7 +79,7 @@ <script src="/bg/js/dictionary.js"></script> <script src="/bg/js/handlebars.js"></script> - <script src="/fg/js/document.js"></script> + <script src="/fg/js/document-util.js"></script> <script src="/fg/js/dom-text-scanner.js"></script> <script src="/fg/js/source.js"></script> <script src="/mixed/js/audio-system.js"></script> diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index 5b3a9692..3479efa4 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -127,7 +127,7 @@ <script src="/mixed/js/frame-client.js"></script> <script src="/mixed/js/text-scanner.js"></script> - <script src="/fg/js/document.js"></script> + <script src="/fg/js/document-util.js"></script> <script src="/fg/js/dom-text-scanner.js"></script> <script src="/fg/js/popup.js"></script> <script src="/fg/js/source.js"></script> diff --git a/ext/fg/float.html b/ext/fg/float.html index 17378713..51aa2350 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -51,7 +51,7 @@ <script src="/mixed/js/api.js"></script> <script src="/mixed/js/japanese.js"></script> - <script src="/fg/js/document.js"></script> + <script src="/fg/js/document-util.js"></script> <script src="/fg/js/dom-text-scanner.js"></script> <script src="/fg/js/source.js"></script> <script src="/mixed/js/audio-system.js"></script> diff --git a/ext/fg/js/document-util.js b/ext/fg/js/document-util.js new file mode 100644 index 00000000..d3bba30f --- /dev/null +++ b/ext/fg/js/document-util.js @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2016-2020 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 + * DOM + * DOMTextScanner + * TextSourceElement + * TextSourceRange + */ + +class DocumentUtil { + constructor() { + this._transparentColorPattern = /rgba\s*\([^)]*,\s*0(?:\.0+)?\s*\)/; + } + + getRangeFromPoint(x, y, deepContentScan) { + const elements = this._getElementsFromPoint(x, y, deepContentScan); + let imposter = null; + let imposterContainer = null; + let imposterSourceElement = null; + if (elements.length > 0) { + const element = elements[0]; + switch (element.nodeName.toUpperCase()) { + case 'IMG': + case 'BUTTON': + return new TextSourceElement(element); + case 'INPUT': + imposterSourceElement = element; + [imposter, imposterContainer] = this._createImposter(element, false); + break; + case 'TEXTAREA': + imposterSourceElement = element; + [imposter, imposterContainer] = this._createImposter(element, true); + break; + } + } + + const range = this._caretRangeFromPointExt(x, y, deepContentScan ? elements : []); + if (range !== null) { + if (imposter !== null) { + this._setImposterStyle(imposterContainer.style, 'z-index', '-2147483646'); + this._setImposterStyle(imposter.style, 'pointer-events', 'none'); + } + return new TextSourceRange(range, '', imposterContainer, imposterSourceElement); + } else { + if (imposterContainer !== null) { + imposterContainer.parentNode.removeChild(imposterContainer); + } + return null; + } + } + + extractSentence(source, extent, layoutAwareScan) { + const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'}; + const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'}; + const terminators = '…。..??!!'; + + const sourceLocal = source.clone(); + const position = sourceLocal.setStartOffset(extent, layoutAwareScan); + sourceLocal.setEndOffset(extent * 2 - position, layoutAwareScan, true); + const content = sourceLocal.text(); + + let quoteStack = []; + + let startPos = 0; + for (let i = position; i >= startPos; --i) { + const c = content[i]; + + if (c === '\n') { + startPos = i + 1; + break; + } + + if (quoteStack.length === 0 && (terminators.includes(c) || c in quotesFwd)) { + startPos = i + 1; + break; + } + + if (quoteStack.length > 0 && c === quoteStack[0]) { + quoteStack.pop(); + } else if (c in quotesBwd) { + quoteStack.unshift(quotesBwd[c]); + } + } + + quoteStack = []; + + let endPos = content.length; + for (let i = position; i <= endPos; ++i) { + const c = content[i]; + + if (c === '\n') { + endPos = i + 1; + break; + } + + if (quoteStack.length === 0) { + if (terminators.includes(c)) { + endPos = i + 1; + break; + } else if (c in quotesBwd) { + endPos = i; + break; + } + } + + if (quoteStack.length > 0 && c === quoteStack[0]) { + quoteStack.pop(); + } else if (c in quotesFwd) { + quoteStack.unshift(quotesFwd[c]); + } + } + + const text = content.substring(startPos, endPos); + const padding = text.length - text.replace(/^\s+/, '').length; + + return { + text: text.trim(), + offset: position - startPos - padding + }; + } + + // Private + + _setImposterStyle(style, propertyName, value) { + style.setProperty(propertyName, value, 'important'); + } + + _createImposter(element, isTextarea) { + const body = document.body; + if (body === null) { return [null, null]; } + + const elementStyle = window.getComputedStyle(element); + const elementRect = element.getBoundingClientRect(); + const documentRect = document.documentElement.getBoundingClientRect(); + let left = elementRect.left - documentRect.left; + let top = elementRect.top - documentRect.top; + + // Container + const container = document.createElement('div'); + const containerStyle = container.style; + this._setImposterStyle(containerStyle, 'all', 'initial'); + this._setImposterStyle(containerStyle, 'position', 'absolute'); + this._setImposterStyle(containerStyle, 'left', '0'); + this._setImposterStyle(containerStyle, 'top', '0'); + this._setImposterStyle(containerStyle, 'width', `${documentRect.width}px`); + this._setImposterStyle(containerStyle, 'height', `${documentRect.height}px`); + this._setImposterStyle(containerStyle, 'overflow', 'hidden'); + this._setImposterStyle(containerStyle, 'opacity', '0'); + this._setImposterStyle(containerStyle, 'pointer-events', 'none'); + this._setImposterStyle(containerStyle, 'z-index', '2147483646'); + + // Imposter + const imposter = document.createElement('div'); + const imposterStyle = imposter.style; + + let value = element.value; + if (value.endsWith('\n')) { value += '\n'; } + imposter.textContent = value; + + for (let i = 0, ii = elementStyle.length; i < ii; ++i) { + const property = elementStyle[i]; + this._setImposterStyle(imposterStyle, property, elementStyle.getPropertyValue(property)); + } + this._setImposterStyle(imposterStyle, 'position', 'absolute'); + this._setImposterStyle(imposterStyle, 'top', `${top}px`); + this._setImposterStyle(imposterStyle, 'left', `${left}px`); + this._setImposterStyle(imposterStyle, 'margin', '0'); + this._setImposterStyle(imposterStyle, 'pointer-events', 'auto'); + + if (isTextarea) { + if (elementStyle.overflow === 'visible') { + this._setImposterStyle(imposterStyle, 'overflow', 'auto'); + } + } else { + this._setImposterStyle(imposterStyle, 'overflow', 'hidden'); + this._setImposterStyle(imposterStyle, 'white-space', 'nowrap'); + this._setImposterStyle(imposterStyle, 'line-height', elementStyle.height); + } + + container.appendChild(imposter); + body.appendChild(container); + + // Adjust size + const imposterRect = imposter.getBoundingClientRect(); + if (imposterRect.width !== elementRect.width || imposterRect.height !== elementRect.height) { + const width = parseFloat(elementStyle.width) + (elementRect.width - imposterRect.width); + const height = parseFloat(elementStyle.height) + (elementRect.height - imposterRect.height); + this._setImposterStyle(imposterStyle, 'width', `${width}px`); + this._setImposterStyle(imposterStyle, 'height', `${height}px`); + } + if (imposterRect.x !== elementRect.x || imposterRect.y !== elementRect.y) { + left += (elementRect.left - imposterRect.left); + top += (elementRect.top - imposterRect.top); + this._setImposterStyle(imposterStyle, 'left', `${left}px`); + this._setImposterStyle(imposterStyle, 'top', `${top}px`); + } + + imposter.scrollTop = element.scrollTop; + imposter.scrollLeft = element.scrollLeft; + + return [imposter, container]; + } + + _getElementsFromPoint(x, y, all) { + if (all) { + // document.elementsFromPoint can return duplicates which must be removed. + const elements = document.elementsFromPoint(x, y); + return elements.filter((e, i) => elements.indexOf(e) === i); + } + + const e = document.elementFromPoint(x, y); + return e !== null ? [e] : []; + } + + _isPointInRange(x, y, range) { + // Require a text node to start + if (range.startContainer.nodeType !== Node.TEXT_NODE) { + return false; + } + + // Scan forward + const nodePre = range.endContainer; + const offsetPre = range.endOffset; + try { + const {node, offset, content} = new DOMTextScanner(range.endContainer, range.endOffset, true, false).seek(1); + range.setEnd(node, offset); + + if (!this._isWhitespace(content) && DOM.isPointInAnyRect(x, y, range.getClientRects())) { + return true; + } + } finally { + range.setEnd(nodePre, offsetPre); + } + + // Scan backward + const {node, offset, content} = new DOMTextScanner(range.startContainer, range.startOffset, true, false).seek(-1); + range.setStart(node, offset); + + if (!this._isWhitespace(content) && DOM.isPointInAnyRect(x, y, range.getClientRects())) { + // This purposefully leaves the starting offset as modified and sets the range length to 0. + range.setEnd(node, offset); + return true; + } + + // No match + return false; + } + + _isWhitespace(string) { + return string.trim().length === 0; + } + + _caretRangeFromPoint(x, y) { + if (typeof document.caretRangeFromPoint === 'function') { + // Chrome, Edge + return document.caretRangeFromPoint(x, y); + } + + if (typeof document.caretPositionFromPoint === 'function') { + // Firefox + return this._caretPositionFromPoint(x, y); + } + + // No support + return null; + } + + _caretPositionFromPoint(x, y) { + const position = document.caretPositionFromPoint(x, y); + if (position === null) { + return null; + } + const node = position.offsetNode; + if (node === null) { + return null; + } + + const range = document.createRange(); + const offset = (node.nodeType === Node.TEXT_NODE ? position.offset : 0); + try { + range.setStart(node, offset); + range.setEnd(node, offset); + } catch (e) { + // Firefox throws new DOMException("The operation is insecure.") + // when trying to select a node from within a ShadowRoot. + return null; + } + return range; + } + + _caretRangeFromPointExt(x, y, elements) { + const modifications = []; + try { + let i = 0; + let startContinerPre = null; + while (true) { + const range = this._caretRangeFromPoint(x, y); + if (range === null) { + return null; + } + + const startContainer = range.startContainer; + if (startContinerPre !== startContainer) { + if (this._isPointInRange(x, y, range)) { + return range; + } + startContinerPre = startContainer; + } + + i = this._disableTransparentElement(elements, i, modifications); + if (i < 0) { + return null; + } + } + } finally { + if (modifications.length > 0) { + this._restoreElementStyleModifications(modifications); + } + } + } + + _disableTransparentElement(elements, i, modifications) { + while (true) { + if (i >= elements.length) { + return -1; + } + + const element = elements[i++]; + if (this._isElementTransparent(element)) { + const style = element.hasAttribute('style') ? element.getAttribute('style') : null; + modifications.push({element, style}); + element.style.setProperty('pointer-events', 'none', 'important'); + return i; + } + } + } + + _restoreElementStyleModifications(modifications) { + for (const {element, style} of modifications) { + if (style === null) { + element.removeAttribute('style'); + } else { + element.setAttribute('style', style); + } + } + } + + _isElementTransparent(element) { + if ( + element === document.body || + element === document.documentElement + ) { + return false; + } + const style = window.getComputedStyle(element); + return ( + parseFloat(style.opacity) <= 0 || + style.visibility === 'hidden' || + (style.backgroundImage === 'none' && this._isColorTransparent(style.backgroundColor)) + ); + } + + _isColorTransparent(cssColor) { + return this._transparentColorPattern.test(cssColor); + } +} diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js deleted file mode 100644 index c288502c..00000000 --- a/ext/fg/js/document.js +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Copyright (C) 2016-2020 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 - * DOM - * DOMTextScanner - * TextSourceElement - * TextSourceRange - */ - -const REGEX_TRANSPARENT_COLOR = /rgba\s*\([^)]*,\s*0(?:\.0+)?\s*\)/; - -function docSetImposterStyle(style, propertyName, value) { - style.setProperty(propertyName, value, 'important'); -} - -function docImposterCreate(element, isTextarea) { - const body = document.body; - if (body === null) { return [null, null]; } - - const elementStyle = window.getComputedStyle(element); - const elementRect = element.getBoundingClientRect(); - const documentRect = document.documentElement.getBoundingClientRect(); - let left = elementRect.left - documentRect.left; - let top = elementRect.top - documentRect.top; - - // Container - const container = document.createElement('div'); - const containerStyle = container.style; - docSetImposterStyle(containerStyle, 'all', 'initial'); - docSetImposterStyle(containerStyle, 'position', 'absolute'); - docSetImposterStyle(containerStyle, 'left', '0'); - docSetImposterStyle(containerStyle, 'top', '0'); - docSetImposterStyle(containerStyle, 'width', `${documentRect.width}px`); - docSetImposterStyle(containerStyle, 'height', `${documentRect.height}px`); - docSetImposterStyle(containerStyle, 'overflow', 'hidden'); - docSetImposterStyle(containerStyle, 'opacity', '0'); - - docSetImposterStyle(containerStyle, 'pointer-events', 'none'); - docSetImposterStyle(containerStyle, 'z-index', '2147483646'); - - // Imposter - const imposter = document.createElement('div'); - const imposterStyle = imposter.style; - - let value = element.value; - if (value.endsWith('\n')) { value += '\n'; } - imposter.textContent = value; - - for (let i = 0, ii = elementStyle.length; i < ii; ++i) { - const property = elementStyle[i]; - docSetImposterStyle(imposterStyle, property, elementStyle.getPropertyValue(property)); - } - docSetImposterStyle(imposterStyle, 'position', 'absolute'); - docSetImposterStyle(imposterStyle, 'top', `${top}px`); - docSetImposterStyle(imposterStyle, 'left', `${left}px`); - docSetImposterStyle(imposterStyle, 'margin', '0'); - docSetImposterStyle(imposterStyle, 'pointer-events', 'auto'); - - if (isTextarea) { - if (elementStyle.overflow === 'visible') { - docSetImposterStyle(imposterStyle, 'overflow', 'auto'); - } - } else { - docSetImposterStyle(imposterStyle, 'overflow', 'hidden'); - docSetImposterStyle(imposterStyle, 'white-space', 'nowrap'); - docSetImposterStyle(imposterStyle, 'line-height', elementStyle.height); - } - - container.appendChild(imposter); - body.appendChild(container); - - // Adjust size - const imposterRect = imposter.getBoundingClientRect(); - if (imposterRect.width !== elementRect.width || imposterRect.height !== elementRect.height) { - const width = parseFloat(elementStyle.width) + (elementRect.width - imposterRect.width); - const height = parseFloat(elementStyle.height) + (elementRect.height - imposterRect.height); - docSetImposterStyle(imposterStyle, 'width', `${width}px`); - docSetImposterStyle(imposterStyle, 'height', `${height}px`); - } - if (imposterRect.x !== elementRect.x || imposterRect.y !== elementRect.y) { - left += (elementRect.left - imposterRect.left); - top += (elementRect.top - imposterRect.top); - docSetImposterStyle(imposterStyle, 'left', `${left}px`); - docSetImposterStyle(imposterStyle, 'top', `${top}px`); - } - - imposter.scrollTop = element.scrollTop; - imposter.scrollLeft = element.scrollLeft; - - return [imposter, container]; -} - -function docElementsFromPoint(x, y, all) { - if (all) { - // document.elementsFromPoint can return duplicates which must be removed. - const elements = document.elementsFromPoint(x, y); - return elements.filter((e, i) => elements.indexOf(e) === i); - } - - const e = document.elementFromPoint(x, y); - return e !== null ? [e] : []; -} - -function docRangeFromPoint(x, y, deepDomScan) { - const elements = docElementsFromPoint(x, y, deepDomScan); - let imposter = null; - let imposterContainer = null; - let imposterSourceElement = null; - if (elements.length > 0) { - const element = elements[0]; - switch (element.nodeName.toUpperCase()) { - case 'IMG': - case 'BUTTON': - return new TextSourceElement(element); - case 'INPUT': - imposterSourceElement = element; - [imposter, imposterContainer] = docImposterCreate(element, false); - break; - case 'TEXTAREA': - imposterSourceElement = element; - [imposter, imposterContainer] = docImposterCreate(element, true); - break; - } - } - - const range = caretRangeFromPointExt(x, y, deepDomScan ? elements : []); - if (range !== null) { - if (imposter !== null) { - docSetImposterStyle(imposterContainer.style, 'z-index', '-2147483646'); - docSetImposterStyle(imposter.style, 'pointer-events', 'none'); - } - return new TextSourceRange(range, '', imposterContainer, imposterSourceElement); - } else { - if (imposterContainer !== null) { - imposterContainer.parentNode.removeChild(imposterContainer); - } - return null; - } -} - -function docSentenceExtract(source, extent, layoutAwareScan) { - const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'}; - const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'}; - const terminators = '…。..??!!'; - - const sourceLocal = source.clone(); - const position = sourceLocal.setStartOffset(extent, layoutAwareScan); - sourceLocal.setEndOffset(extent * 2 - position, layoutAwareScan, true); - const content = sourceLocal.text(); - - let quoteStack = []; - - let startPos = 0; - for (let i = position; i >= startPos; --i) { - const c = content[i]; - - if (c === '\n') { - startPos = i + 1; - break; - } - - if (quoteStack.length === 0 && (terminators.includes(c) || c in quotesFwd)) { - startPos = i + 1; - break; - } - - if (quoteStack.length > 0 && c === quoteStack[0]) { - quoteStack.pop(); - } else if (c in quotesBwd) { - quoteStack.unshift(quotesBwd[c]); - } - } - - quoteStack = []; - - let endPos = content.length; - for (let i = position; i <= endPos; ++i) { - const c = content[i]; - - if (c === '\n') { - endPos = i + 1; - break; - } - - if (quoteStack.length === 0) { - if (terminators.includes(c)) { - endPos = i + 1; - break; - } else if (c in quotesBwd) { - endPos = i; - break; - } - } - - if (quoteStack.length > 0 && c === quoteStack[0]) { - quoteStack.pop(); - } else if (c in quotesFwd) { - quoteStack.unshift(quotesFwd[c]); - } - } - - const text = content.substring(startPos, endPos); - const padding = text.length - text.replace(/^\s+/, '').length; - - return { - text: text.trim(), - offset: position - startPos - padding - }; -} - -function isPointInRange(x, y, range) { - // Require a text node to start - if (range.startContainer.nodeType !== Node.TEXT_NODE) { - return false; - } - - // Scan forward - const nodePre = range.endContainer; - const offsetPre = range.endOffset; - try { - const {node, offset, content} = new DOMTextScanner(range.endContainer, range.endOffset, true, false).seek(1); - range.setEnd(node, offset); - - if (!isWhitespace(content) && DOM.isPointInAnyRect(x, y, range.getClientRects())) { - return true; - } - } finally { - range.setEnd(nodePre, offsetPre); - } - - // Scan backward - const {node, offset, content} = new DOMTextScanner(range.startContainer, range.startOffset, true, false).seek(-1); - range.setStart(node, offset); - - if (!isWhitespace(content) && DOM.isPointInAnyRect(x, y, range.getClientRects())) { - // This purposefully leaves the starting offset as modified and sets the range length to 0. - range.setEnd(node, offset); - return true; - } - - // No match - return false; -} - -function isWhitespace(string) { - return string.trim().length === 0; -} - -const caretRangeFromPoint = (() => { - if (typeof document.caretRangeFromPoint === 'function') { - // Chrome, Edge - return (x, y) => document.caretRangeFromPoint(x, y); - } - - if (typeof document.caretPositionFromPoint === 'function') { - // Firefox - return (x, y) => { - const position = document.caretPositionFromPoint(x, y); - if (position === null) { - return null; - } - const node = position.offsetNode; - if (node === null) { - return null; - } - - const range = document.createRange(); - const offset = (node.nodeType === Node.TEXT_NODE ? position.offset : 0); - try { - range.setStart(node, offset); - range.setEnd(node, offset); - } catch (e) { - // Firefox throws new DOMException("The operation is insecure.") - // when trying to select a node from within a ShadowRoot. - return null; - } - return range; - }; - } - - // No support - return () => null; -})(); - -function caretRangeFromPointExt(x, y, elements) { - const modifications = []; - try { - let i = 0; - let startContinerPre = null; - while (true) { - const range = caretRangeFromPoint(x, y); - if (range === null) { - return null; - } - - const startContainer = range.startContainer; - if (startContinerPre !== startContainer) { - if (isPointInRange(x, y, range)) { - return range; - } - startContinerPre = startContainer; - } - - i = disableTransparentElement(elements, i, modifications); - if (i < 0) { - return null; - } - } - } finally { - if (modifications.length > 0) { - restoreElementStyleModifications(modifications); - } - } -} - -function disableTransparentElement(elements, i, modifications) { - while (true) { - if (i >= elements.length) { - return -1; - } - - const element = elements[i++]; - if (isElementTransparent(element)) { - const style = element.hasAttribute('style') ? element.getAttribute('style') : null; - modifications.push({element, style}); - element.style.setProperty('pointer-events', 'none', 'important'); - return i; - } - } -} - -function restoreElementStyleModifications(modifications) { - for (const {element, style} of modifications) { - if (style === null) { - element.removeAttribute('style'); - } else { - element.setAttribute('style', style); - } - } -} - -function isElementTransparent(element) { - if ( - element === document.body || - element === document.documentElement - ) { - return false; - } - const style = window.getComputedStyle(element); - return ( - parseFloat(style.opacity) <= 0 || - style.visibility === 'hidden' || - (style.backgroundImage === 'none' && isColorTransparent(style.backgroundColor)) - ); -} - -function isColorTransparent(cssColor) { - return REGEX_TRANSPARENT_COLOR.test(cssColor); -} diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 1214e74b..ccffbab6 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -17,12 +17,12 @@ /* global * DOM + * DocumentUtil * FrameOffsetForwarder * PopupProxy * TextScanner * TextSourceElement * api - * docSentenceExtract */ class Frontend { @@ -36,11 +36,13 @@ class Frontend { this._lastShowPromise = Promise.resolve(); this._activeModifiers = new Set(); this._optionsUpdatePending = false; + this._documentUtil = new DocumentUtil(); this._textScanner = new TextScanner({ node: window, ignoreElements: this._ignoreElements.bind(this), ignorePoint: this._ignorePoint.bind(this), - search: this._search.bind(this) + search: this._search.bind(this), + documentUtil: this._documentUtil }); const { @@ -432,7 +434,7 @@ class Frontend { const {url} = optionsContext; const sentenceExtent = this._options.anki.sentenceExt; const layoutAwareScan = this._options.scanning.layoutAwareScan; - const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); + const sentence = this._documentUtil.extractSentence(textSource, sentenceExtent, layoutAwareScan); const query = textSource.text(); const details = { focus, diff --git a/ext/manifest.json b/ext/manifest.json index 91516751..b22e75c6 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -46,7 +46,7 @@ "mixed/js/dynamic-loader.js", "mixed/js/frame-client.js", "mixed/js/text-scanner.js", - "fg/js/document.js", + "fg/js/document-util.js", "fg/js/dom-text-scanner.js", "fg/js/popup.js", "fg/js/source.js", diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 927c2c2c..170b9d23 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -20,14 +20,13 @@ * DOM * DisplayGenerator * DisplayHistory + * DocumentUtil * Frontend * MediaLoader * PopupFactory * QueryParser * WindowScroll * api - * docRangeFromPoint - * docSentenceExtract * dynamicLoader */ @@ -74,12 +73,14 @@ class Display extends EventDispatcher { this._defaultTitle = 'Yomichan Search'; this._defaultTitleMaxLength = 1000; this._fullQuery = ''; + this._documentUtil = new DocumentUtil(); this._queryParserVisible = false; this._queryParserVisibleOverride = null; this._queryParserContainer = document.querySelector('#query-parser-container'); this._queryParser = new QueryParser({ getOptionsContext: this.getOptionsContext.bind(this), - setSpinnerVisible: this.setSpinnerVisible.bind(this) + setSpinnerVisible: this.setSpinnerVisible.bind(this), + documentUtil: this._documentUtil }); this._mode = null; @@ -588,7 +589,7 @@ class Display extends EventDispatcher { const scannedElement = e.target; const sentenceExtent = this._options.anki.sentenceExt; const layoutAwareScan = this._options.scanning.layoutAwareScan; - const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); + const sentence = this._documentUtil.extractSentence(textSource, sentenceExtent, layoutAwareScan); state.focusEntry = this._entryIndexFind(scannedElement); state.scrollX = this._windowScroll.x; @@ -616,7 +617,7 @@ class Display extends EventDispatcher { e.preventDefault(); const {length: scanLength, deepDomScan: deepScan, layoutAwareScan} = this._options.scanning; - const textSource = docRangeFromPoint(e.clientX, e.clientY, deepScan); + const textSource = this._documentUtil.getRangeFromPoint(e.clientX, e.clientY, deepScan); if (textSource === null) { return false; } diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index 61e9256d..923784b3 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -17,16 +17,16 @@ /* global * DOM - * docRangeFromPoint */ class TextScanner extends EventDispatcher { - constructor({node, ignoreElements, ignorePoint, search}) { + constructor({node, ignoreElements, ignorePoint, search, documentUtil}) { super(); this._node = node; this._ignoreElements = ignoreElements; this._ignorePoint = ignorePoint; this._search = search; + this._documentUtil = documentUtil; this._isPrepared = false; this._ignoreNodes = null; @@ -124,7 +124,7 @@ class TextScanner extends EventDispatcher { return; } - const textSource = docRangeFromPoint(x, y, this._deepContentScan); + const textSource = this._documentUtil.getRangeFromPoint(x, y, this._deepContentScan); try { if (this._textSourceCurrent !== null && this._textSourceCurrent.equals(textSource)) { return; diff --git a/package.json b/package.json index 5704ddb3..4a788980 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build": "node ./dev/build.js", "test": "npm run test-lint && npm run test-code && npm run test-manifest", "test-lint": "eslint . && node ./test/lint/global-declarations.js", - "test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-document.js && node ./test/test-object-property-accessor.js && node ./test/test-japanese.js && node ./test/test-text-source-map.js && node ./test/test-dom-text-scanner.js && node ./test/test-cache-map.js", + "test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-document-util.js && node ./test/test-object-property-accessor.js && node ./test/test-japanese.js && node ./test/test-text-source-map.js && node ./test/test-dom-text-scanner.js && node ./test/test-cache-map.js", "test-manifest": "node ./test/test-manifest.js" }, "repository": { diff --git a/test/test-document.js b/test/test-document-util.js index ba7acc49..40b65ed1 100644 --- a/test/test-document.js +++ b/test/test-document-util.js @@ -96,25 +96,24 @@ async function testDocument1() { 'mixed/js/dom.js', 'fg/js/dom-text-scanner.js', 'fg/js/source.js', - 'fg/js/document.js' + 'fg/js/document-util.js' ]); - const [DOMTextScanner, TextSourceRange, TextSourceElement, docRangeFromPoint, docSentenceExtract] = vm.get([ + const [DOMTextScanner, TextSourceRange, TextSourceElement, DocumentUtil] = vm.get([ 'DOMTextScanner', 'TextSourceRange', 'TextSourceElement', - 'docRangeFromPoint', - 'docSentenceExtract' + 'DocumentUtil' ]); try { - await testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSentenceExtract, TextSourceRange, TextSourceElement}); + await testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement}); await testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}); } finally { window.close(); } } -async function testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSentenceExtract, TextSourceRange, TextSourceElement}) { +async function testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement}) { const document = dom.window.document; for (const testElement of document.querySelectorAll('.test[data-test-type=scan]')) { @@ -163,7 +162,8 @@ async function testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSen }; // Test docRangeFromPoint - const source = docRangeFromPoint(0, 0, false); + const documentUtil = new DocumentUtil(); + const source = documentUtil.getRangeFromPoint(0, 0, false); switch (resultType) { case 'TextSourceRange': assert.strictEqual(getPrototypeOfOrNull(source), TextSourceRange.prototype); @@ -181,7 +181,7 @@ async function testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSen if (source === null) { continue; } // Test docSentenceExtract - const sentenceActual = docSentenceExtract(source, sentenceExtent, false).text; + const sentenceActual = documentUtil.extractSentence(source, sentenceExtent, false).text; assert.strictEqual(sentenceActual, sentence); // Clean |