diff options
Diffstat (limited to 'ext/fg/js')
| -rw-r--r-- | ext/fg/js/content-script-main.js | 148 | ||||
| -rw-r--r-- | ext/fg/js/document.js | 11 | ||||
| -rw-r--r-- | ext/fg/js/float-main.js | 47 | ||||
| -rw-r--r-- | ext/fg/js/float.js | 76 | ||||
| -rw-r--r-- | ext/fg/js/frame-offset-forwarder.js | 34 | ||||
| -rw-r--r-- | ext/fg/js/frontend-api-receiver.js | 76 | ||||
| -rw-r--r-- | ext/fg/js/frontend-api-sender.js | 128 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 253 | ||||
| -rw-r--r-- | ext/fg/js/popup-factory.js | 16 | ||||
| -rw-r--r-- | ext/fg/js/popup-proxy.js | 28 | ||||
| -rw-r--r-- | ext/fg/js/popup.js | 88 | ||||
| -rw-r--r-- | ext/fg/js/source.js | 224 | 
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; -    }  } |