diff options
Diffstat (limited to 'ext/bg')
| -rw-r--r-- | ext/bg/background.html | 4 | ||||
| -rw-r--r-- | ext/bg/css/settings.css | 14 | ||||
| -rw-r--r-- | ext/bg/data/options-schema.json | 5 | ||||
| -rw-r--r-- | ext/bg/js/api.js | 8 | ||||
| -rw-r--r-- | ext/bg/js/backend.js | 127 | ||||
| -rw-r--r-- | ext/bg/js/clipboard-monitor.js | 80 | ||||
| -rw-r--r-- | ext/bg/js/context.js | 4 | ||||
| -rw-r--r-- | ext/bg/js/japanese.js | 454 | ||||
| -rw-r--r-- | ext/bg/js/options.js | 1 | ||||
| -rw-r--r-- | ext/bg/js/search-query-parser-generator.js | 77 | ||||
| -rw-r--r-- | ext/bg/js/search-query-parser.js | 64 | ||||
| -rw-r--r-- | ext/bg/js/search.js | 178 | ||||
| -rw-r--r-- | ext/bg/js/settings/main.js | 17 | ||||
| -rw-r--r-- | ext/bg/js/templates.js | 55 | ||||
| -rw-r--r-- | ext/bg/query-parser-templates.html | 11 | ||||
| -rw-r--r-- | ext/bg/search.html | 44 | ||||
| -rw-r--r-- | ext/bg/settings.html | 7 | 
17 files changed, 883 insertions, 267 deletions
| diff --git a/ext/bg/background.html b/ext/bg/background.html index af87eddb..7fd1c477 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -26,20 +26,20 @@          <script src="/bg/js/mecab.js"></script>          <script src="/bg/js/audio.js"></script>          <script src="/bg/js/backend-api-forwarder.js"></script> +        <script src="/bg/js/clipboard-monitor.js"></script>          <script src="/bg/js/conditions.js"></script>          <script src="/bg/js/database.js"></script>          <script src="/bg/js/deinflector.js"></script>          <script src="/bg/js/dictionary.js"></script>          <script src="/bg/js/handlebars.js"></script> +        <script src="/bg/js/japanese.js"></script>          <script src="/bg/js/json-schema.js"></script>          <script src="/bg/js/options.js"></script>          <script src="/bg/js/profile-conditions.js"></script>          <script src="/bg/js/request.js"></script> -        <script src="/bg/js/templates.js"></script>          <script src="/bg/js/translator.js"></script>          <script src="/bg/js/util.js"></script>          <script src="/mixed/js/audio.js"></script> -        <script src="/mixed/js/japanese.js"></script>          <script src="/bg/js/backend.js"></script>      </body> diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index 815a88fa..d686e8f8 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -222,6 +222,20 @@ html:root[data-operating-system=openbsd] [data-show-for-operating-system~=openbs      display: initial;  } +html:root[data-browser=edge] [data-hide-for-browser~=edge], +html:root[data-browser=chrome] [data-hide-for-browser~=chrome], +html:root[data-browser=firefox] [data-hide-for-browser~=firefox], +html:root[data-browser=firefox-mobile] [data-hide-for-browser~=firefox-mobile], +html:root[data-operating-system=mac] [data-hide-for-operating-system~=mac], +html:root[data-operating-system=win] [data-hide-for-operating-system~=win], +html:root[data-operating-system=android] [data-hide-for-operating-system~=android], +html:root[data-operating-system=cros] [data-hide-for-operating-system~=cros], +html:root[data-operating-system=linux] [data-hide-for-operating-system~=linux], +html:root[data-operating-system=openbsd] [data-hide-for-operating-system~=openbsd] { +    display: none; +} + +  @media screen and (max-width: 740px) {      .col-xs-6 {          float: none; diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json index 7e12481d..d6207952 100644 --- a/ext/bg/data/options-schema.json +++ b/ext/bg/data/options-schema.json @@ -79,6 +79,7 @@                                  "type": "object",                                  "required": [                                      "enable", +                                    "enableClipboardPopups",                                      "resultOutputMode",                                      "debugInfo",                                      "maxResults", @@ -111,6 +112,10 @@                                          "type": "boolean",                                          "default": true                                      }, +                                    "enableClipboardPopups": { +                                        "type": "boolean", +                                        "default": false +                                    },                                      "resultOutputMode": {                                          "type": "string",                                          "enum": ["group", "merge", "split"], diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index 285b8016..cd6a9d18 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -29,6 +29,14 @@ function apiGetDisplayTemplatesHtml() {      return _apiInvoke('getDisplayTemplatesHtml');  } +function apiClipboardGet() { +    return _apiInvoke('clipboardGet'); +} + +function apiGetQueryParserTemplatesHtml() { +    return _apiInvoke('getQueryParserTemplatesHtml'); +} +  function _apiInvoke(action, params={}) {      const data = {action, params};      return new Promise((resolve, reject) => { diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index eeab68a5..529055d2 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -22,6 +22,7 @@ class Backend {          this.translator = new Translator();          this.anki = new AnkiNull();          this.mecab = new Mecab(); +        this.clipboardMonitor = new ClipboardMonitor();          this.options = null;          this.optionsSchema = null;          this.optionsContext = { @@ -34,6 +35,8 @@ class Backend {          this.clipboardPasteTarget = document.querySelector('#clipboard-paste-target'); +        this.popupWindow = null; +          this.apiForwarder = new BackendApiForwarder();      } @@ -67,6 +70,8 @@ class Backend {          this.isPreparedResolve();          this.isPreparedResolve = null;          this.isPreparedPromise = null; + +        this.clipboardMonitor.onClipboardText = (text) => this._onClipboardText(text);      }      onOptionsUpdated(source) { @@ -97,6 +102,10 @@ class Backend {          }      } +    _onClipboardText(text) { +        this._onCommandSearch({mode: 'popup', query: text}); +    } +      _onZoomChange({tabId, oldZoomFactor, newZoomFactor}) {          const callback = () => this.checkLastError(chrome.runtime.lastError);          chrome.tabs.sendMessage(tabId, {action: 'zoomChanged', params: {oldZoomFactor, newZoomFactor}}, callback); @@ -121,6 +130,12 @@ class Backend {          } else {              this.mecab.stopListener();          } + +        if (options.general.enableClipboardPopups) { +            this.clipboardMonitor.start(); +        } else { +            this.clipboardMonitor.stop(); +        }      }      async getOptionsSchema() { @@ -521,13 +536,30 @@ class Backend {      }      async _onApiClipboardGet() { -        const clipboardPasteTarget = this.clipboardPasteTarget; -        clipboardPasteTarget.value = ''; -        clipboardPasteTarget.focus(); -        document.execCommand('paste'); -        const result = clipboardPasteTarget.value; -        clipboardPasteTarget.value = ''; -        return result; +        /* +        Notes: +            document.execCommand('paste') doesn't work on Firefox. +            This may be a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985 +            Therefore, navigator.clipboard.readText() is used on Firefox. + +            navigator.clipboard.readText() can't be used in Chrome for two reasons: +            * Requires page to be focused, else it rejects with an exception. +            * When the page is focused, Chrome will request clipboard permission, despite already +              being an extension with clipboard permissions. It effectively asks for the +              non-extension permission for clipboard access. +        */ +        const browser = await Backend._getBrowser(); +        if (browser === 'firefox' || browser === 'firefox-mobile') { +            return await navigator.clipboard.readText(); +        } else { +            const clipboardPasteTarget = this.clipboardPasteTarget; +            clipboardPasteTarget.value = ''; +            clipboardPasteTarget.focus(); +            document.execCommand('paste'); +            const result = clipboardPasteTarget.value; +            clipboardPasteTarget.value = ''; +            return result; +        }      }      async _onApiGetDisplayTemplatesHtml() { @@ -535,6 +567,11 @@ class Backend {          return await requestText(url, 'GET');      } +    async _onApiGetQueryParserTemplatesHtml() { +        const url = chrome.runtime.getURL('/bg/query-parser-templates.html'); +        return await requestText(url, 'GET'); +    } +      _onApiGetZoom(params, sender) {          if (!sender || !sender.tab) {              return Promise.reject(new Error('Invalid tab')); @@ -565,23 +602,68 @@ class Backend {      // Command handlers      async _onCommandSearch(params) { -        const url = chrome.runtime.getURL('/bg/search.html'); -        if (!(params && params.newTab)) { -            try { -                const tab = await Backend._findTab(1000, (url2) => ( -                    url2 !== null && -                    url2.startsWith(url) && -                    (url2.length === url.length || url2[url.length] === '?' || url2[url.length] === '#') -                )); -                if (tab !== null) { -                    await Backend._focusTab(tab); -                    return; +        const {mode='existingOrNewTab', query} = params || {}; + +        const options = await this.getOptions(this.optionsContext); +        const {popupWidth, popupHeight} = options.general; + +        const baseUrl = chrome.runtime.getURL('/bg/search.html'); +        const queryParams = {mode}; +        if (query && query.length > 0) { queryParams.query = query; } +        const queryString = new URLSearchParams(queryParams).toString(); +        const url = `${baseUrl}?${queryString}`; + +        const isTabMatch = (url2) => { +            if (url2 === null || !url2.startsWith(baseUrl)) { return false; } +            const {baseUrl: baseUrl2, queryParams: queryParams2} = parseUrl(url2); +            return baseUrl2 === baseUrl && (queryParams2.mode === mode || (!queryParams2.mode && mode === 'existingOrNewTab')); +        }; + +        const openInTab = async () => { +            const tab = await Backend._findTab(1000, isTabMatch); +            if (tab !== null) { +                await Backend._focusTab(tab); +                if (queryParams.query) { +                    await new Promise((resolve) => chrome.tabs.sendMessage( +                        tab.id, {action: 'searchQueryUpdate', params: {query: queryParams.query}}, resolve +                    ));                  } -            } catch (e) { -                // NOP +                return true;              } +        }; + +        switch (mode) { +            case 'existingOrNewTab': +                try { +                    if (await openInTab()) { return; } +                } catch (e) { +                    // NOP +                } +                chrome.tabs.create({url}); +                return; +            case 'newTab': +                chrome.tabs.create({url}); +                return; +            case 'popup': +                try { +                    // chrome.windows not supported (e.g. on Firefox mobile) +                    if (!isObject(chrome.windows)) { return; } +                    if (await openInTab()) { return; } +                    // if the previous popup is open in an invalid state, close it +                    if (this.popupWindow !== null) { +                        const callback = () => this.checkLastError(chrome.runtime.lastError); +                        chrome.windows.remove(this.popupWindow.id, callback); +                    } +                    // open new popup +                    this.popupWindow = await new Promise((resolve) => chrome.windows.create( +                        {url, width: popupWidth, height: popupHeight, type: 'popup'}, +                        resolve +                    )); +                } catch (e) { +                    // NOP +                } +                return;          } -        chrome.tabs.create({url});      }      _onCommandHelp() { @@ -708,7 +790,7 @@ class Backend {          }          try { -            const tabWindow = await new Promise((resolve) => { +            const tabWindow = await new Promise((resolve, reject) => {                  chrome.windows.get(tab.windowId, {}, (tabWindow) => {                      const e = chrome.runtime.lastError;                      if (e) { reject(e); } @@ -777,6 +859,7 @@ Backend._messageHandlers = new Map([      ['getEnvironmentInfo', (self, ...args) => self._onApiGetEnvironmentInfo(...args)],      ['clipboardGet', (self, ...args) => self._onApiClipboardGet(...args)],      ['getDisplayTemplatesHtml', (self, ...args) => self._onApiGetDisplayTemplatesHtml(...args)], +    ['getQueryParserTemplatesHtml', (self, ...args) => self._onApiGetQueryParserTemplatesHtml(...args)],      ['getZoom', (self, ...args) => self._onApiGetZoom(...args)]  ]); diff --git a/ext/bg/js/clipboard-monitor.js b/ext/bg/js/clipboard-monitor.js new file mode 100644 index 00000000..b4a27fa2 --- /dev/null +++ b/ext/bg/js/clipboard-monitor.js @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020  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 <https://www.gnu.org/licenses/>. + */ + + +class ClipboardMonitor { +    constructor() { +        this.timerId = null; +        this.timerToken = null; +        this.interval = 250; +        this.previousText = null; +    } + +    onClipboardText(_text) { +        throw new Error('Override me'); +    } + +    start() { +        this.stop(); + +        // The token below is used as a unique identifier to ensure that a new clipboard monitor +        // hasn't been started during the await call. The check below the await apiClipboardGet() +        // call will exit early if the reference has changed. +        const token = {}; +        const intervalCallback = async () => { +            this.timerId = null; + +            let text = null; +            try { +                text = await apiClipboardGet(); +            } catch (e) { +                // NOP +            } +            if (this.timerToken !== token) { return; } + +            if ( +                typeof text === 'string' && +                (text = text.trim()).length > 0 && +                text !== this.previousText +            ) { +                this.previousText = text; +                if (jpIsStringPartiallyJapanese(text)) { +                    this.onClipboardText(text); +                } +            } + +            this.timerId = setTimeout(intervalCallback, this.interval); +        }; + +        this.timerToken = token; + +        intervalCallback(); +    } + +    stop() { +        this.timerToken = null; +        if (this.timerId !== null) { +            clearTimeout(this.timerId); +            this.timerId = null; +        } +    } + +    setPreviousText(text) { +        this.previousText = text; +    } +} diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js index 834174bf..37adb6b7 100644 --- a/ext/bg/js/context.js +++ b/ext/bg/js/context.js @@ -30,12 +30,12 @@ function setupButtonEvents(selector, command, url) {      for (const node of nodes) {          node.addEventListener('click', (e) => {              if (e.button !== 0) { return; } -            apiCommandExec(command, {newTab: e.ctrlKey}); +            apiCommandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'});              e.preventDefault();          }, false);          node.addEventListener('auxclick', (e) => {              if (e.button !== 1) { return; } -            apiCommandExec(command, {newTab: true}); +            apiCommandExec(command, {mode: 'newTab'});              e.preventDefault();          }, false); diff --git a/ext/bg/js/japanese.js b/ext/bg/js/japanese.js new file mode 100644 index 00000000..c45c0958 --- /dev/null +++ b/ext/bg/js/japanese.js @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2016-2020  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 <https://www.gnu.org/licenses/>. + */ + + +const JP_HALFWIDTH_KATAKANA_MAPPING = new Map([ +    ['ヲ', 'ヲヺ-'], +    ['ァ', 'ァ--'], +    ['ィ', 'ィ--'], +    ['ゥ', 'ゥ--'], +    ['ェ', 'ェ--'], +    ['ォ', 'ォ--'], +    ['ャ', 'ャ--'], +    ['ュ', 'ュ--'], +    ['ョ', 'ョ--'], +    ['ッ', 'ッ--'], +    ['ー', 'ー--'], +    ['ア', 'ア--'], +    ['イ', 'イ--'], +    ['ウ', 'ウヴ-'], +    ['エ', 'エ--'], +    ['オ', 'オ--'], +    ['カ', 'カガ-'], +    ['キ', 'キギ-'], +    ['ク', 'クグ-'], +    ['ケ', 'ケゲ-'], +    ['コ', 'コゴ-'], +    ['サ', 'サザ-'], +    ['シ', 'シジ-'], +    ['ス', 'スズ-'], +    ['セ', 'セゼ-'], +    ['ソ', 'ソゾ-'], +    ['タ', 'タダ-'], +    ['チ', 'チヂ-'], +    ['ツ', 'ツヅ-'], +    ['テ', 'テデ-'], +    ['ト', 'トド-'], +    ['ナ', 'ナ--'], +    ['ニ', 'ニ--'], +    ['ヌ', 'ヌ--'], +    ['ネ', 'ネ--'], +    ['ノ', 'ノ--'], +    ['ハ', 'ハバパ'], +    ['ヒ', 'ヒビピ'], +    ['フ', 'フブプ'], +    ['ヘ', 'ヘベペ'], +    ['ホ', 'ホボポ'], +    ['マ', 'マ--'], +    ['ミ', 'ミ--'], +    ['ム', 'ム--'], +    ['メ', 'メ--'], +    ['モ', 'モ--'], +    ['ヤ', 'ヤ--'], +    ['ユ', 'ユ--'], +    ['ヨ', 'ヨ--'], +    ['ラ', 'ラ--'], +    ['リ', 'リ--'], +    ['ル', 'ル--'], +    ['レ', 'レ--'], +    ['ロ', 'ロ--'], +    ['ワ', 'ワ--'], +    ['ン', 'ン--'] +]); + +const JP_HIRAGANA_RANGE = [0x3040, 0x309f]; +const JP_KATAKANA_RANGE = [0x30a0, 0x30ff]; +const JP_KANA_RANGES = [JP_HIRAGANA_RANGE, JP_KATAKANA_RANGE]; + +const JP_CJK_COMMON_RANGE = [0x4e00, 0x9fff]; +const JP_CJK_RARE_RANGE = [0x3400, 0x4dbf]; +const JP_CJK_RANGES = [JP_CJK_COMMON_RANGE, JP_CJK_RARE_RANGE]; + +const JP_ITERATION_MARK_CHAR_CODE = 0x3005; + +// Japanese character ranges, roughly ordered in order of expected frequency +const JP_JAPANESE_RANGES = [ +    JP_HIRAGANA_RANGE, +    JP_KATAKANA_RANGE, + +    JP_CJK_COMMON_RANGE, +    JP_CJK_RARE_RANGE, + +    [0xff66, 0xff9f], // Halfwidth katakana + +    [0x30fb, 0x30fc], // Katakana punctuation +    [0xff61, 0xff65], // Kana punctuation +    [0x3000, 0x303f], // CJK punctuation + +    [0xff10, 0xff19], // Fullwidth numbers +    [0xff21, 0xff3a], // Fullwidth upper case Latin letters +    [0xff41, 0xff5a], // Fullwidth lower case Latin letters + +    [0xff01, 0xff0f], // Fullwidth punctuation 1 +    [0xff1a, 0xff1f], // Fullwidth punctuation 2 +    [0xff3b, 0xff3f], // Fullwidth punctuation 3 +    [0xff5b, 0xff60], // Fullwidth punctuation 4 +    [0xffe0, 0xffee]  // Currency markers +]; + + +// Helper functions + +function _jpIsCharCodeInRanges(charCode, ranges) { +    for (const [min, max] of ranges) { +        if (charCode >= min && charCode <= max) { +            return true; +        } +    } +    return false; +} + + +// Character code testing functions + +function jpIsCharCodeKanji(charCode) { +    return _jpIsCharCodeInRanges(charCode, JP_CJK_RANGES); +} + +function jpIsCharCodeKana(charCode) { +    return _jpIsCharCodeInRanges(charCode, JP_KANA_RANGES); +} + +function jpIsCharCodeJapanese(charCode) { +    return _jpIsCharCodeInRanges(charCode, JP_JAPANESE_RANGES); +} + + +// String testing functions + +function jpIsStringEntirelyKana(str) { +    if (str.length === 0) { return false; } +    for (let i = 0, ii = str.length; i < ii; ++i) { +        if (!jpIsCharCodeKana(str.charCodeAt(i))) { +            return false; +        } +    } +    return true; +} + +function jpIsStringPartiallyJapanese(str) { +    if (str.length === 0) { return false; } +    for (let i = 0, ii = str.length; i < ii; ++i) { +        if (jpIsCharCodeJapanese(str.charCodeAt(i))) { +            return true; +        } +    } +    return false; +} + + +// Conversion functions + +function jpKatakanaToHiragana(text) { +    let result = ''; +    for (const c of text) { +        if (wanakana.isKatakana(c)) { +            result += wanakana.toHiragana(c); +        } else { +            result += c; +        } +    } + +    return result; +} + +function jpHiraganaToKatakana(text) { +    let result = ''; +    for (const c of text) { +        if (wanakana.isHiragana(c)) { +            result += wanakana.toKatakana(c); +        } else { +            result += c; +        } +    } + +    return result; +} + +function jpToRomaji(text) { +    return wanakana.toRomaji(text); +} + +function jpConvertReading(expressionFragment, readingFragment, readingMode) { +    switch (readingMode) { +        case 'hiragana': +            return jpKatakanaToHiragana(readingFragment || ''); +        case 'katakana': +            return jpHiraganaToKatakana(readingFragment || ''); +        case 'romaji': +            if (readingFragment) { +                return jpToRomaji(readingFragment); +            } else { +                if (jpIsStringEntirelyKana(expressionFragment)) { +                    return jpToRomaji(expressionFragment); +                } +            } +            return readingFragment; +        case 'none': +            return null; +        default: +            return readingFragment; +    } +} + +function jpDistributeFurigana(expression, reading) { +    const fallback = [{furigana: reading, text: expression}]; +    if (!reading) { +        return fallback; +    } + +    let isAmbiguous = false; +    const segmentize = (reading, groups) => { +        if (groups.length === 0 || isAmbiguous) { +            return []; +        } + +        const group = groups[0]; +        if (group.mode === 'kana') { +            if (jpKatakanaToHiragana(reading).startsWith(jpKatakanaToHiragana(group.text))) { +                const readingLeft = reading.substring(group.text.length); +                const segs = segmentize(readingLeft, groups.splice(1)); +                if (segs) { +                    return [{text: group.text}].concat(segs); +                } +            } +        } else { +            let foundSegments = null; +            for (let i = reading.length; i >= group.text.length; --i) { +                const readingUsed = reading.substring(0, i); +                const readingLeft = reading.substring(i); +                const segs = segmentize(readingLeft, groups.slice(1)); +                if (segs) { +                    if (foundSegments !== null) { +                        // more than one way to segmentize the tail, mark as ambiguous +                        isAmbiguous = true; +                        return null; +                    } +                    foundSegments = [{text: group.text, furigana: readingUsed}].concat(segs); +                } +                // there is only one way to segmentize the last non-kana group +                if (groups.length === 1) { +                    break; +                } +            } +            return foundSegments; +        } +    }; + +    const groups = []; +    let modePrev = null; +    for (const c of expression) { +        const charCode = c.charCodeAt(0); +        const modeCurr = jpIsCharCodeKanji(charCode) || charCode === JP_ITERATION_MARK_CHAR_CODE ? 'kanji' : 'kana'; +        if (modeCurr === modePrev) { +            groups[groups.length - 1].text += c; +        } else { +            groups.push({mode: modeCurr, text: c}); +            modePrev = modeCurr; +        } +    } + +    const segments = segmentize(reading, groups); +    if (segments && !isAmbiguous) { +        return segments; +    } +    return fallback; +} + +function jpDistributeFuriganaInflected(expression, reading, source) { +    const output = []; + +    let stemLength = 0; +    const shortest = Math.min(source.length, expression.length); +    const sourceHiragana = jpKatakanaToHiragana(source); +    const expressionHiragana = jpKatakanaToHiragana(expression); +    while (stemLength < shortest && sourceHiragana[stemLength] === expressionHiragana[stemLength]) { +        ++stemLength; +    } +    const offset = source.length - stemLength; + +    const stemExpression = source.substring(0, source.length - offset); +    const stemReading = reading.substring( +        0, +        offset === 0 ? reading.length : reading.length - expression.length + stemLength +    ); +    for (const segment of jpDistributeFurigana(stemExpression, stemReading)) { +        output.push(segment); +    } + +    if (stemLength !== source.length) { +        output.push({text: source.substring(stemLength)}); +    } + +    return output; +} + +function jpConvertHalfWidthKanaToFullWidth(text, sourceMapping) { +    let result = ''; +    const ii = text.length; +    const hasSourceMapping = Array.isArray(sourceMapping); + +    for (let i = 0; i < ii; ++i) { +        const c = text[i]; +        const mapping = JP_HALFWIDTH_KATAKANA_MAPPING.get(c); +        if (typeof mapping !== 'string') { +            result += c; +            continue; +        } + +        let index = 0; +        switch (text.charCodeAt(i + 1)) { +            case 0xff9e: // dakuten +                index = 1; +                break; +            case 0xff9f: // handakuten +                index = 2; +                break; +        } + +        let c2 = mapping[index]; +        if (index > 0) { +            if (c2 === '-') { // invalid +                index = 0; +                c2 = mapping[0]; +            } else { +                ++i; +            } +        } + +        if (hasSourceMapping && index > 0) { +            index = result.length; +            const v = sourceMapping.splice(index + 1, 1)[0]; +            sourceMapping[index] += v; +        } +        result += c2; +    } + +    return result; +} + +function jpConvertNumericTofullWidth(text) { +    let result = ''; +    for (let i = 0, ii = text.length; i < ii; ++i) { +        let c = text.charCodeAt(i); +        if (c >= 0x30 && c <= 0x39) { // ['0', '9'] +            c += 0xff10 - 0x30; // 0xff10 = '0' full width +            result += String.fromCharCode(c); +        } else { +            result += text[i]; +        } +    } +    return result; +} + +function jpConvertAlphabeticToKana(text, sourceMapping) { +    let part = ''; +    let result = ''; +    const ii = text.length; + +    if (sourceMapping.length === ii) { +        sourceMapping.length = ii; +        sourceMapping.fill(1); +    } + +    for (let i = 0; i < ii; ++i) { +        // Note: 0x61 is the character code for 'a' +        let c = text.charCodeAt(i); +        if (c >= 0x41 && c <= 0x5a) { // ['A', 'Z'] +            c += (0x61 - 0x41); +        } else if (c >= 0x61 && c <= 0x7a) { // ['a', 'z'] +            // NOP; c += (0x61 - 0x61); +        } else if (c >= 0xff21 && c <= 0xff3a) { // ['A', 'Z'] fullwidth +            c += (0x61 - 0xff21); +        } else if (c >= 0xff41 && c <= 0xff5a) { // ['a', 'z'] fullwidth +            c += (0x61 - 0xff41); +        } else if (c === 0x2d || c === 0xff0d) { // '-' or fullwidth dash +            c = 0x2d; // '-' +        } else { +            if (part.length > 0) { +                result += jpToHiragana(part, sourceMapping, result.length); +                part = ''; +            } +            result += text[i]; +            continue; +        } +        part += String.fromCharCode(c); +    } + +    if (part.length > 0) { +        result += jpToHiragana(part, sourceMapping, result.length); +    } +    return result; +} + +function jpToHiragana(text, sourceMapping, sourceMappingStart) { +    const result = wanakana.toHiragana(text); + +    // Generate source mapping +    if (Array.isArray(sourceMapping)) { +        if (typeof sourceMappingStart !== 'number') { sourceMappingStart = 0; } +        let i = 0; +        let resultPos = 0; +        const ii = text.length; +        while (i < ii) { +            // Find smallest matching substring +            let iNext = i + 1; +            let resultPosNext = result.length; +            while (iNext < ii) { +                const t = wanakana.toHiragana(text.substring(0, iNext)); +                if (t === result.substring(0, t.length)) { +                    resultPosNext = t.length; +                    break; +                } +                ++iNext; +            } + +            // Merge characters +            const removals = iNext - i - 1; +            if (removals > 0) { +                let sum = 0; +                const vs = sourceMapping.splice(sourceMappingStart + 1, removals); +                for (const v of vs) { sum += v; } +                sourceMapping[sourceMappingStart] += sum; +            } +            ++sourceMappingStart; + +            // Empty elements +            const additions = resultPosNext - resultPos - 1; +            for (let j = 0; j < additions; ++j) { +                sourceMapping.splice(sourceMappingStart, 0, 0); +                ++sourceMappingStart; +            } + +            i = iNext; +            resultPos = resultPosNext; +        } +    } + +    return result; +} diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 78508059..97032660 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -266,6 +266,7 @@ function profileOptionsCreateDefaults() {      return {          general: {              enable: true, +            enableClipboardPopups: false,              resultOutputMode: 'group',              debugInfo: false,              maxResults: 32, diff --git a/ext/bg/js/search-query-parser-generator.js b/ext/bg/js/search-query-parser-generator.js new file mode 100644 index 00000000..8d71890b --- /dev/null +++ b/ext/bg/js/search-query-parser-generator.js @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020  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 <https://www.gnu.org/licenses/>. + */ + + +class QueryParserGenerator { +    constructor() { +        this._templateHandler = null; +    } + +    async prepare() { +        const html = await apiGetQueryParserTemplatesHtml(); +        this._templateHandler = new TemplateHandler(html); +    } + +    createParseResult(terms, preview=false) { +        const fragment = document.createDocumentFragment(); +        for (const term of terms) { +            const termContainer = this._templateHandler.instantiate(preview ? 'term-preview' : 'term'); +            for (const segment of term) { +                if (!segment.text.trim()) { continue; } +                if (!segment.reading || !segment.reading.trim()) { +                    termContainer.appendChild(this.createSegmentText(segment.text)); +                } else { +                    termContainer.appendChild(this.createSegment(segment)); +                } +            } +            fragment.appendChild(termContainer); +        } +        return fragment; +    } + +    createSegment(segment) { +        const segmentContainer = this._templateHandler.instantiate('segment'); +        const segmentTextContainer = segmentContainer.querySelector('.query-parser-segment-text'); +        const segmentReadingContainer = segmentContainer.querySelector('.query-parser-segment-reading'); +        segmentTextContainer.appendChild(this.createSegmentText(segment.text)); +        segmentReadingContainer.innerText = segment.reading; +        return segmentContainer; +    } + +    createSegmentText(text) { +        const fragment = document.createDocumentFragment(); +        for (const chr of text) { +            const charContainer = this._templateHandler.instantiate('char'); +            charContainer.innerText = chr; +            fragment.appendChild(charContainer); +        } +        return fragment; +    } + +    createParserSelect(parseResults, selectedParser) { +        const selectContainer = this._templateHandler.instantiate('select'); +        for (const parseResult of parseResults) { +            const optionContainer = this._templateHandler.instantiate('select-option'); +            optionContainer.value = parseResult.id; +            optionContainer.innerText = parseResult.name; +            optionContainer.defaultSelected = selectedParser === parseResult.id; +            selectContainer.appendChild(optionContainer); +        } +        return selectContainer; +    } +} diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index e8e6d11f..f648fdd4 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -19,14 +19,20 @@  class QueryParser extends TextScanner {      constructor(search) { -        super(document.querySelector('#query-parser'), [], [], []); +        super(document.querySelector('#query-parser-content'), [], [], []);          this.search = search;          this.parseResults = [];          this.selectedParser = null; -        this.queryParser = document.querySelector('#query-parser'); -        this.queryParserSelect = document.querySelector('#query-parser-select'); +        this.queryParser = document.querySelector('#query-parser-content'); +        this.queryParserSelect = document.querySelector('#query-parser-select-container'); + +        this.queryParserGenerator = new QueryParserGenerator(); +    } + +    async prepare() { +        await this.queryParserGenerator.prepare();      }      onError(error) { @@ -64,7 +70,7 @@ class QueryParser extends TextScanner {          const selectedParser = e.target.value;          this.selectedParser = selectedParser;          apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext()); -        this.renderParseResult(this.getParseResult()); +        this.renderParseResult();      }      getMouseEventListeners() { @@ -113,13 +119,13 @@ class QueryParser extends TextScanner {      async setText(text) {          this.search.setSpinnerVisible(true); -        await this.setPreview(text); +        this.setPreview(text);          this.parseResults = await this.parseText(text);          this.refreshSelectedParser();          this.renderParserSelect(); -        await this.renderParseResult(); +        this.renderParseResult();          this.search.setSpinnerVisible(false);      } @@ -146,57 +152,29 @@ class QueryParser extends TextScanner {          return results;      } -    async setPreview(text) { +    setPreview(text) {          const previewTerms = [];          for (let i = 0, ii = text.length; i < ii; i += 2) {              const tempText = text.substring(i, i + 2); -            previewTerms.push([{text: tempText.split('')}]); +            previewTerms.push([{text: tempText}]);          } -        this.queryParser.innerHTML = await apiTemplateRender('query-parser.html', { -            terms: previewTerms, -            preview: true -        }); +        this.queryParser.textContent = ''; +        this.queryParser.appendChild(this.queryParserGenerator.createParseResult(previewTerms, true));      }      renderParserSelect() {          this.queryParserSelect.innerHTML = '';          if (this.parseResults.length > 1) { -            const select = document.createElement('select'); -            select.classList.add('form-control'); -            for (const parseResult of this.parseResults) { -                const option = document.createElement('option'); -                option.value = parseResult.id; -                option.innerText = parseResult.name; -                option.defaultSelected = this.selectedParser === parseResult.id; -                select.appendChild(option); -            } +            const select = this.queryParserGenerator.createParserSelect(this.parseResults, this.selectedParser);              select.addEventListener('change', this.onParserChange.bind(this));              this.queryParserSelect.appendChild(select);          }      } -    async renderParseResult() { +    renderParseResult() {          const parseResult = this.getParseResult(); -        if (!parseResult) { -            this.queryParser.innerHTML = ''; -            return; -        } - -        this.queryParser.innerHTML = await apiTemplateRender( -            'query-parser.html', -            {terms: QueryParser.processParseResultForDisplay(parseResult.parsedText)} -        ); -    } - -    static processParseResultForDisplay(result) { -        return result.map((term) => { -            return term.filter((part) => part.text.trim()).map((part) => { -                return { -                    text: part.text.split(''), -                    reading: part.reading, -                    raw: !part.reading || !part.reading.trim() -                }; -            }); -        }); +        this.queryParser.textContent = ''; +        if (!parseResult) { return; } +        this.queryParser.appendChild(this.queryParserGenerator.createParseResult(parseResult.parsedText));      }  } diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index f5c641a8..6641255f 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -36,12 +36,7 @@ class DisplaySearch extends Display {          this.introVisible = true;          this.introAnimationTimer = null; -        this.isFirefox = false; - -        this.clipboardMonitorTimerId = null; -        this.clipboardMonitorTimerToken = null; -        this.clipboardInterval = 250; -        this.clipboardPreviousText = null; +        this.clipboardMonitor = new ClipboardMonitor();      }      static create() { @@ -53,12 +48,16 @@ class DisplaySearch extends Display {      async prepare() {          try {              await this.initialize(); -            this.isFirefox = await DisplaySearch._isFirefox(); + +            await this.queryParser.prepare(); + +            const {queryParams: {query='', mode=''}} = parseUrl(window.location.href);              if (this.search !== null) {                  this.search.addEventListener('click', (e) => this.onSearch(e), false);              }              if (this.query !== null) { +                document.documentElement.dataset.searchMode = mode;                  this.query.addEventListener('input', () => this.onSearchInput(), false);                  if (this.wanakanaEnable !== null) { @@ -69,34 +68,26 @@ class DisplaySearch extends Display {                          this.wanakanaEnable.checked = false;                      }                      this.wanakanaEnable.addEventListener('change', (e) => { -                        const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || ''; +                        const {queryParams: {query=''}} = parseUrl(window.location.href);                          if (e.target.checked) {                              window.wanakana.bind(this.query); -                            this.setQuery(window.wanakana.toKana(query));                              apiOptionsSet({general: {enableWanakana: true}}, this.getOptionsContext());                          } else {                              window.wanakana.unbind(this.query); -                            this.setQuery(query);                              apiOptionsSet({general: {enableWanakana: false}}, this.getOptionsContext());                          } +                        this.setQuery(query);                          this.onSearchQueryUpdated(this.query.value, false);                      });                  } -                const query = DisplaySearch.getSearchQueryFromLocation(window.location.href); -                if (query !== null) { -                    if (this.isWanakanaEnabled()) { -                        this.setQuery(window.wanakana.toKana(query)); -                    } else { -                        this.setQuery(query); -                    } -                    this.onSearchQueryUpdated(this.query.value, false); -                } +                this.setQuery(query); +                this.onSearchQueryUpdated(this.query.value, false);              } -            if (this.clipboardMonitorEnable !== null) { +            if (this.clipboardMonitorEnable !== null && mode !== 'popup') {                  if (this.options.general.enableClipboardMonitor === true) {                      this.clipboardMonitorEnable.checked = true; -                    this.startClipboardMonitor(); +                    this.clipboardMonitor.start();                  } else {                      this.clipboardMonitorEnable.checked = false;                  } @@ -106,7 +97,7 @@ class DisplaySearch extends Display {                              {permissions: ['clipboardRead']},                              (granted) => {                                  if (granted) { -                                    this.startClipboardMonitor(); +                                    this.clipboardMonitor.start();                                      apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext());                                  } else {                                      e.target.checked = false; @@ -114,16 +105,20 @@ class DisplaySearch extends Display {                              }                          );                      } else { -                        this.stopClipboardMonitor(); +                        this.clipboardMonitor.stop();                          apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext());                      }                  });              } +            chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); +              window.addEventListener('popstate', (e) => this.onPopState(e)); +            window.addEventListener('copy', (e) => this.onCopy(e)); + +            this.clipboardMonitor.onClipboardText = (text) => this.onExternalSearchUpdate(text);              this.updateSearchButton(); -            this.initClipboardMonitor();          } catch (e) {              this.onError(e);          } @@ -159,25 +154,32 @@ class DisplaySearch extends Display {          e.preventDefault();          const query = this.query.value; +          this.queryParser.setText(query); -        const queryString = query.length > 0 ? `?query=${encodeURIComponent(query)}` : ''; -        window.history.pushState(null, '', `${window.location.pathname}${queryString}`); + +        const url = new URL(window.location.href); +        url.searchParams.set('query', query); +        window.history.pushState(null, '', url.toString()); +          this.onSearchQueryUpdated(query, true);      }      onPopState() { -        const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || ''; -        if (this.query !== null) { -            if (this.isWanakanaEnabled()) { -                this.setQuery(window.wanakana.toKana(query)); -            } else { -                this.setQuery(query); -            } -        } - +        const {queryParams: {query='', mode=''}} = parseUrl(window.location.href); +        document.documentElement.dataset.searchMode = mode; +        this.setQuery(query);          this.onSearchQueryUpdated(this.query.value, false);      } +    onRuntimeMessage({action, params}, sender, callback) { +        const handler = DisplaySearch._runtimeMessageHandlers.get(action); +        if (typeof handler !== 'function') { return false; } + +        const result = handler(this, params, sender); +        callback(result); +        return false; +    } +      onKeyDown(e) {          const key = Display.getKeyFromEvent(e);          const ignoreKeys = DisplaySearch.onKeyDownIgnoreKeys; @@ -202,6 +204,19 @@ class DisplaySearch extends Display {          }      } +    onCopy() { +        // ignore copy from search page +        this.clipboardMonitor.setPreviousText(document.getSelection().toString().trim()); +    } + +    onExternalSearchUpdate(text) { +        this.setQuery(text); +        const url = new URL(window.location.href); +        url.searchParams.set('query', text); +        window.history.pushState(null, '', url.toString()); +        this.onSearchQueryUpdated(this.query.value, true); +    } +      async onSearchQueryUpdated(query, animate) {          try {              const details = {}; @@ -241,74 +256,6 @@ class DisplaySearch extends Display {          this.queryParser.setOptions(this.options);      } -    initClipboardMonitor() { -        // ignore copy from search page -        window.addEventListener('copy', () => { -            this.clipboardPreviousText = document.getSelection().toString().trim(); -        }); -    } - -    startClipboardMonitor() { -        // The token below is used as a unique identifier to ensure that a new clipboard monitor -        // hasn't been started during the await call. The check below the await this.getClipboardText() -        // call will exit early if the reference has changed. -        const token = {}; -        const intervalCallback = async () => { -            this.clipboardMonitorTimerId = null; - -            let text = await this.getClipboardText(); -            if (this.clipboardMonitorTimerToken !== token) { return; } - -            if ( -                typeof text === 'string' && -                (text = text.trim()).length > 0 && -                text !== this.clipboardPreviousText -            ) { -                this.clipboardPreviousText = text; -                if (jpIsStringPartiallyJapanese(text)) { -                    this.setQuery(this.isWanakanaEnabled() ? window.wanakana.toKana(text) : text); -                    window.history.pushState(null, '', `${window.location.pathname}?query=${encodeURIComponent(text)}`); -                    this.onSearchQueryUpdated(this.query.value, true); -                } -            } - -            this.clipboardMonitorTimerId = setTimeout(intervalCallback, this.clipboardInterval); -        }; - -        this.clipboardMonitorTimerToken = token; - -        intervalCallback(); -    } - -    stopClipboardMonitor() { -        this.clipboardMonitorTimerToken = null; -        if (this.clipboardMonitorTimerId !== null) { -            clearTimeout(this.clipboardMonitorTimerId); -            this.clipboardMonitorTimerId = null; -        } -    } - -    async getClipboardText() { -        /* -        Notes: -            apiClipboardGet doesn't work on Firefox because document.execCommand('paste') -            results in an empty string on the web extension background page. -            This may be a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985 -            Therefore, navigator.clipboard.readText() is used on Firefox. - -            navigator.clipboard.readText() can't be used in Chrome for two reasons: -            * Requires page to be focused, else it rejects with an exception. -            * When the page is focused, Chrome will request clipboard permission, despite already -              being an extension with clipboard permissions. It effectively asks for the -              non-extension permission for clipboard access. -        */ -        try { -            return this.isFirefox ? await navigator.clipboard.readText() : await apiClipboardGet(); -        } catch (e) { -            return null; -        } -    } -      isWanakanaEnabled() {          return this.wanakanaEnable !== null && this.wanakanaEnable.checked;      } @@ -318,8 +265,9 @@ class DisplaySearch extends Display {      }      setQuery(query) { -        this.query.value = query; -        this.queryParser.setText(query); +        const interpretedQuery = this.isWanakanaEnabled() ? window.wanakana.toKana(query) : query; +        this.query.value = interpretedQuery; +        this.queryParser.setText(interpretedQuery);      }      setIntroVisible(visible, animate) { @@ -394,22 +342,6 @@ class DisplaySearch extends Display {              document.title = `${text} - Yomichan Search`;          }      } - -    static getSearchQueryFromLocation(url) { -        const match = /^[^?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url); -        return match !== null ? decodeURIComponent(match[1]) : null; -    } - -    static async _isFirefox() { -        const {browser} = await apiGetEnvironmentInfo(); -        switch (browser) { -            case 'firefox': -            case 'firefox-mobile': -                return true; -            default: -                return false; -        } -    }  }  DisplaySearch.onKeyDownIgnoreKeys = { @@ -427,4 +359,8 @@ DisplaySearch.onKeyDownIgnoreKeys = {      'Shift': []  }; +DisplaySearch._runtimeMessageHandlers = new Map([ +    ['searchQueryUpdate', (self, {query}) => { self.onExternalSearchUpdate(query); }] +]); +  DisplaySearch.instance = DisplaySearch.create(); diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 4492cd42..cf0f08db 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -28,6 +28,22 @@ function getOptionsFullMutable() {  async function formRead(options) {      options.general.enable = $('#enable').prop('checked'); +    const enableClipboardPopups = $('#enable-clipboard-popups').prop('checked'); +    if (enableClipboardPopups) { +        options.general.enableClipboardPopups = await new Promise((resolve, _reject) => { +            chrome.permissions.request( +                {permissions: ['clipboardRead']}, +                (granted) => { +                    if (!granted) { +                        $('#enable-clipboard-popups').prop('checked', false); +                    } +                    resolve(granted); +                } +            ); +        }); +    } else { +        options.general.enableClipboardPopups = false; +    }      options.general.showGuide = $('#show-usage-guide').prop('checked');      options.general.compactTags = $('#compact-tags').prop('checked');      options.general.compactGlossaries = $('#compact-glossaries').prop('checked'); @@ -104,6 +120,7 @@ async function formRead(options) {  async function formWrite(options) {      $('#enable').prop('checked', options.general.enable); +    $('#enable-clipboard-popups').prop('checked', options.general.enableClipboardPopups);      $('#show-usage-guide').prop('checked', options.general.showGuide);      $('#compact-tags').prop('checked', options.general.compactTags);      $('#compact-glossaries').prop('checked', options.general.compactGlossaries); diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js deleted file mode 100644 index 2f65be31..00000000 --- a/ext/bg/js/templates.js +++ /dev/null @@ -1,55 +0,0 @@ -(function() { -  var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; -templates['query-parser.html'] = template({"1":function(container,depth0,helpers,partials,data) { -    var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); - -  return ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.preview : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.program(4, data, 0),"data":data})) != null ? stack1 : "") -    + ((stack1 = helpers.each.call(alias1,depth0,{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "</span>"; -},"2":function(container,depth0,helpers,partials,data) { -    return "<span class=\"query-parser-term-preview\">"; -},"4":function(container,depth0,helpers,partials,data) { -    return "<span class=\"query-parser-term\">"; -},"6":function(container,depth0,helpers,partials,data) { -    var stack1; - -  return ((stack1 = container.invokePartial(partials.part,depth0,{"name":"part","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"8":function(container,depth0,helpers,partials,data) { -    var stack1; - -  return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.raw : depth0),{"name":"if","hash":{},"fn":container.program(9, data, 0),"inverse":container.program(12, data, 0),"data":data})) != null ? stack1 : ""); -},"9":function(container,depth0,helpers,partials,data) { -    var stack1; - -  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.text : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"10":function(container,depth0,helpers,partials,data) { -    return "<span class=\"query-parser-char\">" -    + container.escapeExpression(container.lambda(depth0, depth0)) -    + "</span>"; -},"12":function(container,depth0,helpers,partials,data) { -    var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}); - -  return "<ruby>" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.text : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "<rt>" -    + container.escapeExpression(((helper = (helper = helpers.reading || (depth0 != null ? depth0.reading : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"reading","hash":{},"data":data}) : helper))) -    + "</rt></ruby>"; -},"14":function(container,depth0,helpers,partials,data,blockParams,depths) { -    var stack1; - -  return ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"preview":(depths[1] != null ? depths[1].preview : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) { -    var stack1; - -  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.terms : depth0),{"name":"each","hash":{},"fn":container.program(14, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"main_d":  function(fn, props, container, depth0, data, blockParams, depths) { - -  var decorators = container.decorators; - -  fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"args":["term"],"data":data}) || fn; -  fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(8, data, 0, blockParams, depths),"inverse":container.noop,"args":["part"],"data":data}) || fn; -  return fn; -  } - -,"useDecorators":true,"usePartial":true,"useData":true,"useDepths":true}); -})();
\ No newline at end of file diff --git a/ext/bg/query-parser-templates.html b/ext/bg/query-parser-templates.html new file mode 100644 index 00000000..7cab16a9 --- /dev/null +++ b/ext/bg/query-parser-templates.html @@ -0,0 +1,11 @@ +<!DOCTYPE html><html><head></head><body> + +<template id="term-template"><span class="query-parser-term" data-type="normal"></span></template> +<template id="term-preview-template"><span class="query-parser-term" data-type="preview"></span></template> +<template id="segment-template"><ruby class="query-parser-segment"><span class="query-parser-segment-text"></span><rt class="query-parser-segment-reading"></rt></ruby></template> +<template id="char-template"><span class="query-parser-char"></span></template> + +<template id="select-template"><select class="query-parser-select form-control"></select></template> +<template id="select-option-template"><option class="query-parser-select-option"></option></template> + +</body></html> diff --git a/ext/bg/search.html b/ext/bg/search.html index 74afbb68..d6336826 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -25,29 +25,31 @@                  <p style="margin-bottom: 0;">Search your installed dictionaries by entering a Japanese expression into the field below.</p>              </div> -            <div class="input-group" style="padding-top: 20px;"> -                <span title="Enable kana input method" class="input-group-text"> -                    <input type="checkbox" id="wanakana-enable" class="icon-checkbox" /> -                    <label for="wanakana-enable" class="scan-disable">あ</label> -                </span> -                <span title="Enable clipboard monitor" class="input-group-text"> -                    <input type="checkbox" id="clipboard-monitor-enable" class="icon-checkbox" /> -                    <label for="clipboard-monitor-enable"><span class="glyphicon glyphicon-paste"></span></label> -                </span> -            </div> +            <div class="search-input"> +                <div class="input-group" style="padding-top: 20px;"> +                    <span title="Enable kana input method" class="input-group-text"> +                        <input type="checkbox" id="wanakana-enable" class="icon-checkbox" /> +                        <label for="wanakana-enable" class="scan-disable">あ</label> +                    </span> +                    <span title="Enable clipboard monitor" class="input-group-text"> +                        <input type="checkbox" id="clipboard-monitor-enable" class="icon-checkbox" /> +                        <label for="clipboard-monitor-enable"><span class="glyphicon glyphicon-paste"></span></label> +                    </span> +                </div> -            <form class="input-group"> -                <input type="text" class="form-control" placeholder="Search for..." id="query" autofocus> -                <span class="input-group-btn"> -                    <input type="submit" class="btn btn-default form-control" id="search" value="Search"> -                </span> -            </form> +                <form class="input-group"> +                    <input type="text" class="form-control" placeholder="Search for..." id="query" autofocus> +                    <span class="input-group-btn"> +                        <input type="submit" class="btn btn-default form-control" id="search" value="Search"> +                    </span> +                </form> +            </div>              <div id="spinner" hidden><img src="/mixed/img/spinner.gif"></div>              <div class="scan-disable"> -                <div id="query-parser-select" class="input-group"></div> -                <div id="query-parser"></div> +                <div id="query-parser-select-container" class="input-group"></div> +                <div id="query-parser-content"></div>              </div>              <hr> @@ -75,18 +77,20 @@          <script src="/bg/js/dictionary.js"></script>          <script src="/bg/js/handlebars.js"></script> -        <script src="/bg/js/templates.js"></script> +        <script src="/bg/js/japanese.js"></script>          <script src="/fg/js/document.js"></script>          <script src="/fg/js/source.js"></script>          <script src="/mixed/js/audio.js"></script>          <script src="/mixed/js/display-context.js"></script>          <script src="/mixed/js/display.js"></script>          <script src="/mixed/js/display-generator.js"></script> -        <script src="/mixed/js/japanese.js"></script>          <script src="/mixed/js/scroll.js"></script>          <script src="/mixed/js/text-scanner.js"></script> +        <script src="/mixed/js/template-handler.js"></script> +        <script src="/bg/js/search-query-parser-generator.js"></script>          <script src="/bg/js/search-query-parser.js"></script> +        <script src="/bg/js/clipboard-monitor.js"></script>          <script src="/bg/js/search.js"></script>          <script src="/bg/js/search-frontend.js"></script>      </body> diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 77bcc359..b048a36c 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -134,6 +134,10 @@                      <label><input type="checkbox" id="enable"> Enable content scanning</label>                  </div> +                <div class="checkbox" data-hide-for-browser="firefox-mobile"> +                    <label><input type="checkbox" id="enable-clipboard-popups"> Enable native popups when copying Japanese text</label> +                </div> +                  <div class="checkbox">                      <label><input type="checkbox" id="show-usage-guide"> Show usage guide on startup</label>                  </div> @@ -1084,16 +1088,15 @@          <script src="/mixed/js/core.js"></script>          <script src="/mixed/js/dom.js"></script>          <script src="/mixed/js/api.js"></script> -        <script src="/mixed/js/japanese.js"></script>          <script src="/bg/js/anki.js"></script>          <script src="/bg/js/conditions.js"></script>          <script src="/bg/js/dictionary.js"></script>          <script src="/bg/js/handlebars.js"></script> +        <script src="/bg/js/japanese.js"></script>          <script src="/bg/js/options.js"></script>          <script src="/bg/js/page-exit-prevention.js"></script>          <script src="/bg/js/profile-conditions.js"></script> -        <script src="/bg/js/templates.js"></script>          <script src="/bg/js/util.js"></script>          <script src="/mixed/js/audio.js"></script> |