diff options
| author | Alex Yatskov <alex@foosoft.net> | 2016-09-18 19:50:32 -0700 | 
|---|---|---|
| committer | Alex Yatskov <alex@foosoft.net> | 2016-09-18 19:50:32 -0700 | 
| commit | b44d19b35e120133a4bb751e176aec4d14b6cbc5 (patch) | |
| tree | 05b23ef7e70e57f2cf38584a20c864ff0d7e4853 /ext/fg/js | |
| parent | 7bd642e53868e0a3154b285be969cc2e6068f46f (diff) | |
| parent | 4e2d7327c5b1aa8eed035dc4c177a3b9b49c5ab7 (diff) | |
Merge branch 'dev'
Diffstat (limited to 'ext/fg/js')
| -rw-r--r-- | ext/fg/js/api.js | 42 | ||||
| -rw-r--r-- | ext/fg/js/client.js | 289 | ||||
| -rw-r--r-- | ext/fg/js/driver.js | 244 | ||||
| -rw-r--r-- | ext/fg/js/frame.js | 43 | ||||
| -rw-r--r-- | ext/fg/js/popup.js | 6 | ||||
| -rw-r--r-- | ext/fg/js/util.js | 114 | 
6 files changed, 383 insertions, 355 deletions
| diff --git a/ext/fg/js/api.js b/ext/fg/js/api.js deleted file mode 100644 index 643d0360..00000000 --- a/ext/fg/js/api.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2016  Alex Yatskov <alex@foosoft.net> - * Author: Alex Yatskov <alex@foosoft.net> - * - * 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 <http://www.gnu.org/licenses/>. - */ - - -function bgSendMessage(action, params) { -    return new Promise((resolve, reject) => chrome.runtime.sendMessage({action, params}, resolve)); -} - -function bgFindTerm(text) { -    return bgSendMessage('findTerm', {text}); -} - -function bgFindKanji(text) { -    return bgSendMessage('findKanji', {text}); -} - -function bgRenderText(data, template) { -    return bgSendMessage('renderText', {data, template}); -} - -function bgCanAddDefinitions(definitions, modes) { -    return bgSendMessage('canAddDefinitions', {definitions, modes}); -} - -function bgAddDefinition(definition, mode) { -    return bgSendMessage('addDefinition', {definition, mode}); -} diff --git a/ext/fg/js/client.js b/ext/fg/js/client.js deleted file mode 100644 index 628d5b30..00000000 --- a/ext/fg/js/client.js +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright (C) 2016  Alex Yatskov <alex@foosoft.net> - * Author: Alex Yatskov <alex@foosoft.net> - * - * 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 <http://www.gnu.org/licenses/>. - */ - - -class Client { -    constructor() { -        this.popup = new Popup(); -        this.audio = {}; -        this.lastMousePos = null; -        this.lastTextSource = null; -        this.activateKey = 16; -        this.activateBtn = 2; -        this.enabled = false; -        this.options = {}; -        this.definitions = null; -        this.sequence = 0; -        this.fgRoot = chrome.extension.getURL('fg'); - -        chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this)); -        window.addEventListener('message', this.onFrameMessage.bind(this)); -        window.addEventListener('mousedown', this.onMouseDown.bind(this)); -        window.addEventListener('mousemove', this.onMouseMove.bind(this)); -        window.addEventListener('keydown', this.onKeyDown.bind(this)); -        window.addEventListener('scroll', (e) => this.hidePopup()); -        window.addEventListener('resize', (e) => this.hidePopup()); -    } - -    onKeyDown(e) { -        if (this.enabled && this.lastMousePos !== null && (e.keyCode === this.activateKey || e.charCode === this.activateKey)) { -            this.searchAt(this.lastMousePos); -        } -    } - -    onMouseMove(e) { -        this.lastMousePos = {x: e.clientX, y: e.clientY}; -        if (this.enabled && (e.shiftKey || e.which === this.activateBtn)) { -            this.searchAt(this.lastMousePos); -        } -    } - -    onMouseDown(e) { -        this.lastMousePos = {x: e.clientX, y: e.clientY}; -        if (this.enabled && (e.shiftKey || e.which === this.activateBtn)) { -            this.searchAt(this.lastMousePos); -        } else { -            this.hidePopup(); -        } -    } - -    onBgMessage({action, params}, sender, callback) { -        const method = this['api_' + action]; -        if (typeof(method) === 'function') { -            method.call(this, params); -        } - -        callback(); -    } - -    onFrameMessage(e) { -        const {action, params} = e.data, method = this['api_' + action]; -        if (typeof(method) === 'function') { -            method.call(this, params); -        } -    } - -    searchAt(point) { -        const textSource = Client.textSourceFromPoint(point); -        if (textSource === null || !textSource.containsPoint(point)) { -            this.hidePopup(); -            return; -        } - -        if (this.lastTextSource !== null && this.lastTextSource.equals(textSource)) { -            return; -        } - -        textSource.setEndOffset(this.options.scanLength); - -        let defs = []; -        let seq = -1; - -        bgFindTerm(textSource.text()) -            .then(({definitions, length}) => { -                if (length === 0) { -                    return Promise.reject(); -                } - -                textSource.setEndOffset(length); - -                const sentence = Client.extractSentence(textSource, this.options.sentenceExtent); -                definitions.forEach((definition) => { -                    definition.url = window.location.href; -                    definition.sentence = sentence; -                }); - -                defs = definitions; -                seq = ++this.sequence; - -                return bgRenderText({definitions, root: this.fgRoot, options: this.options, sequence: seq}, 'term-list.html'); -            }) -            .then((content) => { -                this.definitions = defs; -                this.showPopup(textSource, content); - -                return bgCanAddDefinitions(defs, ['term_kanji', 'term_kana']); -            }) -            .then((states) => { -                if (states !== null) { -                    states.forEach((state, index) => this.popup.sendMessage('setActionState', {index, state, sequence: seq})); -                } -            }, () => this.hidePopup()); -    } - -    showPopup(textSource, content) { -        this.popup.showNextTo(textSource.getRect(), content); - -        if (this.options.selectMatchedText) { -            textSource.select(); -        } - -        this.lastTextSource = textSource; -    } - -    hidePopup() { -        this.popup.hide(); - -        if (this.options.selectMatchedText && this.lastTextSource !== null) { -            this.lastTextSource.deselect(); -        } - -        this.lastTextSource = null; -        this.definitions = null; -    } - -    api_setOptions(opts) { -        this.options = opts; -    } - -    api_setEnabled(enabled) { -        if (!(this.enabled = enabled)) { -            this.hidePopup(); -        } -    } - -    api_addNote({index, mode}) { -        const state = {[mode]: false}; -        bgAddDefinition(this.definitions[index], mode).then((success) => { -            if (success) { -                this.popup.sendMessage('setActionState', {index, state, sequence: this.sequence}); -            } else { -                alert('Note could not be added'); -            } -        }); -    } - -    api_playAudio(index) { -        const definition = this.definitions[index]; - -        let url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=${encodeURIComponent(definition.expression)}`; -        if (definition.reading) { -            url += `&kana=${encodeURIComponent(definition.reading)}`; -        } - -        for (const key in this.audio) { -            this.audio[key].pause(); -        } - -        const audio = this.audio[url] || new Audio(url); -        audio.currentTime = 0; -        audio.play(); - -        this.audio[url] = audio; -    } - -    api_displayKanji(kanji) { -        let defs = []; -        let seq = -1; - -        bgFindKanji(kanji) -            .then((definitions) => { -                definitions.forEach((definition) => definition.url = window.location.href); - -                defs = definitions; -                seq = ++this.sequence; - -                return bgRenderText({definitions, root: this.fgRoot, options: this.options, sequence: seq}, 'kanji-list.html'); -            }) -            .then((content) => { -                this.definitions = defs; -                this.popup.setContent(content, defs); - -                return bgCanAddDefinitions(defs, ['kanji']); -            }) -            .then((states) => { -                if (states !== null) { -                    states.forEach((state, index) => this.popup.sendMessage('setActionState', {index, state, sequence: seq})); -                } -            }); -    } - -    static textSourceFromPoint(point) { -        const element = document.elementFromPoint(point.x, point.y); -        if (element !== null) { -            const names = ['IMG', 'INPUT', 'BUTTON', 'TEXTAREA']; -            if (names.indexOf(element.nodeName) !== -1) { -                return new TextSourceElement(element); -            } -        } - -        const range = document.caretRangeFromPoint(point.x, point.y); -        if (range !== null) { -            return new TextSourceRange(range); -        } - -        return null; -    } - -    static extractSentence(source, extent) { -        const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'}; -        const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'}; -        const terminators = '…。..??!!'; - -        const sourceLocal = source.clone(); -        const position = sourceLocal.setStartOffset(extent); -        sourceLocal.setEndOffset(position + extent); -        const content = sourceLocal.text(); - -        let quoteStack = []; - -        let startPos = 0; -        for (let i = position; i >= startPos; --i) { -            const c = content[i]; - -            if (quoteStack.length === 0 && (terminators.indexOf(c) !== -1 || c in quotesFwd)) { -                startPos = i + 1; -                break; -            } - -            if (quoteStack.length > 0 && c === quoteStack[0]) { -                quoteStack.pop(); -            } else if (c in quotesBwd) { -                quoteStack = [quotesBwd[c]].concat(quoteStack); -            } -        } - -        quoteStack = []; - -        let endPos = content.length; -        for (let i = position; i < endPos; ++i) { -            const c = content[i]; - -            if (quoteStack.length === 0) { -                if (terminators.indexOf(c) !== -1) { -                    endPos = i + 1; -                    break; -                } -                else if (c in quotesBwd) { -                    endPos = i; -                    break; -                } -            } - -            if (quoteStack.length > 0 && c === quoteStack[0]) { -                quoteStack.pop(); -            } else if (c in quotesFwd) { -                quoteStack = [quotesFwd[c]].concat(quoteStack); -            } -        } - -        return content.substring(startPos, endPos).trim(); -    } -} - -window.yomiClient = new Client(); diff --git a/ext/fg/js/driver.js b/ext/fg/js/driver.js new file mode 100644 index 00000000..68cbcdee --- /dev/null +++ b/ext/fg/js/driver.js @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2016  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * 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 <http://www.gnu.org/licenses/>. + */ + + +class Driver { +    constructor() { +        this.popup = new Popup(); +        this.audio = {}; +        this.lastMousePos = null; +        this.lastTextSource = null; +        this.pendingLookup = false; +        this.enabled = false; +        this.options = {}; +        this.definitions = null; +        this.sequence = 0; +        this.fgRoot = chrome.extension.getURL('fg'); + +        chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this)); +        window.addEventListener('message', this.onFrameMessage.bind(this)); +        window.addEventListener('mousedown', this.onMouseDown.bind(this)); +        window.addEventListener('mousemove', this.onMouseMove.bind(this)); +        window.addEventListener('keydown', this.onKeyDown.bind(this)); +        window.addEventListener('scroll', e => this.hidePopup()); +        window.addEventListener('resize', e => this.hidePopup()); +    } + +    onKeyDown(e) { +        if (this.enabled && this.lastMousePos !== null && (e.keyCode === 16 || e.charCode === 16)) { +            this.searchAt(this.lastMousePos, e.ctrlKey ? 'kanji' : 'terms'); +        } else { +            this.hidePopup(); +        } +    } + +    onMouseMove(e) { +        this.lastMousePos = {x: e.clientX, y: e.clientY}; +        if (this.enabled && (e.shiftKey || e.which === 2)) { +            this.searchAt(this.lastMousePos, e.ctrlKey ? 'kanji' : 'terms'); +        } +    } + +    onMouseDown(e) { +        this.lastMousePos = {x: e.clientX, y: e.clientY}; +        if (this.enabled && (e.shiftKey || e.which === 2)) { +            this.searchAt(this.lastMousePos, e.ctrlKey ? 'kanji' : 'terms'); +        } else { +            this.hidePopup(); +        } +    } + +    onBgMessage({action, params}, sender, callback) { +        const method = this['api_' + action]; +        if (typeof(method) === 'function') { +            method.call(this, params); +        } + +        callback(); +    } + +    onFrameMessage(e) { +        const {action, params} = e.data, method = this['api_' + action]; +        if (typeof(method) === 'function') { +            method.call(this, params); +        } +    } + +    searchTerms(textSource) { +        textSource.setEndOffset(this.options.scanLength); + +        this.pendingLookup = true; +        findTerm(textSource.text()).then(({definitions, length}) => { +            if (definitions.length === 0) { +                this.pendingLookup = false; +                this.hidePopup(); +            } else { +                textSource.setEndOffset(length); + +                const sentence = extractSentence(textSource, this.options.sentenceExtent); +                definitions.forEach(definition => { +                    definition.url = window.location.href; +                    definition.sentence = sentence; +                }); + +                const sequence = ++this.sequence; +                return renderText({definitions, sequence, root: this.fgRoot, options: this.options}, 'term-list.html').then(content => { +                    this.definitions = definitions; +                    this.pendingLookup = false; +                    this.showPopup(textSource, content); +                    return canAddDefinitions(definitions, ['term_kanji', 'term_kana']); +                }).then(states => { +                    if (states !== null) { +                        states.forEach((state, index) => this.popup.invokeApi('setActionState', {index, state, sequence})); +                    } +                }); +            } +        }); +    } + +    searchKanji(textSource) { +        textSource.setEndOffset(1); + +        this.pendingLookup = true; +        findKanji(textSource.text()).then(definitions => { +            if (definitions.length === 0) { +                this.pendingLookup = false; +                this.hidePopup(); +            } else { +                definitions.forEach(definition => definition.url = window.location.href); + +                const sequence = ++this.sequence; +                return renderText({definitions, sequence, root: this.fgRoot, options: this.options}, 'kanji-list.html').then(content => { +                    this.definitions = definitions; +                    this.pendingLookup = false; +                    this.showPopup(textSource, content); +                    return canAddDefinitions(definitions, ['kanji']); +                }).then(states => { +                    if (states !== null) { +                        states.forEach((state, index) => this.popup.invokeApi('setActionState', {index, state, sequence})); +                    } +                }); +            } +        }); +    } + +    searchAt(point, mode) { +        if (this.pendingLookup) { +            return; +        } + +        const textSource = textSourceFromPoint(point); +        if (textSource === null || !textSource.containsPoint(point)) { +            this.hidePopup(); +            return; +        } + +        if (this.lastTextSource !== null && this.lastTextSource.equals(textSource)) { +            return; +        } + +        switch (mode) { +            case 'terms': +                this.searchTerms(textSource); +                break; +            case 'kanji': +                this.searchKanji(textSource); +                break; +        } +    } + +    showPopup(textSource, content) { +        this.popup.showNextTo(textSource.getRect(), content); + +        if (this.options.selectMatchedText) { +            textSource.select(); +        } + +        this.lastTextSource = textSource; +    } + +    hidePopup() { +        this.popup.hide(); + +        if (this.options.selectMatchedText && this.lastTextSource !== null) { +            this.lastTextSource.deselect(); +        } + +        this.lastTextSource = null; +        this.definitions = null; +    } + +    api_setOptions(opts) { +        this.options = opts; +    } + +    api_setEnabled(enabled) { +        if (!(this.enabled = enabled)) { +            this.hidePopup(); +        } +    } + +    api_addNote({index, mode}) { +        const state = {[mode]: false}; +        addDefinition(this.definitions[index], mode).then(success => { +            if (success) { +                this.popup.invokeApi('setActionState', {index, state, sequence: this.sequence}); +            } else { +                alert('Note could not be added'); +            } +        }); +    } + +    api_playAudio(index) { +        const definition = this.definitions[index]; + +        let url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=${encodeURIComponent(definition.expression)}`; +        if (definition.reading) { +            url += `&kana=${encodeURIComponent(definition.reading)}`; +        } + +        for (const key in this.audio) { +            this.audio[key].pause(); +        } + +        const audio = this.audio[url] || new Audio(url); +        audio.currentTime = 0; +        audio.play(); + +        this.audio[url] = audio; +    } + +    api_displayKanji(kanji) { +        findKanji(kanji).then(definitions => { +            definitions.forEach(definition => definition.url = window.location.href); + +            const sequence = ++this.sequence; +            return renderText({definitions, sequence, root: this.fgRoot, options: this.options}, 'kanji-list.html').then(content => { +                this.definitions = definitions; +                this.popup.setContent(content, definitions); +                return canAddDefinitions(definitions, ['kanji']); +            }).then(states => { +                if (states !== null) { +                    states.forEach((state, index) => this.popup.invokeApi('setActionState', {index, state, sequence})); +                } +            }); +        }); +    } +} + +window.driver = new Driver(); diff --git a/ext/fg/js/frame.js b/ext/fg/js/frame.js index 8f3d3877..8a99a405 100644 --- a/ext/fg/js/frame.js +++ b/ext/fg/js/frame.js @@ -17,48 +17,39 @@   */ +function invokeApi(action, params, target) { +    target.postMessage({action, params}, '*'); +} +  function registerKanjiLinks() {      for (const link of Array.from(document.getElementsByClassName('kanji-link'))) { -        link.addEventListener('click', (e) => { +        link.addEventListener('click', e => {              e.preventDefault(); -            window.parent.postMessage({action: 'displayKanji', params: e.target.innerHTML}, '*'); +            invokeApi('displayKanji', e.target.innerHTML, window.parent);          });      }  }  function registerAddNoteLinks() {      for (const link of Array.from(document.getElementsByClassName('action-add-note'))) { -        link.addEventListener('click', (e) => { +        link.addEventListener('click', e => {              e.preventDefault();              const ds = e.currentTarget.dataset; -            window.parent.postMessage({action: 'addNote', params: {index: ds.index, mode: ds.mode}}, '*'); +            invokeApi('addNote', {index: ds.index, mode: ds.mode}, window.parent);          });      }  }  function registerAudioLinks() {      for (const link of Array.from(document.getElementsByClassName('action-play-audio'))) { -        link.addEventListener('click', (e) => { +        link.addEventListener('click', e => {              e.preventDefault();              const ds = e.currentTarget.dataset; -            window.parent.postMessage({action: 'playAudio', params: ds.index}, '*'); +            invokeApi('playAudio', ds.index, window.parent);          });      }  } -function onDomContentLoaded() { -    registerKanjiLinks(); -    registerAddNoteLinks(); -    registerAudioLinks(); -} - -function onMessage(e) { -    const {action, params} = e.data, method = window['api_' + action]; -    if (typeof(method) === 'function') { -        method(params); -    } -} -  function api_setActionState({index, state, sequence}) {      for (const mode in state) {          const matches = document.querySelectorAll(`.action-bar[data-sequence="${sequence}"] .action-add-note[data-index="${index}"][data-mode="${mode}"]`); @@ -75,5 +66,15 @@ function api_setActionState({index, state, sequence}) {      }  } -document.addEventListener('DOMContentLoaded', onDomContentLoaded, false); -window.addEventListener('message', onMessage); +document.addEventListener('DOMContentLoaded', () => { +    registerKanjiLinks(); +    registerAddNoteLinks(); +    registerAudioLinks(); +}); + +window.addEventListener('message', e => { +    const {action, params} = e.data, method = window['api_' + action]; +    if (typeof(method) === 'function') { +        method(params); +    } +}); diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index a0eb725c..88b8e4e3 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -68,7 +68,7 @@ class Popup {          doc.close();      } -    sendMessage(action, params, callback) { +    invokeApi(action, params) {          if (this.popup !== null) {              this.popup.contentWindow.postMessage({action, params}, '*');          } @@ -81,8 +81,8 @@ class Popup {          this.popup = document.createElement('iframe');          this.popup.id = 'yomichan-popup'; -        this.popup.addEventListener('mousedown', (e) => e.stopPropagation()); -        this.popup.addEventListener('scroll', (e) => e.stopPropagation()); +        this.popup.addEventListener('mousedown', e => e.stopPropagation()); +        this.popup.addEventListener('scroll', e => e.stopPropagation());          document.body.appendChild(this.popup);      } diff --git a/ext/fg/js/util.js b/ext/fg/js/util.js new file mode 100644 index 00000000..c24ad885 --- /dev/null +++ b/ext/fg/js/util.js @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * 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 <http://www.gnu.org/licenses/>. + */ + + +function invokeApiBg(action, params) { +    return new Promise((resolve, reject) => chrome.runtime.sendMessage({action, params}, resolve)); +} + +function findTerm(text) { +    return invokeApiBg('findTerm', {text}); +} + +function findKanji(text) { +    return invokeApiBg('findKanji', {text}); +} + +function renderText(data, template) { +    return invokeApiBg('renderText', {data, template}); +} + +function canAddDefinitions(definitions, modes) { +    return invokeApiBg('canAddDefinitions', {definitions, modes}); +} + +function addDefinition(definition, mode) { +    return invokeApiBg('addDefinition', {definition, mode}); +} + +function textSourceFromPoint(point) { +    const element = document.elementFromPoint(point.x, point.y); +    if (element !== null) { +        const names = ['IMG', 'INPUT', 'BUTTON', 'TEXTAREA']; +        if (names.includes(element.nodeName)) { +            return new TextSourceElement(element); +        } +    } + +    const range = document.caretRangeFromPoint(point.x, point.y); +    if (range !== null) { +        return new TextSourceRange(range); +    } + +    return null; +} + +function extractSentence(source, extent) { +    const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'}; +    const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'}; +    const terminators = '…。..??!!'; + +    const sourceLocal = source.clone(); +    const position = sourceLocal.setStartOffset(extent); +    sourceLocal.setEndOffset(position + extent); +    const content = sourceLocal.text(); + +    let quoteStack = []; + +    let startPos = 0; +    for (let i = position; i >= startPos; --i) { +        const c = content[i]; + +        if (quoteStack.length === 0 && (terminators.includes(c) || c in quotesFwd)) { +            startPos = i + 1; +            break; +        } + +        if (quoteStack.length > 0 && c === quoteStack[0]) { +            quoteStack.pop(); +        } else if (c in quotesBwd) { +            quoteStack = [quotesBwd[c]].concat(quoteStack); +        } +    } + +    quoteStack = []; + +    let endPos = content.length; +    for (let i = position; i < endPos; ++i) { +        const c = content[i]; + +        if (quoteStack.length === 0) { +            if (terminators.includes(c)) { +                endPos = i + 1; +                break; +            } +            else if (c in quotesBwd) { +                endPos = i; +                break; +            } +        } + +        if (quoteStack.length > 0 && c === quoteStack[0]) { +            quoteStack.pop(); +        } else if (c in quotesFwd) { +            quoteStack = [quotesFwd[c]].concat(quoteStack); +        } +    } + +    return content.substring(startPos, endPos).trim(); +} |