From 4d2e5b93f4634bd5dfe55c305e441f75f0e0690e Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 24 May 2020 13:37:23 -0400 Subject: Style adjustments (#537) * Remove newlines for term-definition-item-template * Update how action button icons are styled * Fix spacing * Group text colors together * Correct image styles * Fix missing --- ext/bg/search.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ext/bg/search.html') diff --git a/ext/bg/search.html b/ext/bg/search.html index f3f156d8..c0721e5c 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -54,8 +54,8 @@
-- cgit v1.2.3 From 9767b765536279023045ed4280b12d297ec78f0a Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 7 Jun 2020 21:40:11 -0400 Subject: Use cross frame API (#553) * Use new CrossFrameAPI for popup proxy communication * Remove use of old cross-frame communication classes * Remove use of old cross-frame communication files * Make the crossFrame object a member of the api object --- ext/bg/background.html | 1 - ext/bg/context.html | 1 + ext/bg/js/backend-api-forwarder.js | 44 ------------- ext/bg/js/backend.js | 4 -- ext/bg/js/search-main.js | 1 - ext/bg/search.html | 1 + ext/bg/settings-popup-preview.html | 2 +- ext/bg/settings.html | 1 + ext/fg/float.html | 1 + ext/fg/js/float-main.js | 1 - ext/fg/js/frontend-api-receiver.js | 76 ---------------------- ext/fg/js/frontend-api-sender.js | 128 ------------------------------------- ext/fg/js/popup-factory.js | 7 +- ext/fg/js/popup-proxy.js | 6 +- ext/manifest.json | 3 +- ext/mixed/js/api.js | 18 +++++- 16 files changed, 29 insertions(+), 266 deletions(-) delete mode 100644 ext/bg/js/backend-api-forwarder.js delete mode 100644 ext/fg/js/frontend-api-receiver.js delete mode 100644 ext/fg/js/frontend-api-sender.js (limited to 'ext/bg/search.html') diff --git a/ext/bg/background.html b/ext/bg/background.html index 53e8b140..d51858a7 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -28,7 +28,6 @@ - diff --git a/ext/bg/context.html b/ext/bg/context.html index 93012d70..89695d0e 100644 --- a/ext/bg/context.html +++ b/ext/bg/context.html @@ -180,6 +180,7 @@ + diff --git a/ext/bg/js/backend-api-forwarder.js b/ext/bg/js/backend-api-forwarder.js deleted file mode 100644 index 4ac12730..00000000 --- a/ext/bg/js/backend-api-forwarder.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2019-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 . - */ - - -class BackendApiForwarder { - prepare() { - chrome.runtime.onConnect.addListener(this._onConnect.bind(this)); - } - - _onConnect(port) { - if (port.name !== 'backend-api-forwarder') { return; } - - let tabId; - if (!( - port.sender && - port.sender.tab && - (typeof (tabId = port.sender.tab.id)) === 'number' - )) { - port.disconnect(); - return; - } - - const forwardPort = chrome.tabs.connect(tabId, {name: 'frontend-api-receiver'}); - - port.onMessage.addListener((message) => forwardPort.postMessage(message)); - forwardPort.onMessage.addListener((message) => port.postMessage(message)); - port.onDisconnect.addListener(() => forwardPort.disconnect()); - forwardPort.onDisconnect.addListener(() => port.disconnect()); - } -} diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 5eb7982d..7971d16f 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -20,7 +20,6 @@ * AnkiNoteBuilder * AudioSystem * AudioUriBuilder - * BackendApiForwarder * ClipboardMonitor * Database * DictionaryImporter @@ -76,9 +75,6 @@ class Backend { this.popupWindow = null; - const apiForwarder = new BackendApiForwarder(); - apiForwarder.prepare(); - this._defaultBrowserActionTitle = null; this._isPrepared = false; this._prepareError = false; diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js index 3e089594..f18d6d88 100644 --- a/ext/bg/js/search-main.js +++ b/ext/bg/js/search-main.js @@ -24,7 +24,6 @@ async function injectSearchFrontend() { await dynamicLoader.loadScripts([ '/mixed/js/text-scanner.js', - '/fg/js/frontend-api-receiver.js', '/fg/js/frame-offset-forwarder.js', '/fg/js/popup.js', '/fg/js/popup-factory.js', diff --git a/ext/bg/search.html b/ext/bg/search.html index c0721e5c..de08cdae 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -71,6 +71,7 @@ + diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index 2f0b841b..fe92f24f 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -119,13 +119,13 @@ + - diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 7c295f0d..a530534c 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1121,6 +1121,7 @@ + diff --git a/ext/fg/float.html b/ext/fg/float.html index e9f5acae..17dbcc6d 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -40,6 +40,7 @@ + diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js index 249b4dbe..2ec334c8 100644 --- a/ext/fg/js/float-main.js +++ b/ext/fg/js/float-main.js @@ -24,7 +24,6 @@ async function injectPopupNested() { await dynamicLoader.loadScripts([ '/mixed/js/text-scanner.js', - '/fg/js/frontend-api-sender.js', '/fg/js/popup.js', '/fg/js/popup-proxy.js', '/fg/js/frontend.js', diff --git a/ext/fg/js/frontend-api-receiver.js b/ext/fg/js/frontend-api-receiver.js deleted file mode 100644 index 3fa9e8b6..00000000 --- a/ext/fg/js/frontend-api-receiver.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2019-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 . - */ - - -class FrontendApiReceiver { - constructor(source, messageHandlers) { - this._source = source; - this._messageHandlers = messageHandlers; - } - - prepare() { - chrome.runtime.onConnect.addListener(this._onConnect.bind(this)); - } - - _onConnect(port) { - if (port.name !== 'frontend-api-receiver') { return; } - - port.onMessage.addListener(this._onMessage.bind(this, port)); - } - - _onMessage(port, {id, action, params, target, senderId}) { - if (target !== this._source) { return; } - - const messageHandler = this._messageHandlers.get(action); - if (typeof messageHandler === 'undefined') { return; } - - const {handler, async} = messageHandler; - - this._sendAck(port, id, senderId); - if (async) { - this._invokeHandlerAsync(handler, params, port, id, senderId); - } else { - this._invokeHandler(handler, params, port, id, senderId); - } - } - - _invokeHandler(handler, params, port, id, senderId) { - try { - const result = handler(params); - this._sendResult(port, id, senderId, {result}); - } catch (error) { - this._sendResult(port, id, senderId, {error: errorToJson(error)}); - } - } - - async _invokeHandlerAsync(handler, params, port, id, senderId) { - try { - const result = await handler(params); - this._sendResult(port, id, senderId, {result}); - } catch (error) { - this._sendResult(port, id, senderId, {error: errorToJson(error)}); - } - } - - _sendAck(port, id, senderId) { - port.postMessage({type: 'ack', id, senderId}); - } - - _sendResult(port, id, senderId, data) { - port.postMessage({type: 'result', id, senderId, data}); - } -} diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js deleted file mode 100644 index 4dcde638..00000000 --- a/ext/fg/js/frontend-api-sender.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2019-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 . - */ - - -class FrontendApiSender { - constructor(target) { - this._target = target; - this._senderId = yomichan.generateId(16); - this._ackTimeout = 3000; // 3 seconds - this._responseTimeout = 10000; // 10 seconds - this._callbacks = new Map(); - this._disconnected = false; - this._nextId = 0; - this._port = null; - } - - invoke(action, params) { - if (this._disconnected) { - // attempt to reconnect the next time - this._disconnected = false; - return Promise.reject(new Error('Disconnected')); - } - - if (this._port === null) { - this._createPort(); - } - - const id = `${this._nextId}`; - ++this._nextId; - - return new Promise((resolve, reject) => { - const info = {id, resolve, reject, ack: false, timer: null}; - this._callbacks.set(id, info); - info.timer = setTimeout(() => this._onError(id, 'Timeout (ack)'), this._ackTimeout); - - this._port.postMessage({id, action, params, target: this._target, senderId: this._senderId}); - }); - } - - _createPort() { - this._port = chrome.runtime.connect(null, {name: 'backend-api-forwarder'}); - this._port.onDisconnect.addListener(this._onDisconnect.bind(this)); - this._port.onMessage.addListener(this._onMessage.bind(this)); - } - - _onMessage({type, id, data, senderId}) { - if (senderId !== this._senderId) { return; } - switch (type) { - case 'ack': - this._onAck(id); - break; - case 'result': - this._onResult(id, data); - break; - } - } - - _onDisconnect() { - this._disconnected = true; - this._port = null; - - for (const id of this._callbacks.keys()) { - this._onError(id, 'Disconnected'); - } - } - - _onAck(id) { - const info = this._callbacks.get(id); - if (typeof info === 'undefined') { - yomichan.logWarning(new Error(`ID ${id} not found for ack`)); - return; - } - - if (info.ack) { - yomichan.logWarning(new Error(`Request ${id} already ack'd`)); - return; - } - - info.ack = true; - clearTimeout(info.timer); - info.timer = setTimeout(() => this._onError(id, 'Timeout (response)'), this._responseTimeout); - } - - _onResult(id, data) { - const info = this._callbacks.get(id); - if (typeof info === 'undefined') { - yomichan.logWarning(new Error(`ID ${id} not found`)); - return; - } - - if (!info.ack) { - yomichan.logWarning(new Error(`Request ${id} not ack'd`)); - return; - } - - this._callbacks.delete(id); - clearTimeout(info.timer); - info.timer = null; - - if (typeof data.error !== 'undefined') { - info.reject(jsonToError(data.error)); - } else { - info.resolve(data.result); - } - } - - _onError(id, reason) { - const info = this._callbacks.get(id); - if (typeof info === 'undefined') { return; } - this._callbacks.delete(id); - info.timer = null; - info.reject(new Error(reason)); - } -} diff --git a/ext/fg/js/popup-factory.js b/ext/fg/js/popup-factory.js index b10acbaf..b5997253 100644 --- a/ext/fg/js/popup-factory.js +++ b/ext/fg/js/popup-factory.js @@ -16,8 +16,8 @@ */ /* global - * FrontendApiReceiver * Popup + * api */ class PopupFactory { @@ -29,7 +29,7 @@ class PopupFactory { // Public functions async prepare() { - const apiReceiver = new FrontendApiReceiver(`popup-factory#${this._frameId}`, new Map([ + api.crossFrame.registerHandlers([ ['getOrCreatePopup', {async: false, handler: this._onApiGetOrCreatePopup.bind(this)}], ['setOptionsContext', {async: true, handler: this._onApiSetOptionsContext.bind(this)}], ['hide', {async: false, handler: this._onApiHide.bind(this)}], @@ -41,8 +41,7 @@ class PopupFactory { ['clearAutoPlayTimer', {async: false, handler: this._onApiClearAutoPlayTimer.bind(this)}], ['setContentScale', {async: false, handler: this._onApiSetContentScale.bind(this)}], ['getUrl', {async: false, handler: this._onApiGetUrl.bind(this)}] - ])); - apiReceiver.prepare(); + ]); } getOrCreatePopup(id=null, parentId=null, depth=null) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 82da839a..3387d941 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -16,7 +16,7 @@ */ /* global - * FrontendApiSender + * api */ class PopupProxy { @@ -24,7 +24,7 @@ class PopupProxy { this._id = id; this._depth = depth; this._parentPopupId = parentPopupId; - this._apiSender = new FrontendApiSender(`popup-factory#${parentFrameId}`); + this._parentFrameId = parentFrameId; this._getFrameOffset = getFrameOffset; this._setDisabled = setDisabled; @@ -111,7 +111,7 @@ class PopupProxy { // Private _invoke(action, params={}) { - return this._apiSender.invoke(action, params); + return api.crossFrame.invoke(this._parentFrameId, action, params); } async _updateFrameOffset() { diff --git a/ext/manifest.json b/ext/manifest.json index 6db257d7..75334675 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -36,13 +36,12 @@ "matches": ["http://*/*", "https://*/*", "file://*/*"], "js": [ "mixed/js/core.js", + "mixed/js/comm.js", "mixed/js/dom.js", "mixed/js/api.js", "mixed/js/dynamic-loader.js", "mixed/js/text-scanner.js", "fg/js/document.js", - "fg/js/frontend-api-sender.js", - "fg/js/frontend-api-receiver.js", "fg/js/popup.js", "fg/js/source.js", "fg/js/popup-factory.js", diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 5e3195d6..c54196e2 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -15,10 +15,23 @@ * along with this program. If not, see . */ +/* global + * CrossFrameAPI + */ + const api = (() => { class API { constructor() { this._forwardLogsToBackendEnabled = false; + this._crossFrame = new CrossFrameAPI(); + } + + get crossFrame() { + return this._crossFrame; + } + + prepare() { + this._crossFrame.prepare(); } forwardLogsToBackend() { @@ -331,5 +344,8 @@ const api = (() => { } } - return new API(); + // eslint-disable-next-line no-shadow + const api = new API(); + api.prepare(); + return api; })(); -- cgit v1.2.3 From e23504613f8526b90a497512c086ed48e66cde95 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 21 Jun 2020 16:07:51 -0400 Subject: Use DOMTextScanner (#536) * Use DOMTextScanner instead of TextSourceRange.seek* * Move getNodesInRange to dom.js * Move anyNodeMatchesSelector to dom.js * Remove unused functions * Update tests * Add layoutAwareScan option * Use layoutAwareScan for source and sentence scanning * Remove unused IGNORE_TEXT_PATTERN --- ext/bg/data/options-schema.json | 7 +- ext/bg/js/options.js | 3 +- ext/bg/js/search-query-parser.js | 8 +- ext/bg/search.html | 1 + ext/bg/settings-popup-preview.html | 1 + ext/bg/settings.html | 4 + ext/fg/float.html | 1 + ext/fg/js/document.js | 11 +- ext/fg/js/frontend.js | 14 ++- ext/fg/js/source.js | 224 ++----------------------------------- ext/manifest.json | 1 + ext/mixed/js/display.js | 11 +- ext/mixed/js/dom.js | 38 +++++++ ext/mixed/js/text-scanner.js | 11 +- test/test-document.js | 14 ++- 15 files changed, 102 insertions(+), 247 deletions(-) (limited to 'ext/bg/search.html') diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json index 0379fa75..5885e036 100644 --- a/ext/bg/data/options-schema.json +++ b/ext/bg/data/options-schema.json @@ -321,7 +321,8 @@ "enablePopupSearch", "enableOnPopupExpressions", "enableOnSearchPage", - "enableSearchTags" + "enableSearchTags", + "layoutAwareScan" ], "properties": { "middleMouse": { @@ -383,6 +384,10 @@ "enableSearchTags": { "type": "boolean", "default": false + }, + "layoutAwareScan": { + "type": "boolean", + "default": false } } }, diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 97368a0b..170e4799 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -203,7 +203,8 @@ function profileOptionsCreateDefaults() { enablePopupSearch: false, enableOnPopupExpressions: false, enableOnSearchPage: true, - enableSearchTags: false + enableSearchTags: false, + layoutAwareScan: false }, translation: { diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index addfc686..97e98b40 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -75,15 +75,17 @@ class QueryParser { async _search(textSource, cause) { if (textSource === null) { return null; } - const searchText = this._textScanner.getTextSourceContent(textSource, this._options.scanning.length); + const {length: scanLength, layoutAwareScan} = this._options.scanning; + const searchText = this._textScanner.getTextSourceContent(textSource, scanLength, layoutAwareScan); if (searchText.length === 0) { return null; } const {definitions, length} = await api.termsFind(searchText, {}, this._getOptionsContext()); if (definitions.length === 0) { return null; } - const sentence = docSentenceExtract(textSource, this._options.anki.sentenceExt); + const sentenceExtent = this._options.anki.sentenceExt; + const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); - textSource.setEndOffset(length); + textSource.setEndOffset(length, layoutAwareScan); this._setContent('terms', {definitions, context: { focus: false, diff --git a/ext/bg/search.html b/ext/bg/search.html index de08cdae..4a28dd88 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -79,6 +79,7 @@ + diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index fe92f24f..5eecd005 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -126,6 +126,7 @@ + diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 118a13b9..77b61aef 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -400,6 +400,10 @@ +
+ +
+
diff --git a/ext/fg/float.html b/ext/fg/float.html index 17dbcc6d..3e41cde5 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -46,6 +46,7 @@ + diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index d639bc86..c288502c 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -17,6 +17,7 @@ /* global * DOM + * DOMTextScanner * TextSourceElement * TextSourceRange */ @@ -152,14 +153,14 @@ function docRangeFromPoint(x, y, deepDomScan) { } } -function docSentenceExtract(source, extent) { +function docSentenceExtract(source, extent, layoutAwareScan) { const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'}; const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'}; const terminators = '…。..??!!'; const sourceLocal = source.clone(); - const position = sourceLocal.setStartOffset(extent); - sourceLocal.setEndOffset(extent * 2 - position, true); + const position = sourceLocal.setStartOffset(extent, layoutAwareScan); + sourceLocal.setEndOffset(extent * 2 - position, layoutAwareScan, true); const content = sourceLocal.text(); let quoteStack = []; @@ -232,7 +233,7 @@ function isPointInRange(x, y, range) { const nodePre = range.endContainer; const offsetPre = range.endOffset; try { - const {node, offset, content} = TextSourceRange.seekForward(range.endContainer, range.endOffset, 1); + 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())) { @@ -243,7 +244,7 @@ function isPointInRange(x, y, range) { } // Scan backward - const {node, offset, content} = TextSourceRange.seekBackward(range.startContainer, range.startOffset, 1); + 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())) { diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 70bd8a48..ab455c09 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -258,32 +258,36 @@ class Frontend { } async _findTerms(textSource, optionsContext) { - const searchText = this._textScanner.getTextSourceContent(textSource, this._options.scanning.length); + const {length: scanLength, layoutAwareScan} = this._options.scanning; + const searchText = this._textScanner.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); + textSource.setEndOffset(length, layoutAwareScan); return {definitions, type: 'terms'}; } async _findKanji(textSource, optionsContext) { - const searchText = this._textScanner.getTextSourceContent(textSource, 1); + const layoutAwareScan = this._options.scanning.layoutAwareScan; + const searchText = this._textScanner.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); + textSource.setEndOffset(1, layoutAwareScan); return {definitions, type: 'kanji'}; } _showContent(textSource, focus, definitions, type, optionsContext) { const {url} = optionsContext; - const sentence = docSentenceExtract(textSource, this._options.anki.sentenceExt); + const sentenceExtent = this._options.anki.sentenceExt; + const layoutAwareScan = this._options.scanning.layoutAwareScan; + const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); this._showPopupContent( textSource, optionsContext, diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index fa4706f2..38810f07 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -15,9 +15,9 @@ * along with this program. If not, see . */ -// \u200c (Zero-width non-joiner) appears on Google Docs from Chrome 76 onwards -const IGNORE_TEXT_PATTERN = /\u200c/; - +/* global + * DOMTextScanner + */ /* * TextSourceRange @@ -46,19 +46,19 @@ class TextSourceRange { return this.content; } - setEndOffset(length, fromEnd=false) { + setEndOffset(length, layoutAwareScan, fromEnd=false) { const state = ( fromEnd ? - TextSourceRange.seekForward(this.range.endContainer, this.range.endOffset, length) : - TextSourceRange.seekForward(this.range.startContainer, this.range.startOffset, length) + new DOMTextScanner(this.range.endContainer, this.range.endOffset, !layoutAwareScan, layoutAwareScan).seek(length) : + new DOMTextScanner(this.range.startContainer, this.range.startOffset, !layoutAwareScan, layoutAwareScan).seek(length) ); this.range.setEnd(state.node, state.offset); this.content = (fromEnd ? this.content + state.content : state.content); return length - state.remainder; } - setStartOffset(length) { - const state = TextSourceRange.seekBackward(this.range.startContainer, this.range.startOffset, length); + setStartOffset(length, layoutAwareScan) { + const state = new DOMTextScanner(this.range.startContainer, this.range.startOffset, !layoutAwareScan, layoutAwareScan).seek(-length); this.range.setStart(state.node, state.offset); this.rangeStartOffset = this.range.startOffset; this.content = state.content + this.content; @@ -110,154 +110,6 @@ class TextSourceRange { } } - static shouldEnter(node) { - switch (node.nodeName.toUpperCase()) { - case 'RT': - case 'SCRIPT': - case 'STYLE': - return false; - } - - const style = window.getComputedStyle(node); - return !( - style.visibility === 'hidden' || - style.display === 'none' || - parseFloat(style.fontSize) === 0 - ); - } - - static getRubyElement(node) { - node = TextSourceRange.getParentElement(node); - if (node !== null && node.nodeName.toUpperCase() === 'RT') { - node = node.parentNode; - return (node !== null && node.nodeName.toUpperCase() === 'RUBY') ? node : null; - } - return null; - } - - static seekForward(node, offset, length) { - const state = {node, offset, remainder: length, content: ''}; - if (length <= 0) { - return state; - } - - const TEXT_NODE = Node.TEXT_NODE; - const ELEMENT_NODE = Node.ELEMENT_NODE; - let resetOffset = false; - - const ruby = TextSourceRange.getRubyElement(node); - if (ruby !== null) { - node = ruby; - resetOffset = true; - } - - while (node !== null) { - let visitChildren = true; - const nodeType = node.nodeType; - - if (nodeType === TEXT_NODE) { - state.node = node; - if (TextSourceRange.seekForwardTextNode(state, resetOffset)) { - break; - } - resetOffset = true; - } else if (nodeType === ELEMENT_NODE) { - visitChildren = TextSourceRange.shouldEnter(node); - } - - node = TextSourceRange.getNextNode(node, visitChildren); - } - - return state; - } - - static seekForwardTextNode(state, resetOffset) { - const nodeValue = state.node.nodeValue; - const nodeValueLength = nodeValue.length; - let content = state.content; - let offset = resetOffset ? 0 : state.offset; - let remainder = state.remainder; - let result = false; - - for (; offset < nodeValueLength; ++offset) { - const c = nodeValue[offset]; - if (!IGNORE_TEXT_PATTERN.test(c)) { - content += c; - if (--remainder <= 0) { - result = true; - ++offset; - break; - } - } - } - - state.offset = offset; - state.content = content; - state.remainder = remainder; - return result; - } - - static seekBackward(node, offset, length) { - const state = {node, offset, remainder: length, content: ''}; - if (length <= 0) { - return state; - } - - const TEXT_NODE = Node.TEXT_NODE; - const ELEMENT_NODE = Node.ELEMENT_NODE; - let resetOffset = false; - - const ruby = TextSourceRange.getRubyElement(node); - if (ruby !== null) { - node = ruby; - resetOffset = true; - } - - while (node !== null) { - let visitChildren = true; - const nodeType = node.nodeType; - - if (nodeType === TEXT_NODE) { - state.node = node; - if (TextSourceRange.seekBackwardTextNode(state, resetOffset)) { - break; - } - resetOffset = true; - } else if (nodeType === ELEMENT_NODE) { - visitChildren = TextSourceRange.shouldEnter(node); - } - - node = TextSourceRange.getPreviousNode(node, visitChildren); - } - - return state; - } - - static seekBackwardTextNode(state, resetOffset) { - const nodeValue = state.node.nodeValue; - let content = state.content; - let offset = resetOffset ? nodeValue.length : state.offset; - let remainder = state.remainder; - let result = false; - - for (; offset > 0; --offset) { - const c = nodeValue[offset - 1]; - if (!IGNORE_TEXT_PATTERN.test(c)) { - content = c + content; - if (--remainder <= 0) { - result = true; - --offset; - break; - } - } - } - - state.offset = offset; - state.content = content; - state.remainder = remainder; - return result; - } - static getParentElement(node) { while (node !== null && node.nodeType !== Node.ELEMENT_NODE) { node = node.parentNode; @@ -290,66 +142,6 @@ class TextSourceRange { return writingMode; } } - - static getNodesInRange(range) { - const end = range.endContainer; - const nodes = []; - for (let node = range.startContainer; node !== null; node = TextSourceRange.getNextNode(node, true)) { - nodes.push(node); - if (node === end) { break; } - } - return nodes; - } - - static getNextNode(node, visitChildren) { - let next = visitChildren ? node.firstChild : null; - if (next === null) { - while (true) { - next = node.nextSibling; - if (next !== null) { break; } - - next = node.parentNode; - if (next === null) { break; } - - node = next; - } - } - return next; - } - - static getPreviousNode(node, visitChildren) { - let next = visitChildren ? node.lastChild : null; - if (next === null) { - while (true) { - next = node.previousSibling; - if (next !== null) { break; } - - next = node.parentNode; - if (next === null) { break; } - - node = next; - } - } - return next; - } - - static anyNodeMatchesSelector(nodeList, selector) { - for (const node of nodeList) { - if (TextSourceRange.nodeMatchesSelector(node, selector)) { - return true; - } - } - return false; - } - - static nodeMatchesSelector(node, selector) { - for (; node !== null; node = node.parentNode) { - if (node.nodeType === Node.ELEMENT_NODE) { - return node.matches(selector); - } - } - return false; - } } diff --git a/ext/manifest.json b/ext/manifest.json index 75334675..4d4f0c06 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -42,6 +42,7 @@ "mixed/js/dynamic-loader.js", "mixed/js/text-scanner.js", "fg/js/document.js", + "fg/js/dom-text-scanner.js", "fg/js/popup.js", "fg/js/source.js", "fg/js/popup-factory.js", diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 90fd1037..1d699706 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -236,7 +236,9 @@ class Display { const {textSource, definitions} = termLookupResults; const scannedElement = e.target; - const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); + const sentenceExtent = this.options.anki.sentenceExt; + const layoutAwareScan = this.options.scanning.layoutAwareScan; + const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); this.context.update({ index: this.entryIndexFind(scannedElement), @@ -273,21 +275,22 @@ class Display { try { e.preventDefault(); - const textSource = docRangeFromPoint(e.clientX, e.clientY, this.options.scanning.deepDomScan); + const {length: scanLength, deepDomScan: deepScan, layoutAwareScan} = this.options.scanning; + const textSource = docRangeFromPoint(e.clientX, e.clientY, deepScan); if (textSource === null) { return false; } let definitions, length; try { - textSource.setEndOffset(this.options.scanning.length); + textSource.setEndOffset(scanLength, layoutAwareScan); ({definitions, length} = await api.termsFind(textSource.text(), {}, this.getOptionsContext())); if (definitions.length === 0) { return false; } - textSource.setEndOffset(length); + textSource.setEndOffset(length, layoutAwareScan); } finally { textSource.cleanup(); } diff --git a/ext/mixed/js/dom.js b/ext/mixed/js/dom.js index 0e8f4462..05764443 100644 --- a/ext/mixed/js/dom.js +++ b/ext/mixed/js/dom.js @@ -86,4 +86,42 @@ class DOM { null ); } + + static getNodesInRange(range) { + const end = range.endContainer; + const nodes = []; + for (let node = range.startContainer; node !== null; node = DOM.getNextNode(node)) { + nodes.push(node); + if (node === end) { break; } + } + return nodes; + } + + static getNextNode(node) { + let next = node.firstChild; + if (next === null) { + while (true) { + next = node.nextSibling; + if (next !== null) { break; } + + next = node.parentNode; + if (next === null) { break; } + + node = next; + } + } + return next; + } + + static anyNodeMatchesSelector(nodes, selector) { + const ELEMENT_NODE = Node.ELEMENT_NODE; + for (let node of nodes) { + for (; node !== null; node = node.parentNode) { + if (node.nodeType !== ELEMENT_NODE) { continue; } + if (node.matches(selector)) { return true; } + break; + } + } + return false; + } } diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index b8688b08..fb275452 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -17,7 +17,6 @@ /* global * DOM - * TextSourceRange * docRangeFromPoint */ @@ -119,20 +118,20 @@ class TextScanner extends EventDispatcher { } } - getTextSourceContent(textSource, length) { + getTextSourceContent(textSource, length, layoutAwareScan) { const clonedTextSource = textSource.clone(); - clonedTextSource.setEndOffset(length); + clonedTextSource.setEndOffset(length, layoutAwareScan); if (this._ignoreNodes !== null && clonedTextSource.range) { length = clonedTextSource.text().length; while (clonedTextSource.range && length > 0) { - const nodes = TextSourceRange.getNodesInRange(clonedTextSource.range); - if (!TextSourceRange.anyNodeMatchesSelector(nodes, this._ignoreNodes)) { + const nodes = DOM.getNodesInRange(clonedTextSource.range); + if (!DOM.anyNodeMatchesSelector(nodes, this._ignoreNodes)) { break; } --length; - clonedTextSource.setEndOffset(length); + clonedTextSource.setEndOffset(length, layoutAwareScan); } } diff --git a/test/test-document.js b/test/test-document.js index 0d9026db..ba7acc49 100644 --- a/test/test-document.js +++ b/test/test-document.js @@ -94,10 +94,12 @@ async function testDocument1() { const vm = new VM({document, window, Range, Node}); vm.execute([ 'mixed/js/dom.js', + 'fg/js/dom-text-scanner.js', 'fg/js/source.js', 'fg/js/document.js' ]); - const [TextSourceRange, TextSourceElement, docRangeFromPoint, docSentenceExtract] = vm.get([ + const [DOMTextScanner, TextSourceRange, TextSourceElement, docRangeFromPoint, docSentenceExtract] = vm.get([ + 'DOMTextScanner', 'TextSourceRange', 'TextSourceElement', 'docRangeFromPoint', @@ -106,7 +108,7 @@ async function testDocument1() { try { await testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSentenceExtract, TextSourceRange, TextSourceElement}); - await testTextSourceRangeSeekFunctions(dom, {TextSourceRange}); + await testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}); } finally { window.close(); } @@ -179,7 +181,7 @@ async function testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSen if (source === null) { continue; } // Test docSentenceExtract - const sentenceActual = docSentenceExtract(source, sentenceExtent).text; + const sentenceActual = docSentenceExtract(source, sentenceExtent, false).text; assert.strictEqual(sentenceActual, sentence); // Clean @@ -187,7 +189,7 @@ async function testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSen } } -async function testTextSourceRangeSeekFunctions(dom, {TextSourceRange}) { +async function testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}) { const document = dom.window.document; for (const testElement of document.querySelectorAll('.test[data-test-type=text-source-range-seek]')) { @@ -220,8 +222,8 @@ async function testTextSourceRangeSeekFunctions(dom, {TextSourceRange}) { const {node, offset, content} = ( seekDirection === 'forward' ? - TextSourceRange.seekForward(seekNode, seekOffset, seekLength) : - TextSourceRange.seekBackward(seekNode, seekOffset, seekLength) + new DOMTextScanner(seekNode, seekOffset, true, false).seek(seekLength) : + new DOMTextScanner(seekNode, seekOffset, true, false).seek(-seekLength) ); assert.strictEqual(node, expectedResultNode); -- cgit v1.2.3