aboutsummaryrefslogtreecommitdiff
path: root/ext/fg/js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/fg/js')
-rw-r--r--ext/fg/js/content-script-main.js148
-rw-r--r--ext/fg/js/document.js11
-rw-r--r--ext/fg/js/float-main.js47
-rw-r--r--ext/fg/js/float.js76
-rw-r--r--ext/fg/js/frame-offset-forwarder.js34
-rw-r--r--ext/fg/js/frontend-api-receiver.js76
-rw-r--r--ext/fg/js/frontend-api-sender.js128
-rw-r--r--ext/fg/js/frontend.js253
-rw-r--r--ext/fg/js/popup-factory.js16
-rw-r--r--ext/fg/js/popup-proxy.js28
-rw-r--r--ext/fg/js/popup.js88
-rw-r--r--ext/fg/js/source.js224
12 files changed, 413 insertions, 716 deletions
diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js
index 57386b85..1f3a69e5 100644
--- a/ext/fg/js/content-script-main.js
+++ b/ext/fg/js/content-script-main.js
@@ -16,141 +16,31 @@
*/
/* global
- * DOM
- * FrameOffsetForwarder
* Frontend
* PopupFactory
- * PopupProxy
- * apiBroadcastTab
- * apiForwardLogsToBackend
- * apiFrameInformationGet
- * apiOptionsGet
+ * api
*/
-async function createIframePopupProxy(frameOffsetForwarder, setDisabled) {
- const rootPopupInformationPromise = yomichan.getTemporaryListenerResult(
- chrome.runtime.onMessage,
- ({action, params}, {resolve}) => {
- if (action === 'rootPopupInformation') {
- resolve(params);
- }
- }
- );
- apiBroadcastTab('rootPopupRequestInformationBroadcast');
- const {popupId, frameId: parentFrameId} = await rootPopupInformationPromise;
-
- const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder);
-
- const popup = new PopupProxy(popupId, 0, null, parentFrameId, getFrameOffset, setDisabled);
- await popup.prepare();
-
- return popup;
-}
-
-async function getOrCreatePopup(depth) {
- const {frameId} = await apiFrameInformationGet();
- if (typeof frameId !== 'number') {
- const error = new Error('Failed to get frameId');
- yomichan.logError(error);
- throw error;
- }
-
- const popupFactory = new PopupFactory(frameId);
- await popupFactory.prepare();
-
- const popup = popupFactory.getOrCreatePopup(null, null, depth);
-
- return popup;
-}
-
-async function createPopupProxy(depth, id, parentFrameId) {
- const popup = new PopupProxy(null, depth + 1, id, parentFrameId);
- await popup.prepare();
-
- return popup;
-}
-
(async () => {
- apiForwardLogsToBackend();
- await yomichan.prepare();
-
- const data = window.frontendInitializationData || {};
- const {id, depth=0, parentFrameId, url=window.location.href, proxy=false, isSearchPage=false} = data;
-
- const isIframe = !proxy && (window !== window.parent);
+ try {
+ api.forwardLogsToBackend();
+ await yomichan.prepare();
- const popups = {
- iframe: null,
- proxy: null,
- normal: null
- };
-
- let frontend = null;
- let frontendPreparePromise = null;
- let frameOffsetForwarder = null;
-
- let iframePopupsInRootFrameAvailable = true;
-
- const disableIframePopupsInRootFrame = () => {
- iframePopupsInRootFrameAvailable = false;
- applyOptions();
- };
-
- let urlUpdatedAt = 0;
- let popupProxyUrlCached = url;
- const getPopupProxyUrl = async () => {
- const now = Date.now();
- if (popups.proxy !== null && now - urlUpdatedAt > 500) {
- popupProxyUrlCached = await popups.proxy.getUrl();
- urlUpdatedAt = now;
- }
- return popupProxyUrlCached;
- };
-
- const applyOptions = async () => {
- const optionsContext = {
- depth: isSearchPage ? 0 : depth,
- url: proxy ? await getPopupProxyUrl() : window.location.href
- };
- const options = await apiOptionsGet(optionsContext);
-
- if (!proxy && frameOffsetForwarder === null) {
- frameOffsetForwarder = new FrameOffsetForwarder();
- frameOffsetForwarder.start();
+ const {frameId} = await api.frameInformationGet();
+ if (typeof frameId !== 'number') {
+ throw new Error('Failed to get frameId');
}
- let popup;
- if (isIframe && options.general.showIframePopupsInRootFrame && DOM.getFullscreenElement() === null && iframePopupsInRootFrameAvailable) {
- popup = popups.iframe || await createIframePopupProxy(frameOffsetForwarder, disableIframePopupsInRootFrame);
- popups.iframe = popup;
- } else if (proxy) {
- popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId);
- popups.proxy = popup;
- } else {
- popup = popups.normal || await getOrCreatePopup(depth);
- popups.normal = popup;
- }
-
- if (frontend === null) {
- const getUrl = proxy ? getPopupProxyUrl : null;
- frontend = new Frontend(popup, getUrl);
- frontendPreparePromise = frontend.prepare();
- await frontendPreparePromise;
- } else {
- await frontendPreparePromise;
- if (isSearchPage) {
- const disabled = !options.scanning.enableOnSearchPage;
- frontend.setDisabledOverride(disabled);
- }
-
- if (isIframe) {
- await frontend.setPopup(popup);
- }
- }
- };
-
- yomichan.on('optionsUpdated', applyOptions);
- window.addEventListener('fullscreenchange', applyOptions, false);
-
- await applyOptions();
+ const popupFactory = new PopupFactory(frameId);
+ popupFactory.prepare();
+
+ const frontend = new Frontend(
+ frameId,
+ popupFactory,
+ {}
+ );
+ await frontend.prepare();
+ } catch (e) {
+ yomichan.logError(e);
+ }
})();
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/float-main.js b/ext/fg/js/float-main.js
index 20771910..3bedfe58 100644
--- a/ext/fg/js/float-main.js
+++ b/ext/fg/js/float-main.js
@@ -17,45 +17,16 @@
/* global
* DisplayFloat
- * apiForwardLogsToBackend
- * apiOptionsGet
- * dynamicLoader
+ * api
*/
-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',
- '/fg/js/content-script-main.js'
- ]);
-}
-
-async function popupNestedInitialize(id, depth, parentFrameId, url) {
- let optionsApplied = false;
-
- const applyOptions = async () => {
- const optionsContext = {depth, url};
- const options = await apiOptionsGet(optionsContext);
- const maxPopupDepthExceeded = !(typeof depth === 'number' && depth < options.scanning.popupNestingMaxDepth);
- if (maxPopupDepthExceeded || optionsApplied) { return; }
-
- optionsApplied = true;
- yomichan.off('optionsUpdated', applyOptions);
-
- window.frontendInitializationData = {id, depth, parentFrameId, url, proxy: true};
- await injectPopupNested();
- };
-
- yomichan.on('optionsUpdated', applyOptions);
-
- await applyOptions();
-}
-
(async () => {
- apiForwardLogsToBackend();
- const display = new DisplayFloat();
- await display.prepare();
+ try {
+ api.forwardLogsToBackend();
+
+ const display = new DisplayFloat();
+ await display.prepare();
+ } catch (e) {
+ yomichan.logError(e);
+ }
})();
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js
index 845bf7f6..d7beb675 100644
--- a/ext/fg/js/float.js
+++ b/ext/fg/js/float.js
@@ -17,9 +17,10 @@
/* global
* Display
- * apiBroadcastTab
- * apiSendMessageToFrame
- * popupNestedInitialize
+ * Frontend
+ * PopupFactory
+ * api
+ * dynamicLoader
*/
class DisplayFloat extends Display {
@@ -31,7 +32,7 @@ class DisplayFloat extends Display {
this._token = null;
this._orphaned = false;
- this._initializedNestedPopups = false;
+ this._nestedPopupsPrepared = false;
this._onKeyDownHandlers = new Map([
['C', (e) => {
@@ -61,7 +62,7 @@ class DisplayFloat extends Display {
yomichan.on('orphaned', this.onOrphaned.bind(this));
window.addEventListener('message', this.onMessage.bind(this), false);
- apiBroadcastTab('popupPrepared', {secret: this._secret});
+ api.broadcastTab('popupPrepared', {secret: this._secret});
}
onError(error) {
@@ -153,7 +154,7 @@ class DisplayFloat extends Display {
},
2000
);
- apiBroadcastTab('requestDocumentInformationBroadcast', {uniqueId});
+ api.broadcastTab('requestDocumentInformationBroadcast', {uniqueId});
const {title} = await promise;
return title;
@@ -176,7 +177,7 @@ class DisplayFloat extends Display {
const {token, frameId} = params;
this._token = token;
- apiSendMessageToFrame(frameId, 'popupInitialized', {secret, token});
+ api.sendMessageToFrame(frameId, 'popupInitialized', {secret, token});
}
async _configure({messageId, frameId, popupId, optionsContext, childrenSupported, scale}) {
@@ -184,15 +185,15 @@ class DisplayFloat extends Display {
await this.updateOptions();
- if (childrenSupported && !this._initializedNestedPopups) {
+ if (childrenSupported && !this._nestedPopupsPrepared) {
const {depth, url} = optionsContext;
- popupNestedInitialize(popupId, depth, frameId, url);
- this._initializedNestedPopups = true;
+ this._prepareNestedPopups(popupId, depth, frameId, url);
+ this._nestedPopupsPrepared = true;
}
this.setContentScale(scale);
- apiSendMessageToFrame(frameId, 'popupConfigured', {messageId});
+ api.sendMessageToFrame(frameId, 'popupConfigured', {messageId});
}
_isMessageAuthenticated(message) {
@@ -202,4 +203,57 @@ class DisplayFloat extends Display {
this._secret === message.secret
);
}
+
+ async _prepareNestedPopups(id, depth, parentFrameId, url) {
+ let complete = false;
+
+ const onOptionsUpdated = async () => {
+ const optionsContext = this.optionsContext;
+ const options = await api.optionsGet(optionsContext);
+ const maxPopupDepthExceeded = !(typeof depth === 'number' && depth < options.scanning.popupNestingMaxDepth);
+ if (maxPopupDepthExceeded || complete) { return; }
+
+ complete = true;
+ yomichan.off('optionsUpdated', onOptionsUpdated);
+
+ try {
+ await this._setupNestedPopups(id, depth, parentFrameId, url);
+ } catch (e) {
+ yomichan.logError(e);
+ }
+ };
+
+ yomichan.on('optionsUpdated', onOptionsUpdated);
+
+ await onOptionsUpdated();
+ }
+
+ async _setupNestedPopups(id, depth, parentFrameId, url) {
+ await dynamicLoader.loadScripts([
+ '/mixed/js/text-scanner.js',
+ '/fg/js/popup.js',
+ '/fg/js/popup-proxy.js',
+ '/fg/js/popup-factory.js',
+ '/fg/js/frame-offset-forwarder.js',
+ '/fg/js/frontend.js'
+ ]);
+
+ const {frameId} = await api.frameInformationGet();
+
+ const popupFactory = new PopupFactory(frameId);
+ popupFactory.prepare();
+
+ const frontend = new Frontend(
+ frameId,
+ popupFactory,
+ {
+ id,
+ depth,
+ parentFrameId,
+ url,
+ proxy: true
+ }
+ );
+ await frontend.prepare();
+ }
}
diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js
index 9b68d34e..f692364a 100644
--- a/ext/fg/js/frame-offset-forwarder.js
+++ b/ext/fg/js/frame-offset-forwarder.js
@@ -16,13 +16,12 @@
*/
/* global
- * apiBroadcastTab
+ * api
*/
class FrameOffsetForwarder {
constructor() {
- this._started = false;
-
+ this._isPrepared = false;
this._cacheMaxSize = 1000;
this._frameCache = new Set();
this._unreachableContentWindowCache = new Set();
@@ -38,10 +37,10 @@ class FrameOffsetForwarder {
]);
}
- start() {
- if (this._started) { return; }
- window.addEventListener('message', this.onMessage.bind(this), false);
- this._started = true;
+ prepare() {
+ if (this._isPrepared) { return; }
+ window.addEventListener('message', this._onMessage.bind(this), false);
+ this._isPrepared = true;
}
async getOffset() {
@@ -69,11 +68,20 @@ class FrameOffsetForwarder {
return offset;
}
- onMessage(e) {
- const {action, params} = e.data;
- const handler = this._windowMessageHandlers.get(action);
- if (typeof handler !== 'function') { return; }
- handler(params, e);
+ // Private
+
+ _onMessage(event) {
+ const data = event.data;
+ if (data === null || typeof data !== 'object') { return; }
+
+ try {
+ const {action, params} = event.data;
+ const handler = this._windowMessageHandlers.get(action);
+ if (typeof handler !== 'function') { return; }
+ handler(params, event);
+ } catch (e) {
+ // NOP
+ }
}
_onGetFrameOffset(offset, uniqueId, e) {
@@ -161,6 +169,6 @@ class FrameOffsetForwarder {
}
_forwardFrameOffsetOrigin(offset, uniqueId) {
- apiBroadcastTab('frameOffset', {offset, uniqueId});
+ api.broadcastTab('frameOffset', {offset, uniqueId});
}
}
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 <https://www.gnu.org/licenses/>.
- */
-
-
-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 <https://www.gnu.org/licenses/>.
- */
-
-
-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/frontend.js b/ext/fg/js/frontend.js
index 575dc413..f6b0d236 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -16,20 +16,18 @@
*/
/* global
+ * DOM
+ * FrameOffsetForwarder
+ * PopupProxy
* TextScanner
- * apiBroadcastTab
- * apiGetZoom
- * apiKanjiFind
- * apiOptionsGet
- * apiTermsFind
+ * api
* docSentenceExtract
*/
class Frontend {
- constructor(popup, getUrl=null) {
+ constructor(frameId, popupFactory, frontendInitializationData) {
this._id = yomichan.generateId(16);
- this._popup = popup;
- this._getUrl = getUrl;
+ this._popup = null;
this._disabledOverride = false;
this._options = null;
this._pageZoomFactor = 1.0;
@@ -41,11 +39,31 @@ class Frontend {
this._optionsUpdatePending = false;
this._textScanner = new TextScanner({
node: window,
- ignoreElements: () => this._popup.isProxy() ? [] : [this._popup.getFrame()],
- ignorePoint: (x, y) => this._popup.containsPoint(x, y),
+ ignoreElements: this._ignoreElements.bind(this),
+ ignorePoint: this._ignorePoint.bind(this),
search: this._search.bind(this)
});
+ const {
+ depth=0,
+ id: proxyPopupId,
+ parentFrameId,
+ proxy: useProxyPopup=false,
+ isSearchPage=false,
+ allowRootFramePopupProxy=true
+ } = frontendInitializationData;
+ this._proxyPopupId = proxyPopupId;
+ this._parentFrameId = parentFrameId;
+ this._useProxyPopup = useProxyPopup;
+ this._isSearchPage = isSearchPage;
+ this._depth = depth;
+ this._frameId = frameId;
+ this._frameOffsetForwarder = new FrameOffsetForwarder();
+ this._popupFactory = popupFactory;
+ this._allowRootFramePopupProxy = allowRootFramePopupProxy;
+ this._popupCache = new Map();
+ this._updatePopupToken = null;
+
this._windowMessageHandlers = new Map([
['popupClose', this._onMessagePopupClose.bind(this)],
['selectionCopy', this._onMessageSelectionCopy.bind()]
@@ -66,39 +84,46 @@ class Frontend {
this._textScanner.canClearSelection = value;
}
+ get popup() {
+ return this._popup;
+ }
+
async prepare() {
+ this._frameOffsetForwarder.prepare();
+
+ await this.updateOptions();
try {
- await this.updateOptions();
- const {zoomFactor} = await apiGetZoom();
+ const {zoomFactor} = await api.getZoom();
this._pageZoomFactor = zoomFactor;
+ } catch (e) {
+ // Ignore exceptions which may occur due to being on an unsupported page (e.g. about:blank)
+ }
- window.addEventListener('resize', this._onResize.bind(this), false);
+ this._textScanner.prepare();
- const visualViewport = window.visualViewport;
- if (visualViewport !== null && typeof visualViewport === 'object') {
- window.visualViewport.addEventListener('scroll', this._onVisualViewportScroll.bind(this));
- window.visualViewport.addEventListener('resize', this._onVisualViewportResize.bind(this));
- }
+ window.addEventListener('resize', this._onResize.bind(this), false);
+ DOM.addFullscreenChangeEventListener(this._updatePopup.bind(this));
- yomichan.on('orphaned', this._onOrphaned.bind(this));
- yomichan.on('optionsUpdated', this.updateOptions.bind(this));
- yomichan.on('zoomChanged', this._onZoomChanged.bind(this));
- chrome.runtime.onMessage.addListener(this._onRuntimeMessage.bind(this));
+ const visualViewport = window.visualViewport;
+ if (visualViewport !== null && typeof visualViewport === 'object') {
+ visualViewport.addEventListener('scroll', this._onVisualViewportScroll.bind(this));
+ visualViewport.addEventListener('resize', this._onVisualViewportResize.bind(this));
+ }
- this._textScanner.on('clearSelection', this._onClearSelection.bind(this));
- this._textScanner.on('activeModifiersChanged', this._onActiveModifiersChanged.bind(this));
+ yomichan.on('orphaned', this._onOrphaned.bind(this));
+ yomichan.on('optionsUpdated', this.updateOptions.bind(this));
+ yomichan.on('zoomChanged', this._onZoomChanged.bind(this));
+ chrome.runtime.onMessage.addListener(this._onRuntimeMessage.bind(this));
- this._updateContentScale();
- this._broadcastRootPopupInformation();
- } catch (e) {
- yomichan.logError(e);
- }
- }
+ this._textScanner.on('clearSelection', this._onClearSelection.bind(this));
+ this._textScanner.on('activeModifiersChanged', this._onActiveModifiersChanged.bind(this));
- async setPopup(popup) {
- this._textScanner.clearSelection(true);
- this._popup = popup;
- await popup.setOptionsContext(await this.getOptionsContext(), this._id);
+ api.crossFrame.registerHandlers([
+ ['getUrl', {async: false, handler: this._onApiGetUrl.bind(this)}]
+ ]);
+
+ this._updateContentScale();
+ this._broadcastRootPopupInformation();
}
setDisabledOverride(disabled) {
@@ -112,15 +137,26 @@ class Frontend {
}
async getOptionsContext() {
- const url = this._getUrl !== null ? await this._getUrl() : window.location.href;
- const depth = this._popup.depth;
+ let url = window.location.href;
+ if (this._useProxyPopup) {
+ try {
+ url = await api.crossFrame.invoke(this._parentFrameId, 'getUrl', {});
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ const depth = this._depth;
const modifierKeys = [...this._activeModifiers];
return {depth, url, modifierKeys};
}
async updateOptions() {
const optionsContext = await this.getOptionsContext();
- this._options = await apiOptionsGet(optionsContext);
+ this._options = await api.optionsGet(optionsContext);
+
+ await this._updatePopup();
+
this._textScanner.setOptions(this._options);
this._updateTextScannerEnabled();
@@ -130,8 +166,6 @@ class Frontend {
}
this._textScanner.ignoreNodes = ignoreNodes.join(',');
- await this._popup.setOptionsContext(optionsContext, this._id);
-
this._updateContentScale();
const textSourceCurrent = this._textScanner.getCurrentTextSource();
@@ -167,6 +201,12 @@ class Frontend {
this._broadcastDocumentInformation(uniqueId);
}
+ // API message handlers
+
+ _onApiGetUrl() {
+ return window.location.href;
+ }
+
// Private
_onResize() {
@@ -223,6 +263,95 @@ class Frontend {
await this.updateOptions();
}
+ async _updatePopup() {
+ const showIframePopupsInRootFrame = this._options.general.showIframePopupsInRootFrame;
+ const isIframe = !this._useProxyPopup && (window !== window.parent);
+
+ let popupPromise;
+ if (
+ isIframe &&
+ showIframePopupsInRootFrame &&
+ DOM.getFullscreenElement() === null &&
+ this._allowRootFramePopupProxy
+ ) {
+ popupPromise = this._popupCache.get('iframe');
+ if (typeof popupPromise === 'undefined') {
+ popupPromise = this._getIframeProxyPopup();
+ this._popupCache.set('iframe', popupPromise);
+ }
+ } else if (this._useProxyPopup) {
+ popupPromise = this._popupCache.get('proxy');
+ if (typeof popupPromise === 'undefined') {
+ popupPromise = this._getProxyPopup();
+ this._popupCache.set('proxy', popupPromise);
+ }
+ } else {
+ popupPromise = this._popupCache.get('default');
+ if (typeof popupPromise === 'undefined') {
+ popupPromise = this._getDefaultPopup();
+ this._popupCache.set('default', popupPromise);
+ }
+ }
+
+ // The token below is used as a unique identifier to ensure that a new _updatePopup call
+ // hasn't been started during the await.
+ const token = {};
+ this._updatePopupToken = token;
+ const popup = await popupPromise;
+ const optionsContext = await this.getOptionsContext();
+ if (this._updatePopupToken !== token) { return; }
+ await popup.setOptionsContext(optionsContext, this._id);
+ if (this._updatePopupToken !== token) { return; }
+
+ if (this._isSearchPage) {
+ this.setDisabledOverride(!this._options.scanning.enableOnSearchPage);
+ }
+
+ this._textScanner.clearSelection(true);
+ this._popup = popup;
+ this._depth = popup.depth;
+ }
+
+ async _getDefaultPopup() {
+ return this._popupFactory.getOrCreatePopup(null, null, this._depth);
+ }
+
+ async _getProxyPopup() {
+ const popup = new PopupProxy(null, this._depth + 1, this._proxyPopupId, this._parentFrameId);
+ await popup.prepare();
+ return popup;
+ }
+
+ async _getIframeProxyPopup() {
+ const rootPopupInformationPromise = yomichan.getTemporaryListenerResult(
+ chrome.runtime.onMessage,
+ ({action, params}, {resolve}) => {
+ if (action === 'rootPopupInformation') {
+ resolve(params);
+ }
+ }
+ );
+ api.broadcastTab('rootPopupRequestInformationBroadcast');
+ const {popupId, frameId: parentFrameId} = await rootPopupInformationPromise;
+
+ const popup = new PopupProxy(popupId, 0, null, parentFrameId, this._frameOffsetForwarder);
+ popup.on('offsetNotFound', () => {
+ this._allowRootFramePopupProxy = false;
+ this._updatePopup();
+ });
+ await popup.prepare();
+
+ return popup;
+ }
+
+ _ignoreElements() {
+ return this._popup === null || this._popup.isProxy() ? [] : [this._popup.getContainer()];
+ }
+
+ _ignorePoint(x, y) {
+ return this._popup !== null && this._popup.containsPoint(x, y);
+ }
+
async _search(textSource, cause) {
await this._updatePendingOptions();
@@ -258,32 +387,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 apiTermsFind(searchText, {}, optionsContext);
+ 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 apiKanjiFind(searchText, optionsContext);
+ 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,
@@ -314,7 +447,7 @@ class Frontend {
_updateTextScannerEnabled() {
const enabled = (
this._options.general.enable &&
- this._popup.depth <= this._options.scanning.popupNestingMaxDepth &&
+ this._depth <= this._options.scanning.popupNestingMaxDepth &&
!this._disabledOverride
);
this._enabledEventListeners.removeAllEventListeners();
@@ -338,27 +471,41 @@ class Frontend {
if (contentScale === this._contentScale) { return; }
this._contentScale = contentScale;
- this._popup.setContentScale(this._contentScale);
+ if (this._popup !== null) {
+ this._popup.setContentScale(this._contentScale);
+ }
this._updatePopupPosition();
}
async _updatePopupPosition() {
const textSource = this._textScanner.getCurrentTextSource();
- if (textSource !== null && await this._popup.isVisible()) {
+ if (
+ textSource !== null &&
+ this._popup !== null &&
+ await this._popup.isVisible()
+ ) {
this._showPopupContent(textSource, await this.getOptionsContext());
}
}
_broadcastRootPopupInformation() {
- if (!this._popup.isProxy() && this._popup.depth === 0 && this._popup.frameId === 0) {
- apiBroadcastTab('rootPopupInformation', {popupId: this._popup.id, frameId: this._popup.frameId});
+ if (
+ this._popup !== null &&
+ !this._popup.isProxy() &&
+ this._depth === 0 &&
+ this._frameId === 0
+ ) {
+ api.broadcastTab('rootPopupInformation', {
+ popupId: this._popup.id,
+ frameId: this._frameId
+ });
}
}
_broadcastDocumentInformation(uniqueId) {
- apiBroadcastTab('documentInformationBroadcast', {
+ api.broadcastTab('documentInformationBroadcast', {
uniqueId,
- frameId: this._popup.frameId,
+ frameId: this._frameId,
title: document.title
});
}
diff --git a/ext/fg/js/popup-factory.js b/ext/fg/js/popup-factory.js
index b10acbaf..904f18b9 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 {
@@ -28,8 +28,8 @@ class PopupFactory {
// Public functions
- async prepare() {
- const apiReceiver = new FrontendApiReceiver(`popup-factory#${this._frameId}`, new Map([
+ prepare() {
+ 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)}],
@@ -39,10 +39,8 @@ class PopupFactory {
['showContent', {async: true, handler: this._onApiShowContent.bind(this)}],
['setCustomCss', {async: false, handler: this._onApiSetCustomCss.bind(this)}],
['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();
+ ['setContentScale', {async: false, handler: this._onApiSetContentScale.bind(this)}]
+ ]);
}
getOrCreatePopup(id=null, parentId=null, depth=null) {
@@ -148,10 +146,6 @@ class PopupFactory {
return popup.setContentScale(scale);
}
- _onApiGetUrl() {
- return window.location.href;
- }
-
// Private functions
_getPopup(id) {
diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js
index 82da839a..a6602eae 100644
--- a/ext/fg/js/popup-proxy.js
+++ b/ext/fg/js/popup-proxy.js
@@ -16,17 +16,17 @@
*/
/* global
- * FrontendApiSender
+ * api
*/
-class PopupProxy {
- constructor(id, depth, parentPopupId, parentFrameId, getFrameOffset=null, setDisabled=null) {
+class PopupProxy extends EventDispatcher {
+ constructor(id, depth, parentPopupId, parentFrameId, frameOffsetForwarder=null) {
+ super();
this._id = id;
this._depth = depth;
this._parentPopupId = parentPopupId;
- this._apiSender = new FrontendApiSender(`popup-factory#${parentFrameId}`);
- this._getFrameOffset = getFrameOffset;
- this._setDisabled = setDisabled;
+ this._parentFrameId = parentFrameId;
+ this._frameOffsetForwarder = frameOffsetForwarder;
this._frameOffset = null;
this._frameOffsetPromise = null;
@@ -75,7 +75,7 @@ class PopupProxy {
}
async containsPoint(x, y) {
- if (this._getFrameOffset !== null) {
+ if (this._frameOffsetForwarder !== null) {
await this._updateFrameOffset();
[x, y] = this._applyFrameOffset(x, y);
}
@@ -84,7 +84,7 @@ class PopupProxy {
async showContent(elementRect, writingMode, type, details, context) {
let {x, y, width, height} = elementRect;
- if (this._getFrameOffset !== null) {
+ if (this._frameOffsetForwarder !== null) {
await this._updateFrameOffset();
[x, y] = this._applyFrameOffset(x, y);
}
@@ -104,14 +104,10 @@ class PopupProxy {
this._invoke('setContentScale', {id: this._id, scale});
}
- async getUrl() {
- return await this._invoke('getUrl', {});
- }
-
// Private
_invoke(action, params={}) {
- return this._apiSender.invoke(action, params);
+ return api.crossFrame.invoke(this._parentFrameId, action, params);
}
async _updateFrameOffset() {
@@ -134,12 +130,12 @@ class PopupProxy {
}
async _updateFrameOffsetInner(now) {
- this._frameOffsetPromise = this._getFrameOffset();
+ this._frameOffsetPromise = this._frameOffsetForwarder.getOffset();
try {
const offset = await this._frameOffsetPromise;
this._frameOffset = offset !== null ? offset : [0, 0];
- if (offset === null && this._setDisabled !== null) {
- this._setDisabled();
+ if (offset === null) {
+ this.trigger('offsetNotFound');
return;
}
this._frameOffsetUpdatedAt = now;
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index b7d4b57e..5ee62c9b 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -17,7 +17,7 @@
/* global
* DOM
- * apiOptionsGet
+ * api
* dynamicLoader
*/
@@ -47,6 +47,9 @@ class Popup {
this._frame.style.width = '0';
this._frame.style.height = '0';
+ this._container = this._frame;
+ this._shadow = null;
+
this._fullscreenEventListeners = new EventListenerCollection();
}
@@ -89,7 +92,7 @@ class Popup {
this._optionsContext = optionsContext;
this._previousOptionsContextSource = source;
- this._options = await apiOptionsGet(optionsContext);
+ this._options = await api.optionsGet(optionsContext);
this.updateTheme();
this._invokeApi('setOptionsContext', {optionsContext});
@@ -180,7 +183,12 @@ class Popup {
}
async setCustomOuterCss(css, useWebExtensionApi) {
- return await dynamicLoader.loadStyle('yomichan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi);
+ let parentNode = null;
+ if (this._shadow !== null) {
+ useWebExtensionApi = false;
+ parentNode = this._shadow;
+ }
+ return await dynamicLoader.loadStyle('yomichan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi, parentNode);
}
setChildrenSupported(value) {
@@ -195,6 +203,10 @@ class Popup {
return this._frame.getBoundingClientRect();
}
+ getContainer() {
+ return this._container;
+ }
+
// Private functions
_inject() {
@@ -326,14 +338,25 @@ class Popup {
}
async _createInjectPromise() {
- this._injectStyles();
+ if (this._options === null) {
+ throw new Error('Options not initialized');
+ }
+
+ const {useSecurePopupFrameUrl, usePopupShadowDom} = this._options.general;
+
+ await this._setUpContainer(usePopupShadowDom);
const {secret, token} = await this._initializeFrame(this._frame, this._targetOrigin, this._frameId, (frame) => {
frame.removeAttribute('src');
frame.removeAttribute('srcdoc');
this._observeFullscreen(true);
this._onFullscreenChanged();
- frame.contentDocument.location.href = chrome.runtime.getURL('/fg/float.html');
+ const url = chrome.runtime.getURL('/fg/float.html');
+ if (useSecurePopupFrameUrl) {
+ frame.contentDocument.location.href = url;
+ } else {
+ frame.setAttribute('src', url);
+ }
});
this._frameSecret = secret;
this._frameToken = token;
@@ -371,9 +394,9 @@ class Popup {
}
_resetFrame() {
- const parent = this._frame.parentNode;
+ const parent = this._container.parentNode;
if (parent !== null) {
- parent.removeChild(this._frame);
+ parent.removeChild(this._container);
}
this._frame.removeAttribute('src');
this._frame.removeAttribute('srcdoc');
@@ -384,9 +407,31 @@ class Popup {
this._injectPromiseComplete = false;
}
+ async _setUpContainer(usePopupShadowDom) {
+ if (usePopupShadowDom && typeof this._frame.attachShadow === 'function') {
+ const container = document.createElement('div');
+ container.style.setProperty('all', 'initial', 'important');
+ const shadow = container.attachShadow({mode: 'closed', delegatesFocus: true});
+ shadow.appendChild(this._frame);
+
+ this._container = container;
+ this._shadow = shadow;
+ } else {
+ const frameParentNode = this._frame.parentNode;
+ if (frameParentNode !== null) {
+ frameParentNode.removeChild(this._frame);
+ }
+
+ this._container = this._frame;
+ this._shadow = null;
+ }
+
+ await this._injectStyles();
+ }
+
async _injectStyles() {
try {
- await dynamicLoader.loadStyle('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true);
+ await this._injectPopupOuterStylesheet();
} catch (e) {
// NOP
}
@@ -398,6 +443,18 @@ class Popup {
}
}
+ async _injectPopupOuterStylesheet() {
+ let fileType = 'file';
+ let useWebExtensionApi = true;
+ let parentNode = null;
+ if (this._shadow !== null) {
+ fileType = 'file-content';
+ useWebExtensionApi = false;
+ parentNode = this._shadow;
+ }
+ await dynamicLoader.loadStyle('yomichan-popup-outer-stylesheet', fileType, '/fg/css/client.css', useWebExtensionApi, parentNode);
+ }
+
_observeFullscreen(observe) {
if (!observe) {
this._fullscreenEventListeners.removeAllEventListeners();
@@ -409,22 +466,13 @@ class Popup {
return;
}
- const fullscreenEvents = [
- 'fullscreenchange',
- 'MSFullscreenChange',
- 'mozfullscreenchange',
- 'webkitfullscreenchange'
- ];
- const onFullscreenChanged = this._onFullscreenChanged.bind(this);
- for (const eventName of fullscreenEvents) {
- this._fullscreenEventListeners.addEventListener(document, eventName, onFullscreenChanged, false);
- }
+ DOM.addFullscreenChangeEventListener(this._onFullscreenChanged.bind(this), this._fullscreenEventListeners);
}
_onFullscreenChanged() {
const parent = this._getFrameParentElement();
- if (parent !== null && this._frame.parentNode !== parent) {
- parent.appendChild(this._frame);
+ if (parent !== null && this._container.parentNode !== parent) {
+ parent.appendChild(this._container);
}
}
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 <https://www.gnu.org/licenses/>.
*/
-// \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;
- }
}