diff options
| -rw-r--r-- | ext/bg/js/ankiweb.js | 180 | ||||
| -rw-r--r-- | ext/bg/js/options-form.js | 7 | ||||
| -rw-r--r-- | ext/bg/js/options.js | 14 | ||||
| -rw-r--r-- | ext/bg/js/templates.js | 355 | ||||
| -rw-r--r-- | ext/bg/js/translator.js | 21 | ||||
| -rw-r--r-- | ext/bg/js/util.js | 154 | ||||
| -rw-r--r-- | ext/bg/js/yomichan.js | 96 | ||||
| -rw-r--r-- | ext/bg/options.html | 4 | ||||
| -rw-r--r-- | ext/fg/css/frame.css | 69 | ||||
| -rw-r--r-- | ext/fg/frame.html | 19 | ||||
| -rw-r--r-- | ext/fg/img/spinner.gif | bin | 0 -> 7358 bytes | |||
| -rw-r--r-- | ext/fg/js/driver.js | 228 | ||||
| -rw-r--r-- | ext/fg/js/frame.js | 168 | ||||
| -rw-r--r-- | ext/fg/js/popup.js | 63 | ||||
| -rw-r--r-- | ext/fg/js/util.js | 22 | ||||
| -rw-r--r-- | ext/manifest.json | 2 | ||||
| -rw-r--r-- | tmpl/footer.html | 3 | ||||
| -rw-r--r-- | tmpl/header.html | 18 | ||||
| -rw-r--r-- | tmpl/kanji-link.html | 1 | ||||
| -rw-r--r-- | tmpl/kanji-list.html | 56 | ||||
| -rw-r--r-- | tmpl/kanji.html | 50 | ||||
| -rw-r--r-- | tmpl/model.html | 2 | ||||
| -rw-r--r-- | tmpl/term-list.html | 68 | ||||
| -rw-r--r-- | tmpl/term.html | 41 | 
24 files changed, 980 insertions, 661 deletions
| diff --git a/ext/bg/js/ankiweb.js b/ext/bg/js/ankiweb.js new file mode 100644 index 00000000..69a1b44d --- /dev/null +++ b/ext/bg/js/ankiweb.js @@ -0,0 +1,180 @@ +/* + * 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 AnkiWeb { +    constructor(username, password) { +        this.username = username; +        this.password = password; +        this.noteInfo = null; + +        chrome.webRequest.onBeforeSendHeaders.addListener( +            details => { +                details.requestHeaders.push({name: 'Origin', value: 'https://ankiweb.net'}); +                return {requestHeaders: details.requestHeaders}; +            }, +            {urls: ['https://ankiweb.net/*']}, +            ['blocking', 'requestHeaders'] +        ); +    } + +    addNote(note) { +        return this.retrieve().then(info => { +            const model = info.models.find(m => m.name === note.modelName); +            if (!model) { +                return Promise.reject('cannot add note model provided'); +            } + +            const fields = []; +            for (const field of model.fields) { +                fields.push(note.fields[field]); +            } + +            const data = { +                data: JSON.stringify([fields, note.tags.join(' ')]), +                mid: model.id, +                deck: note.deckName, +                csrf_token: info.token +            }; + +            return AnkiWeb.loadAccountPage('https://ankiweb.net/edit/save', data, this.username, this.password); +        }).then(response => response !== '0'); +    } + +    canAddNotes(notes) { +        return Promise.resolve(new Array(notes.length).fill(true)); +    } + +    getDeckNames() { +        return this.retrieve().then(info => info.deckNames); +    } + +    getModelNames() { +        return this.retrieve().then(info => info.models.map(m => m.name)); +    } + +    getModelFieldNames(modelName) { +        return this.retrieve().then(info => { +            const model = info.models.find(m => m.name === modelName); +            return model ? model.fields : []; +        }); +    } + +    retrieve() { +        if (this.noteInfo !== null) { +            return Promise.resolve(this.noteInfo); +        } + +        return AnkiWeb.scrape(this.username, this.password).then(({deckNames, models, token}) => { +            this.noteInfo = {deckNames, models, token}; +            return this.noteInfo; +        }); +    } + +    logout() { +        return AnkiWeb.loadPage('https://ankiweb.net/account/logout', null); +    } + +    static scrape(username, password) { +        return AnkiWeb.loadAccountPage('https://ankiweb.net/edit/', null, username, password).then(response => { +            const modelsMatch = /editor\.models = (.*}]);/.exec(response); +            if (modelsMatch === null) { +                return Promise.reject('failed to scrape model data'); +            } + +            const decksMatch = /editor\.decks = (.*}});/.exec(response); +            if (decksMatch === null) { +                return Promise.reject('failed to scrape deck data'); +            } + +            const tokenMatch = /editor\.csrf_token = \'(.*)\';/.exec(response); +            if (tokenMatch === null) { +                return Promise.reject('failed to acquire csrf_token'); +            } + +            const modelsJson = JSON.parse(modelsMatch[1]); +            const decksJson = JSON.parse(decksMatch[1]); +            const token = tokenMatch[1]; + +            const deckNames = Object.keys(decksJson).map(d => decksJson[d].name); +            const models = []; +            for (const modelJson of modelsJson) { +                models.push({ +                    name: modelJson.name, +                    id: modelJson.id, +                    fields: modelJson.flds.map(f => f.name) +                }); +            } + +            return {deckNames, models, token}; +        }); +    } + +    static login(username, password, token) { +        if (username.length === 0 || password.length === 0) { +            return Promise.reject('login credentials not specified'); +        } + +        const data = {username, password, csrf_token: token, submitted: 1}; +        return AnkiWeb.loadPage('https://ankiweb.net/account/login', data).then(response => { +            if (!response.includes('class="mitem"')) { +                return Promise.reject('failed to authenticate'); +            } +        }); +    } + +    static loadAccountPage(url, data, username, password) { +        return AnkiWeb.loadPage(url, data).then(response => { +            if (response.includes('name="password"')) { +                const tokenMatch = /name="csrf_token" value="(.*)"/.exec(response); +                if (tokenMatch === null) { +                    return Promise.reject('failed to acquire csrf_token'); +                } + +                return AnkiWeb.login(username, password, tokenMatch[1]).then(() => AnkiWeb.loadPage(url, data)); +            } else { +                return response; +            } +        }); +    } + +    static loadPage(url, data) { +        return new Promise((resolve, reject) => { +            let dataEnc = null; +            if (data) { +                const params = []; +                for (const key in data) { +                    params.push(`${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`); +                } + +                dataEnc = params.join('&'); +            } + +            const xhr = new XMLHttpRequest(); +            xhr.addEventListener('error', () => reject('failed to execute network request')); +            xhr.addEventListener('load', () => resolve(xhr.responseText)); +            if (dataEnc) { +                xhr.open('POST', url); +                xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); +                xhr.send(dataEnc); +            } else { +                xhr.open('GET', url); +                xhr.send(); +            } +        }); +    } +} diff --git a/ext/bg/js/options-form.js b/ext/bg/js/options-form.js index 8216f158..8cffb2f7 100644 --- a/ext/bg/js/options-form.js +++ b/ext/bg/js/options-form.js @@ -30,8 +30,9 @@ function getFormValues() {          optsNew.activateOnStartup = $('#activate-on-startup').prop('checked');          optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked'); -        optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked');          optsNew.enableSoftKatakanaSearch = $('#enable-soft-katakana-search').prop('checked'); +        optsNew.groupTermResults = $('#group-term-results').prop('checked'); +        optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked');          optsNew.holdShiftToScan = $('#hold-shift-to-scan').prop('checked');          optsNew.selectMatchedText = $('#select-matched-text').prop('checked'); @@ -87,6 +88,7 @@ $(document).ready(() => {          $('#activate-on-startup').prop('checked', opts.activateOnStartup);          $('#enable-audio-playback').prop('checked', opts.enableAudioPlayback);          $('#enable-soft-katakana-search').prop('checked', opts.enableSoftKatakanaSearch); +        $('#group-term-results').prop('checked', opts.groupTermResults);          $('#show-advanced-options').prop('checked', opts.showAdvancedOptions);          $('#hold-shift-to-scan').prop('checked', opts.holdShiftToScan); @@ -313,6 +315,7 @@ function modelIdToMarkers(id) {      return {          'anki-term-model': [              'audio', +            'dictionary',              'expression',              'expression-furigana',              'glossary', @@ -324,6 +327,7 @@ function modelIdToMarkers(id) {          ],          'anki-kanji-model': [              'character', +            'dictionary',              'glossary',              'glossary-list',              'kunyomi', @@ -338,7 +342,6 @@ function populateAnkiDeckAndModel(opts) {      showAnkiSpinner(true);      const ankiFormat = $('#anki-format').hide(); -      return Promise.all([anki().getDeckNames(), anki().getModelNames()]).then(([deckNames, modelNames]) => {          const ankiDeck = $('.anki-deck');          ankiDeck.find('option').remove(); diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index f1fe0dac..2f0bd189 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -17,11 +17,25 @@   */ +function versionOptions(options) { +    const version = options.version || 0; +    const fixups = [ +        () => {} +    ]; + +    if (version < fixups.length) { +        fixups[version](); +        ++options.version; +        versionOptions(options); +    } +} +  function sanitizeOptions(options) {      const defaults = {          activateOnStartup: true,          enableAudioPlayback: true,          enableSoftKatakanaSearch: true, +        groupTermResults: true,          showAdvancedOptions: false,          selectMatchedText: true,          holdShiftToScan: true, diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js index f6ff8467..89719a24 100644 --- a/ext/bg/js/templates.js +++ b/ext/bg/js/templates.js @@ -31,112 +31,104 @@ templates['dictionary.html'] = template({"1":function(container,depth0,helpers,p      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.enableKanji : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "> Enable Kanji search</label>\n    </div>\n</div>\n";  },"useData":true}); -templates['footer.html'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { -    var helper; - -  return "    <script src=\"" -    + container.escapeExpression(((helper = (helper = helpers.root || (depth0 != null ? depth0.root : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"root","hash":{},"data":data}) : helper))) -    + "/js/frame.js\"></script>\n    </body>\n</html>\n"; -},"useData":true}); -templates['header.html'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { -    var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; +templates['kanji-list.html'] = template({"1":function(container,depth0,helpers,partials,data) { +    var stack1, helper, alias1=depth0 != null ? depth0 : {}; -  return "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"UTF-8\">\n        <title></title>\n        <style>\n            @font-face {\n                font-family: kanji-stroke-orders;\n                src:         url('" -    + alias4(((helper = (helper = helpers.root || (depth0 != null ? depth0.root : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"root","hash":{},"data":data}) : helper))) -    + "/ttf/kanji-stroke-orders.ttf');\n            }\n            @font-face {\n                font-family: vl-gothic-regular;\n                src:         url('" -    + alias4(((helper = (helper = helpers.root || (depth0 != null ? depth0.root : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"root","hash":{},"data":data}) : helper))) -    + "/ttf/vl-gothic-regular.ttf');\n            }\n        </style>\n        <link rel=\"stylesheet\" href=\"" -    + alias4(((helper = (helper = helpers.root || (depth0 != null ? depth0.root : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"root","hash":{},"data":data}) : helper))) -    + "/css/frame.css\">\n    </head>\n    <body>\n"; -},"useData":true}); -templates['kanji-link.html'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { +  return "<div class=\"kanji-definition\">\n    <div class=\"action-bar\">\n" +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "    </div>\n\n    <div class=\"kanji-glyph\">" +    + container.escapeExpression(((helper = (helper = helpers.character || (depth0 != null ? depth0.character : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"character","hash":{},"data":data}) : helper))) +    + "</div>\n\n    <div class=\"kanji-reading\">\n        <table>\n            <tr>\n                <th>Kunyomi:</th>\n                <td>\n" +    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.kunyomi : depth0),{"name":"each","hash":{},"fn":container.program(4, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "                </td>\n            </tr>\n            <tr>\n                <th>Onyomi:</th>\n                <td>\n" +    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(4, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "                </td>\n            </tr>\n        </table>\n    </div>\n\n    <div class=\"kanji-tags\">\n" +    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "    </div>\n\n    <div class=\"kanji-glossary\">\n" +    + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(9, data, 0),"inverse":container.program(13, data, 0),"data":data})) != null ? stack1 : "") +    + "    </div>\n</div>\n"; +},"2":function(container,depth0,helpers,partials,data) {      var helper; -  return "<a href=\"#\" class=\"kanji-link\">" -    + container.escapeExpression(((helper = (helper = helpers.kanji || (depth0 != null ? depth0.kanji : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"kanji","hash":{},"data":data}) : helper))) -    + "</a>\n"; -},"useData":true}); -templates['kanji-list.html'] = template({"1":function(container,depth0,helpers,partials,data,blockParams,depths) { -    var stack1; - -  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(2, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"2":function(container,depth0,helpers,partials,data,blockParams,depths) { -    var stack1; - -  return ((stack1 = container.invokePartial(partials["kanji.html"],depth0,{"name":"kanji.html","hash":{"sequence":(depths[1] != null ? depths[1].sequence : depths[1]),"options":(depths[1] != null ? depths[1].options : depths[1]),"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1])},"data":data,"indent":"    ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +  return "            <a href=\"#\" title=\"Add Kanji\" class=\"action-add-note pending disabled\" data-mode=\"kanji\" data-index=\"" +    + container.escapeExpression(((helper = (helper = helpers.index || (data && data.index)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"index","hash":{},"data":data}) : helper))) +    + "\"><img src=\"img/add_kanji.png\"></a>\n";  },"4":function(container,depth0,helpers,partials,data) { -    return "    <p>No results found</p>\n"; -},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) { -    var stack1; - -  return ((stack1 = container.invokePartial(partials["header.html"],depth0,{"name":"header.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") -    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.program(4, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") -    + ((stack1 = container.invokePartial(partials["footer.html"],depth0,{"name":"footer.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"usePartial":true,"useData":true,"useDepths":true}); -templates['kanji.html'] = template({"1":function(container,depth0,helpers,partials,data) { -    var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; - -  return "        <a href=\"#\" title=\"Add Kanji\" class=\"action-add-note disabled\" data-mode=\"kanji\" data-index=\"" -    + alias4(((helper = (helper = helpers.index || (data && data.index)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"index","hash":{},"data":data}) : helper))) -    + "\"><img src=\"" -    + alias4(((helper = (helper = helpers.root || (depth0 != null ? depth0.root : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"root","hash":{},"data":data}) : helper))) -    + "/img/add_kanji.png\"></a>\n"; -},"3":function(container,depth0,helpers,partials,data) {      var stack1; -  return "                    " +  return "                        "      + container.escapeExpression(container.lambda(depth0, depth0)) -    + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : {},(data && data.last),{"name":"unless","hash":{},"fn":container.program(4, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : {},(data && data.last),{"name":"unless","hash":{},"fn":container.program(5, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n"; -},"4":function(container,depth0,helpers,partials,data) { +},"5":function(container,depth0,helpers,partials,data) {      return ", "; -},"6":function(container,depth0,helpers,partials,data) { +},"7":function(container,depth0,helpers,partials,data) {      var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; -  return "        <span class=\"tag tag-" +  return "            <span class=\"tag tag-"      + alias4(((helper = (helper = helpers.category || (depth0 != null ? depth0.category : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"category","hash":{},"data":data}) : helper)))      + "\" title=\""      + alias4(((helper = (helper = helpers.notes || (depth0 != null ? depth0.notes : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"notes","hash":{},"data":data}) : helper)))      + "\">"      + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))      + "</span>\n"; -},"8":function(container,depth0,helpers,partials,data) { +},"9":function(container,depth0,helpers,partials,data) {      var stack1; -  return "        <ol>\n" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(9, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "        </ol>\n"; -},"9":function(container,depth0,helpers,partials,data) { -    return "            <li><span>" -    + container.escapeExpression(container.lambda(depth0, depth0)) -    + "</span></li>\n"; +  return "            <ol \"kanji-glossary-group\">\n" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "            </ol>\n"; +},"10":function(container,depth0,helpers,partials,data) { +    var stack1, helper, options, buffer =  +  "                    <li><span class=\"kanji-glossary-item\">"; +  stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(11, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},options) : helper)); +  if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} +  if (stack1 != null) { buffer += stack1; } +  return buffer + "</span></li>\n";  },"11":function(container,depth0,helpers,partials,data) { +    return container.escapeExpression(container.lambda(depth0, depth0)); +},"13":function(container,depth0,helpers,partials,data) { +    var stack1, helper, options, buffer =  +  "            <div class=\"kanji-glossary-group kanji-glossary-item\">"; +  stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(14, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},options) : helper)); +  if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} +  if (stack1 != null) { buffer += stack1; } +  return buffer + "</div>\n"; +},"14":function(container,depth0,helpers,partials,data) {      var stack1; -  return "        <p>\n            " -    + container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0)) -    + "\n        </p>\n"; -},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { -    var stack1, helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; +  return container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0)); +},"16":function(container,depth0,helpers,partials,data,blockParams,depths) { +    var stack1; -  return "<div class=\"kanji-definition\">\n    <div class=\"action-bar\" data-sequence=\"" -    + alias4(((helper = (helper = helpers.sequence || (depth0 != null ? depth0.sequence : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"sequence","hash":{},"data":data}) : helper))) -    + "\">\n" -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "    </div>\n\n    <div class=\"kanji-glyph\">" -    + alias4(((helper = (helper = helpers.character || (depth0 != null ? depth0.character : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"character","hash":{},"data":data}) : helper))) -    + "</div>\n\n    <div class=\"kanji-reading\">\n        <table>\n            <tr>\n                <th>Kunyomi:</th>\n                <td>\n" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.kunyomi : depth0),{"name":"each","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "                </td>\n            </tr>\n            <tr>\n                <th>Onyomi:</th>\n                <td>\n" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "                </td>\n            </tr>\n        </table>\n    </div>\n\n    <div class=\"kanji-tags\">\n" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "    </div>\n\n    <div class=\"kanji-glossary\">\n" -    + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(8, data, 0),"inverse":container.program(11, data, 0),"data":data})) != null ? stack1 : "") -    + "    </div>\n</div>\n"; -},"useData":true}); +  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(17, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"17":function(container,depth0,helpers,partials,data,blockParams,depths) { +    var stack1; + +  return "        " +    + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : {},(data && data.first),{"name":"unless","hash":{},"fn":container.program(18, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "\n" +    + ((stack1 = container.invokePartial(partials.kanji,depth0,{"name":"kanji","hash":{"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1])},"data":data,"indent":"        ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"18":function(container,depth0,helpers,partials,data) { +    return "<hr>"; +},"20":function(container,depth0,helpers,partials,data) { +    return "    <p>No results found</p>\n"; +},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) { +    var stack1; + +  return "\n" +    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(16, data, 0, blockParams, depths),"inverse":container.program(20, data, 0, blockParams, depths),"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":["kanji"],"data":data}) || fn; +  return fn; +  } + +,"useDecorators":true,"usePartial":true,"useData":true,"useDepths":true});  templates['model.html'] = template({"1":function(container,depth0,helpers,partials,data) { -    return "                    <li><a class=\"marker-link\" href=\"#\">" +    return "                        <li><a class=\"marker-link\" href=\"#\">"      + container.escapeExpression(container.lambda(depth0, depth0))      + "</a></li>\n";  },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { @@ -152,115 +144,168 @@ templates['model.html'] = template({"1":function(container,depth0,helpers,partia      + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.markers : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "                </ul>\n            </div>\n        </div>\n    </td>\n</tr>\n";  },"useData":true}); -templates['term-list.html'] = template({"1":function(container,depth0,helpers,partials,data,blockParams,depths) { +templates['term-list.html'] = template({"1":function(container,depth0,helpers,partials,data) { +    var stack1, alias1=depth0 != null ? depth0 : {}; + +  return ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.program(9, data, 0),"data":data})) != null ? stack1 : ""); +},"2":function(container,depth0,helpers,partials,data) {      var stack1; -  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(2, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"2":function(container,depth0,helpers,partials,data,blockParams,depths) { +  return "        <div class=\"term-tags\">\n" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "        </div>\n"; +},"3":function(container,depth0,helpers,partials,data) { +    var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; + +  return "                <span class=\"tag tag-" +    + alias4(((helper = (helper = helpers.category || (depth0 != null ? depth0.category : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"category","hash":{},"data":data}) : helper))) +    + "\" title=\"" +    + alias4(((helper = (helper = helpers.notes || (depth0 != null ? depth0.notes : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"notes","hash":{},"data":data}) : helper))) +    + "\">" +    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper))) +    + "</span>\n"; +},"5":function(container,depth0,helpers,partials,data) {      var stack1; -  return ((stack1 = container.invokePartial(partials["term.html"],depth0,{"name":"term.html","hash":{"sequence":(depths[1] != null ? depths[1].sequence : depths[1]),"options":(depths[1] != null ? depths[1].options : depths[1]),"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1])},"data":data,"indent":"    ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"4":function(container,depth0,helpers,partials,data) { -    return "    <p>No results found</p>\n"; -},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) { +  return "        <ul class=\"term-glossary-group\">\n" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "        </ul>\n"; +},"6":function(container,depth0,helpers,partials,data) { +    var stack1, helper, options, buffer =  +  "            <li><span class=\"term-glossary-item\">"; +  stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},options) : helper)); +  if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} +  if (stack1 != null) { buffer += stack1; } +  return buffer + "</span></li>\n"; +},"7":function(container,depth0,helpers,partials,data) { +    return container.escapeExpression(container.lambda(depth0, depth0)); +},"9":function(container,depth0,helpers,partials,data) { +    var stack1, helper, options, buffer =  +  "    <div class=\"term-glossary-group term-glossary-item\">"; +  stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},options) : helper)); +  if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} +  if (stack1 != null) { buffer += stack1; } +  return buffer + "</div>\n"; +},"10":function(container,depth0,helpers,partials,data) {      var stack1; -  return ((stack1 = container.invokePartial(partials["header.html"],depth0,{"name":"header.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") -    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.program(4, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") -    + ((stack1 = container.invokePartial(partials["footer.html"],depth0,{"name":"footer.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"usePartial":true,"useData":true,"useDepths":true}); -templates['term.html'] = template({"1":function(container,depth0,helpers,partials,data) { -    var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; +  return container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0)); +},"12":function(container,depth0,helpers,partials,data) { +    var stack1, alias1=depth0 != null ? depth0 : {}; -  return "        <a href=\"#\" title=\"Play audio\" class=\"action-play-audio\" data-index=\"" -    + alias4(((helper = (helper = helpers.index || (data && data.index)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"index","hash":{},"data":data}) : helper))) -    + "\"><img src=\"" -    + alias4(((helper = (helper = helpers.root || (depth0 != null ? depth0.root : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"root","hash":{},"data":data}) : helper))) -    + "/img/play_audio.png\"></a>\n"; -},"3":function(container,depth0,helpers,partials,data) { +  return "<div class=\"term-definition\">\n    <div class=\"action-bar\">\n" +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "    </div>\n\n" +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reading : depth0),{"name":"if","hash":{},"fn":container.program(17, data, 0),"inverse":container.program(20, data, 0),"data":data})) != null ? stack1 : "") +    + "\n" +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(22, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "\n    <div class=\"term-glossary\">\n" +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(26, data, 0),"inverse":container.program(32, data, 0),"data":data})) != null ? stack1 : "") +    + "    </div>\n</div>\n"; +},"13":function(container,depth0,helpers,partials,data) {      var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; -  return "        <a href=\"#\" title=\"Add term as expression\" class=\"action-add-note disabled\" data-mode=\"term_kanji\" data-index=\"" +  return "            <a href=\"#\" title=\"Add term as expression\" class=\"action-add-note pending disabled\" data-mode=\"term_kanji\" data-index=\""      + alias4(((helper = (helper = helpers.index || (data && data.index)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"index","hash":{},"data":data}) : helper))) -    + "\"><img src=\"" -    + alias4(((helper = (helper = helpers.root || (depth0 != null ? depth0.root : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"root","hash":{},"data":data}) : helper))) -    + "/img/add_term_kanji.png\"></a>\n        <a href=\"#\" title=\"Add term as reading\" class=\"action-add-note disabled\" data-mode=\"term_kana\" data-index=\"" +    + "\"><img src=\"img/add_term_kanji.png\"></a>\n            <a href=\"#\" title=\"Add term as reading\" class=\"action-add-note pending disabled\" data-mode=\"term_kana\" data-index=\""      + alias4(((helper = (helper = helpers.index || (data && data.index)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"index","hash":{},"data":data}) : helper))) -    + "\"><img src=\"" -    + alias4(((helper = (helper = helpers.root || (depth0 != null ? depth0.root : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"root","hash":{},"data":data}) : helper))) -    + "/img/add_term_kana.png\"></a>\n"; -},"5":function(container,depth0,helpers,partials,data) { +    + "\"><img src=\"img/add_term_kana.png\"></a>\n"; +},"15":function(container,depth0,helpers,partials,data) { +    var helper; + +  return "            <a href=\"#\" title=\"Play audio\" class=\"action-play-audio\" data-index=\"" +    + container.escapeExpression(((helper = (helper = helpers.index || (data && data.index)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"index","hash":{},"data":data}) : helper))) +    + "\"><img src=\"img/play_audio.png\"></a>\n"; +},"17":function(container,depth0,helpers,partials,data) {      var stack1, helper, options, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", buffer =  -  "    <div class=\"term-expression\"><ruby>"; -  stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper)); +  "        <div class=\"term-expression\"><ruby>"; +  stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(18, data, 0),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper));    if (!helpers.kanjiLinks) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}    if (stack1 != null) { buffer += stack1; }    return buffer + "<rt>"      + container.escapeExpression(((helper = (helper = helpers.reading || (depth0 != null ? depth0.reading : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"reading","hash":{},"data":data}) : helper)))      + "</rt></ruby></div>\n"; -},"6":function(container,depth0,helpers,partials,data) { +},"18":function(container,depth0,helpers,partials,data) {      var helper;    return container.escapeExpression(((helper = (helper = helpers.expression || (depth0 != null ? depth0.expression : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"expression","hash":{},"data":data}) : helper))); -},"8":function(container,depth0,helpers,partials,data) { +},"20":function(container,depth0,helpers,partials,data) {      var stack1, helper, options, buffer =  -  "    <div class=\"term-expression\">"; -  stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},options) : helper)); +  "        <div class=\"term-expression\">"; +  stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(18, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},options) : helper));    if (!helpers.kanjiLinks) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}    if (stack1 != null) { buffer += stack1; }    return buffer + "</div>\n"; -},"10":function(container,depth0,helpers,partials,data) { +},"22":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return "        <div class=\"term-reasons\">\n" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(23, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "        </div>\n"; +},"23":function(container,depth0,helpers,partials,data) {      var stack1; -  return "        <span class=\"reasons\">" +  return "                <span class=\"reasons\">"      + container.escapeExpression(container.lambda(depth0, depth0))      + "</span> " -    + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : {},(data && data.last),{"name":"unless","hash":{},"fn":container.program(11, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : {},(data && data.last),{"name":"unless","hash":{},"fn":container.program(24, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "\n"; -},"11":function(container,depth0,helpers,partials,data) { +},"24":function(container,depth0,helpers,partials,data) {      return "«"; -},"13":function(container,depth0,helpers,partials,data) { -    var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; +},"26":function(container,depth0,helpers,partials,data) { +    var stack1; -  return "        <span class=\"tag tag-" -    + alias4(((helper = (helper = helpers.category || (depth0 != null ? depth0.category : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"category","hash":{},"data":data}) : helper))) -    + "\" title=\"" -    + alias4(((helper = (helper = helpers.notes || (depth0 != null ? depth0.notes : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"notes","hash":{},"data":data}) : helper))) -    + "\">" -    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper))) -    + "</span>\n"; -},"15":function(container,depth0,helpers,partials,data) { +  return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : {},((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(27, data, 0),"inverse":container.program(30, data, 0),"data":data})) != null ? stack1 : ""); +},"27":function(container,depth0,helpers,partials,data) {      var stack1;    return "        <ol>\n" -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(28, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "        </ol>\n"; -},"16":function(container,depth0,helpers,partials,data) { -    return "            <li><span>" -    + container.escapeExpression(container.lambda(depth0, depth0)) -    + "</span></li>\n"; -},"18":function(container,depth0,helpers,partials,data) { +},"28":function(container,depth0,helpers,partials,data) {      var stack1; -  return "        <p>" -    + container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0)) -    + "</p>\n"; -},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { -    var stack1, helper, alias1=depth0 != null ? depth0 : {}; +  return "                <li>" +    + ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") +    + "</li>\n"; +},"30":function(container,depth0,helpers,partials,data) { +    var stack1; -  return "<div class=\"term-definition\">\n    <div class=\"action-bar\" data-sequence=\"" -    + container.escapeExpression(((helper = (helper = helpers.sequence || (depth0 != null ? depth0.sequence : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"sequence","hash":{},"data":data}) : helper))) -    + "\">\n" -    + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.options : depth0)) != null ? stack1.enableAudioPlayback : stack1),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "    </div>\n\n" -    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reading : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.program(8, data, 0),"data":data})) != null ? stack1 : "") -    + "\n    <div class=\"term-reasons\">\n" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "    </div>\n\n    <div class=\"term-tags\">\n" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "    </div>\n\n    <div class=\"term-glossary\">\n" -    + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.program(18, data, 0),"data":data})) != null ? stack1 : "") -    + "    </div>\n</div>\n"; -},"useData":true}); +  return ((stack1 = container.invokePartial(partials.definition,((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["0"] : stack1),{"name":"definition","data":data,"indent":"            ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"32":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","data":data,"indent":"        ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"34":function(container,depth0,helpers,partials,data,blockParams,depths) { +    var stack1; + +  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(35, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"35":function(container,depth0,helpers,partials,data,blockParams,depths) { +    var stack1; + +  return "        " +    + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : {},(data && data.first),{"name":"unless","hash":{},"fn":container.program(36, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "\n" +    + ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"playback":(depths[1] != null ? depths[1].playback : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"grouped":(depths[1] != null ? depths[1].grouped : depths[1])},"data":data,"indent":"        ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"36":function(container,depth0,helpers,partials,data) { +    return "<hr>"; +},"38":function(container,depth0,helpers,partials,data) { +    return "    <p>No results found</p>\n"; +},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) { +    var stack1; + +  return "\n\n" +    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(34, data, 0, blockParams, depths),"inverse":container.program(38, data, 0, blockParams, depths),"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":["definition"],"data":data}) || fn; +  fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(12, data, 0, blockParams, depths),"inverse":container.noop,"args":["term"],"data":data}) || fn; +  return fn; +  } + +,"useDecorators":true,"usePartial":true,"useData":true,"useDepths":true});  })();
\ No newline at end of file diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 2b6b87c1..8710f568 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -54,6 +54,8 @@ class Translator {              let definitions = [];              for (const deinflection of deinflections) {                  for (const definition of deinflection.definitions) { +                    const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta)); +                    tags.push(buildDictTag(definition.dictionary));                      definitions.push({                          source: deinflection.source,                          reasons: deinflection.reasons, @@ -63,7 +65,7 @@ class Translator {                          expression: definition.expression,                          reading: definition.reading,                          glossary: definition.glossary, -                        tags: sortTags(definition.tags.map(tag => buildTag(tag, definition.tagMeta))) +                        tags: sortTags(tags)                      });                  }              } @@ -80,6 +82,12 @@ class Translator {          });      } +    findTermGrouped(text, dictionaries, enableSoftKatakanaSearch) { +        return this.findTerm(text, dictionaries, enableSoftKatakanaSearch).then(({length, definitions}) => { +            return {length, definitions: groupTermDefs(definitions)}; +        }); +    } +      findKanji(text, dictionaries) {          const processed = {}, promises = [];          for (const c of text) { @@ -89,7 +97,16 @@ class Translator {              }          } -        return Promise.all(promises).then(sets => this.processKanji(sets.reduce((a, b) => a.concat(b), []))); +        return Promise.all(promises).then(defSets => { +            const definitions = defSets.reduce((a, b) => a.concat(b), []); +            for (const definition of definitions) { +                const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta)); +                tags.push(buildDictTag(definition.dictionary)); +                definition.tags = sortTags(tags); +            } + +            return definitions; +        });      }      findTermDeinflections(text, dictionaries, cache) { diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 059f3160..504deeda 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -21,7 +21,7 @@ function kanjiLinks(options) {      let result = '';      for (const c of options.fn(this)) {          if (isKanji(c)) { -            result += Handlebars.templates['kanji-link.html']({kanji: c}).trim(); +            result += `<a href="#" class="kanji-link">${c}</a>`;          } else {              result += c;          } @@ -30,6 +30,10 @@ function kanjiLinks(options) {      return result;  } +function multiLine(options) { +    return options.fn(this).split('\n').join('<br>'); +} +  function isKanji(c) {      const code = c.charCodeAt(0);      return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0; @@ -44,28 +48,6 @@ function promiseCallback(promise, callback) {      });  } -function sortTags(tags) { -    return tags.sort((v1, v2) => { -        const order1 = v1.order; -        const order2 = v2.order; -        if (order1 < order2) { -            return -1; -        } else if (order1 > order2) { -            return 1; -        } - -        const name1 = v1.name; -        const name2 = v2.name; -        if (name1 < name2) { -            return -1; -        } else if (name1 > name2) { -            return 1; -        } - -        return 0; -    }); -} -  function sortTermDefs(definitions) {      return definitions.sort((v1, v2) => {          const sl1 = v1.source.length; @@ -113,6 +95,43 @@ function undupeTermDefs(definitions) {      return definitionsUnique;  } +function groupTermDefs(definitions) { +    const groups = {}; +    for (const definition of definitions) { +        const key = [definition.source, definition.expression].concat(definition.reasons); +        if (definition.reading) { +            key.push(definition.reading); +        } + +        const group = groups[key]; +        if (group) { +            group.push(definition); +        } else { +            groups[key] = [definition]; +        } +    } + +    const results = []; +    for (const key in groups) { +        const groupDefs = groups[key]; +        const firstDef = groupDefs[0]; +        results.push({ +            definitions: groupDefs, +            expression: firstDef.expression, +            reading: firstDef.reading, +            reasons: firstDef.reasons, +            score: groupDefs.reduce((x, y) => x > y ? x : y, Number.MIN_SAFE_INTEGER), +            source: firstDef.source, +        }); +    } + +    return sortTermDefs(results); +} + +function buildDictTag(name) { +    return sanitizeTag({name, category: 'dictionary', order: 100}); +} +  function buildTag(name, meta) {      const tag = {name};      const symbol = name.split(':')[0]; @@ -135,6 +154,95 @@ function splitField(field) {      return field.length === 0 ? [] : field.split(' ');  } +function sortTags(tags) { +    return tags.sort((v1, v2) => { +        const order1 = v1.order; +        const order2 = v2.order; +        if (order1 < order2) { +            return -1; +        } else if (order1 > order2) { +            return 1; +        } + +        const name1 = v1.name; +        const name2 = v2.name; +        if (name1 < name2) { +            return -1; +        } else if (name1 > name2) { +            return 1; +        } + +        return 0; +    }); +} + +function formatField(field, definition, mode) { +    const markers = [ +        'audio', +        'character', +        'dictionary', +        'expression', +        'expression-furigana', +        'glossary', +        'glossary-list', +        'kunyomi', +        'onyomi', +        'reading', +        'sentence', +        'tags', +        'url', +    ]; + +    for (const marker of markers) { +        let value = definition[marker] || null; +        switch (marker) { +            case 'expression': +                if (mode === 'term_kana' && definition.reading) { +                    value = definition.reading; +                } +                break; +            case 'expression-furigana': +                if (mode === 'term_kana' && definition.reading) { +                    value = definition.reading; +                } else { +                    value = `<ruby>${definition.expression}<rt>${definition.reading}</rt></ruby>`; +                } +                break; +            case 'reading': +                if (mode === 'term_kana') { +                    value = null; +                } +                break; +            case 'glossary-list': +                if (definition.glossary) { +                    if (definition.glossary.length > 1) { +                        value = '<ol style="white-space: pre; text-align: left; overflow-x: auto;">'; +                        for (const gloss of definition.glossary) { +                            value += `<li>${gloss}</li>`; +                        } +                        value += '</ol>'; +                    } else { +                        value = `<p style="white-space: pre; overflow-x: auto;">${definition.glossary.join('')}</p>`; +                    } +                } +                break; +            case 'tags': +                if (definition.tags) { +                    value = definition.tags.map(t => t.name); +                } +                break; +        } + +        if (value !== null && typeof(value) !== 'string') { +            value = value.join(', '); +        } + +        field = field.replace(`{${marker}}`, value || ''); +    } + +    return field; +} +  function loadJson(url) {      return new Promise((resolve, reject) => {          const xhr = new XMLHttpRequest(); diff --git a/ext/bg/js/yomichan.js b/ext/bg/js/yomichan.js index 12dd89ac..2cdcf1c8 100644 --- a/ext/bg/js/yomichan.js +++ b/ext/bg/js/yomichan.js @@ -21,6 +21,7 @@ class Yomichan {      constructor() {          Handlebars.partials = Handlebars.templates;          Handlebars.registerHelper('kanjiLinks', kanjiLinks); +        Handlebars.registerHelper('multiLine', multiLine);          this.translator = new Translator();          this.anki = new AnkiNull(); @@ -117,75 +118,6 @@ class Yomichan {          chrome.tabs.sendMessage(tabId, {action, params}, () => null);      } -    formatField(field, definition, mode) { -        const markers = [ -            'audio', -            'character', -            'expression', -            'expression-furigana', -            'glossary', -            'glossary-list', -            'kunyomi', -            'onyomi', -            'reading', -            'sentence', -            'tags', -            'url', -        ]; - -        for (const marker of markers) { -            let value = definition[marker] || null; -            switch (marker) { -                case 'audio': -                    value = ''; -                    break; -                case 'expression': -                    if (mode === 'term_kana' && definition.reading) { -                        value = definition.reading; -                    } -                    break; -                case 'expression-furigana': -                    if (mode === 'term_kana' && definition.reading) { -                        value = definition.reading; -                    } else { -                        value = `<ruby>${definition.expression}<rt>${definition.reading}</rt></ruby>`; -                    } -                    break; -                case 'reading': -                    if (mode === 'term_kana') { -                        value = null; -                    } -                    break; -                case 'glossary-list': -                    if (definition.glossary) { -                        if (definition.glossary.length > 1) { -                            value = '<ol style="white-space: pre; text-align: left; overflow-x: auto;">'; -                            for (const gloss of definition.glossary) { -                                value += `<li>${gloss}</li>`; -                            } -                            value += '</ol>'; -                        } else { -                            value = `<p style="white-space: pre; overflow-x: auto;">${definition.glossary.join('')}</p>`; -                        } -                    } -                    break; -                case 'tags': -                    if (definition.tags) { -                        value = definition.tags.map(t => t.name); -                    } -                    break; -            } - -            if (value !== null && typeof(value) !== 'string') { -                value = value.join(', '); -            } - -            field = field.replace(`{${marker}}`, value || ''); -        } - -        return field; -    } -      formatNote(definition, mode) {          const note = {fields: {}, tags: this.options.ankiCardTags}; @@ -217,7 +149,7 @@ class Yomichan {          }          for (const name in fields) { -            note.fields[name] = this.formatField(fields[name], definition, mode); +            note.fields[name] = formatField(fields[name], definition, mode);          }          return note; @@ -254,7 +186,29 @@ class Yomichan {          }          promiseCallback( -            this.translator.findTerm(text, dictionaries, this.options.enableSoftKatakanaSearch), +            this.translator.findTerm( +                text, +                dictionaries, +                this.options.enableSoftKatakanaSearch +            ), +            callback +        ); +    } + +    api_findTermGrouped({text, callback}) { +        const dictionaries = []; +        for (const title in this.options.dictionaries) { +            if (this.options.dictionaries[title].enableTerms) { +                dictionaries.push(title); +            } +        } + +        promiseCallback( +            this.translator.findTermGrouped( +                text, +                dictionaries, +                this.options.enableSoftKatakanaSearch +            ),              callback          );      } diff --git a/ext/bg/options.html b/ext/bg/options.html index 0ef65abb..6bf6fb7b 100644 --- a/ext/bg/options.html +++ b/ext/bg/options.html @@ -38,6 +38,10 @@                  </div>                  <div class="checkbox"> +                    <label><input type="checkbox" id="group-term-results"> Group term results</label> +                </div> + +                <div class="checkbox">                      <label><input type="checkbox" id="show-advanced-options"> Show advanced options</label>                  </div>              </div> diff --git a/ext/fg/css/frame.css b/ext/fg/css/frame.css index 67caf152..7e9c3b19 100644 --- a/ext/fg/css/frame.css +++ b/ext/fg/css/frame.css @@ -18,6 +18,15 @@  /* common styles */ +@font-face { +    font-family: kanji-stroke-orders; +    src:         url('../ttf/kanji-stroke-orders.ttf'); +} +@font-face { +    font-family: vl-gothic-regular; +    src:         url('../ttf/vl-gothic-regular.ttf'); +} +  body {      background-color: #fff;      color:            #333; @@ -66,6 +75,10 @@ body {      background-color: #d9534f;  } +.tag-dictionary { +    background-color: #aa66cc; +} +  .action-bar {      float: right;  } @@ -86,6 +99,26 @@ body {      opacity:        0.25;  } +.action-bar .pending { +    visibility: hidden; +} + +.spinner { +    bottom:   5px; +    display:  none; +    position: fixed; +    right:    5px; +} + +hr { +  background-color: #fff; +  border:           none; +  border-top:       1px dotted #ccc; +  margin-top:       0.8em; +  margin-bottom:    0.8em; +  height:           1px; +} +  /* term styles */  .term-expression { @@ -105,11 +138,7 @@ body {      display: inline-block;  } -.term-glossary { -    font-family: vl-gothic-regular; -} - -.term-glossary ol { +.term-glossary ol, .term-glossary ul {      padding-left: 1.4em;  } @@ -117,15 +146,14 @@ body {      color: #777;  } -.term-glossary li span { -    color:       #000; -    overflow-x:  auto; -    white-space: pre; +.term-glossary-group { +    margin-top:    0.4em; +    margin-bottom: 0.4em;  } -.term-glossary p { -    overflow-x:  auto; -    white-space: pre; +.term-glossary-item { +    color:       #000; +    font-family: vl-gothic-regular;  }  /* kanji styles */ @@ -153,10 +181,6 @@ body {      font-family: vl-gothic-regular;  } -.kanji-glossary { -    font-family: vl-gothic-regular; -} -  .kanji-glossary ol {      padding-left: 1.4em;  } @@ -165,13 +189,12 @@ body {      color: #777;  } -.kanji-glossary li span { -    color:       #000; -    overflow-x:  auto; -    white-space: pre; +.kanji-glossary-group { +    padding-bottom: 0.7em; +    padding-top:    0.7em;  } -.kanji-glossary p { -    overflow-x:  auto; -    white-space: pre; +.kanji-glossary-item { +    color:       #000; +    font-family: vl-gothic-regular;  } diff --git a/ext/fg/frame.html b/ext/fg/frame.html new file mode 100644 index 00000000..8246787b --- /dev/null +++ b/ext/fg/frame.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en"> +    <head> +        <meta charset="UTF-8"> +        <title></title> +        <link rel="stylesheet" href="css/frame.css"> +    </head> +    <body> +        <div class="spinner"> +            <img src="img/spinner.gif"> +        </div> + +        <div class="content"></div> + +        <script src="../lib/jquery-2.2.2.min.js"></script> +        <script src="js/util.js"></script> +        <script src="js/frame.js"></script> +    </body> +</html> diff --git a/ext/fg/img/spinner.gif b/ext/fg/img/spinner.gifBinary files differ new file mode 100644 index 00000000..8ed30cb6 --- /dev/null +++ b/ext/fg/img/spinner.gif diff --git a/ext/fg/js/driver.js b/ext/fg/js/driver.js index 9aab7950..ef7db481 100644 --- a/ext/fg/js/driver.js +++ b/ext/fg/js/driver.js @@ -21,27 +21,22 @@ class Driver {      constructor() {          this.popup = new Popup();          this.popupTimer = null; -        this.audio = {};          this.lastMousePos = null;          this.lastTextSource = null;          this.pendingLookup = false;          this.enabled = false;          this.options = null; -        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('mouseover', this.onMouseOver.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('resize', e => this.hidePopup()); +        window.addEventListener('resize', e => this.searchClear());          getOptions().then(opts => {              this.options = opts; -            return getEnabled(); +            return isEnabled();          }).then(enabled => {              this.enabled = enabled;          }); @@ -65,7 +60,7 @@ class Driver {          if (this.enabled && this.lastMousePos !== null && e.keyCode === 16 /* shift */) {              this.searchAt(this.lastMousePos, true);          } else if (e.keyCode === 27 /* esc */) { -            this.hidePopup(); +            this.searchClear();          }      } @@ -92,7 +87,7 @@ class Driver {          }          const searcher = () => this.searchAt(this.lastMousePos, false); -        if (!this.popup.visible() || e.shiftKey || e.which === 2 /* mmb */) { +        if (!this.popup.isVisible() || e.shiftKey || e.which === 2 /* mmb */) {              searcher();          } else {              this.popupTimerSet(searcher); @@ -102,7 +97,7 @@ class Driver {      onMouseDown(e) {          this.lastMousePos = {x: e.clientX, y: e.clientY};          this.popupTimerClear(); -        this.hidePopup(); +        this.searchClear();      }      onBgMessage({action, params}, sender, callback) { @@ -114,20 +109,46 @@ class Driver {          callback();      } -    onFrameMessage(e) { -        const {action, params} = e.data, method = this['api_' + action]; -        if (typeof(method) === 'function') { -            method.call(this, params); +    searchAt(point, hideNotFound) { +        if (this.pendingLookup) { +            return; +        } + +        const textSource = textSourceFromPoint(point); +        if (textSource === null || !textSource.containsPoint(point)) { +            if (hideNotFound) { +                this.searchClear(); +            } + +            return; +        } + +        if (this.lastTextSource !== null && this.lastTextSource.equals(textSource)) { +            return;          } + +        this.pendingLookup = true; +        this.searchTerms(textSource).then(found => { +            if (!found) { +                this.searchKanji(textSource).then(found => { +                    if (!found && hideNotFound) { +                        this.searchClear(); +                    } +                }); +            } +        }).catch(error => { +            window.alert('Error: ' + error); +        }).then(() => { +            this.pendingLookup = false; +        });      }      searchTerms(textSource) {          textSource.setEndOffset(this.options.scanLength); -        this.pendingLookup = true; -        return findTerm(textSource.text()).then(({definitions, length}) => { +        const findFunc = this.options.groupTermResults ? findTermGrouped : findTerm; +        return findFunc(textSource.text()).then(({definitions, length}) => {              if (definitions.length === 0) { -                this.pendingLookup = false;                  return false;              } else {                  textSource.setEndOffset(length); @@ -138,119 +159,46 @@ class Driver {                      definition.sentence = sentence;                  }); -                const sequence = ++this.sequence; -                const context = { -                    definitions, -                    sequence, -                    addable: this.options.ankiMethod !== 'disabled', -                    root: this.fgRoot, -                    options: this.options -                }; - -                return renderText(context, '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} -                        )); -                    } +                this.popup.showNextTo(textSource.getRect()); +                this.popup.showTermDefs(definitions, this.options); +                this.lastTextSource = textSource; +                if (this.options.selectMatchedText) { +                    textSource.select(); +                } -                    return true; -                }); +                return true;              }          }).catch(error => { -            alert('Error: ' + error); +            window.alert('Error: ' + error); +            return false;          });      }      searchKanji(textSource) {          textSource.setEndOffset(1); -        this.pendingLookup = true;          return findKanji(textSource.text()).then(definitions => {              if (definitions.length === 0) { -                this.pendingLookup = false;                  return false;              } else {                  definitions.forEach(definition => definition.url = window.location.href); -                const sequence = ++this.sequence; -                const context = { -                    definitions, -                    sequence, -                    addable: this.options.ankiMethod !== 'disabled', -                    root: this.fgRoot, -                    options: this.options -                }; - -                return renderText(context, '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} -                        )); -                    } - -                    return true; -                }); -            } -        }).catch(error => { -            alert('Error: ' + error); -        }); -    } - -    searchAt(point, hideNotFound) { -        if (this.pendingLookup) { -            return; -        } - -        const textSource = textSourceFromPoint(point); -        if (textSource === null || !textSource.containsPoint(point)) { -            if (hideNotFound) { -                this.hidePopup(); -            } - -            return; -        } - -        if (this.lastTextSource !== null && this.lastTextSource.equals(textSource)) { -            return true; -        } +                this.popup.showNextTo(textSource.getRect()); +                this.popup.showKanjiDefs(definitions, this.options); +                this.lastTextSource = textSource; +                if (this.options.selectMatchedText) { +                    textSource.select(); +                } -        this.searchTerms(textSource).then(found => { -            if (!found) { -                this.searchKanji(textSource).then(found => { -                    if (!found && hideNotFound) { -                        this.hidePopup(); -                    } -                }); +                return true;              }          }).catch(error => { -            alert('Error: ' + error); +            window.alert('Error: ' + error); +            return false;          });      } -    showPopup(textSource, content) { -        this.popup.showNextTo(textSource.getRect(), content); - -        if (this.options.selectMatchedText) { -            textSource.select(); -        } - -        this.lastTextSource = textSource; -    } - -    hidePopup() { +    searchClear() {          this.popup.hide();          if (this.options.selectMatchedText && this.lastTextSource !== null) { @@ -258,7 +206,6 @@ class Driver {          }          this.lastTextSource = null; -        this.definitions = null;      }      api_setOptions(opts) { @@ -267,68 +214,9 @@ class Driver {      api_setEnabled(enabled) {          if (!(this.enabled = enabled)) { -            this.hidePopup(); +            this.searchClear();          }      } - -    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'); -            } -        }).catch(error => { -            alert('Error: ' + error); -        }); -    } - -    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; -            const context = { -                definitions, -                sequence, -                addable: this.options.ankiMethod !== 'disabled', -                root: this.fgRoot, -                options: this.options -            }; - -            return renderText(context, '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})); -                } -            }); -        }).catch(error => { -            alert('Error: ' + error); -        }); -    }  }  window.driver = new Driver(); diff --git a/ext/fg/js/frame.js b/ext/fg/js/frame.js index 8a99a405..4295dbb3 100644 --- a/ext/fg/js/frame.js +++ b/ext/fg/js/frame.js @@ -16,65 +16,145 @@   * along with this program.  If not, see <http://www.gnu.org/licenses/>.   */ +class Frame { +    constructor() { +        this.definitions = []; +        this.audioCache = {}; +        this.sequence = 0; -function invokeApi(action, params, target) { -    target.postMessage({action, params}, '*'); -} +        $(window).on('message', e => { +            const {action, params} = e.originalEvent.data, method = this['api_' + action]; +            if (typeof(method) === 'function') { +                method.call(this, params); +            } +        }); +    } + +    api_showTermDefs({definitions, options}) { +        const sequence = ++this.sequence; +        const context = { +            definitions, +            grouped: options.groupTermResults, +            addable: options.ankiMethod !== 'disabled', +            playback: options.enableAudioPlayback +        }; + +        this.definitions = definitions; +        this.showSpinner(false); +        window.scrollTo(0, 0); + +        renderText(context, 'term-list.html').then(content => { +            $('.content').html(content); +            $('.action-add-note').click(this.onAddNote.bind(this)); + +            $('.kanji-link').click(e => { +                e.preventDefault(); +                findKanji($(e.target).text()).then(kdefs => this.api_showKanjiDefs({options, definitions: kdefs})); +            }); + +            $('.action-play-audio').click(e => { +                e.preventDefault(); +                const index = $(e.currentTarget).data('index'); +                this.playAudio(this.definitions[index]); +            }); -function registerKanjiLinks() { -    for (const link of Array.from(document.getElementsByClassName('kanji-link'))) { -        link.addEventListener('click', e => { -            e.preventDefault(); -            invokeApi('displayKanji', e.target.innerHTML, window.parent); +            this.updateAddNoteButtons(['term_kanji', 'term_kana'], sequence);          });      } -} -function registerAddNoteLinks() { -    for (const link of Array.from(document.getElementsByClassName('action-add-note'))) { -        link.addEventListener('click', e => { -            e.preventDefault(); -            const ds = e.currentTarget.dataset; -            invokeApi('addNote', {index: ds.index, mode: ds.mode}, window.parent); +    api_showKanjiDefs({definitions, options}) { +        const sequence = ++this.sequence; +        const context = { +            definitions, +            addable: options.ankiMethod !== 'disabled' +        }; + +        this.definitions = definitions; +        this.showSpinner(false); +        window.scrollTo(0, 0); + +        renderText(context, 'kanji-list.html').then(content => { +            $('.content').html(content); +            $('.action-add-note').click(this.onAddNote.bind(this)); + +            this.updateAddNoteButtons(['kanji'], sequence);          });      } -} -function registerAudioLinks() { -    for (const link of Array.from(document.getElementsByClassName('action-play-audio'))) { -        link.addEventListener('click', e => { -            e.preventDefault(); -            const ds = e.currentTarget.dataset; -            invokeApi('playAudio', ds.index, window.parent); +    findAddNoteButton(index, mode) { +        return $(`.action-add-note[data-index="${index}"][data-mode="${mode}"]`); +    } + +    onAddNote(e) { +        e.preventDefault(); +        this.showSpinner(true); + +        const link = $(e.currentTarget); +        const index = link.data('index'); +        const mode = link.data('mode'); + +        addDefinition(this.definitions[index], mode).then(success => { +            if (success) { +                const button = this.findAddNoteButton(index, mode); +                button.addClass('disabled'); +            } else { +                window.alert('Note could not be added'); +            } +        }).catch(error => { +            window.alert('Error: ' + error); +        }).then(() => { +            this.showSpinner(false);          });      } -} -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}"]`); -        if (matches.length === 0) { -            return; -        } +    updateAddNoteButtons(modes, sequence) { +        canAddDefinitions(this.definitions, modes).then(states => { +            if (states === null) { +                return; +            } -        const classes = matches[0].classList; -        if (state[mode]) { -            classes.remove('disabled'); +            if (sequence !== this.sequence) { +                return; +            } + +            states.forEach((state, index) => { +                for (const mode in state) { +                    const button = this.findAddNoteButton(index, mode); +                    if (state[mode]) { +                        button.removeClass('disabled'); +                    } else { +                        button.addClass('disabled'); +                    } + +                    button.removeClass('pending'); +                } +            }); +        }); +    } + +    showSpinner(show) { +        const spinner = $('.spinner'); +        if (show) { +            spinner.show();          } else { -            classes.add('disabled'); +            spinner.hide();          }      } -} -document.addEventListener('DOMContentLoaded', () => { -    registerKanjiLinks(); -    registerAddNoteLinks(); -    registerAudioLinks(); -}); +    playAudio(definition) { +        let url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=${encodeURIComponent(definition.expression)}`; +        if (definition.reading) { +            url += `&kana=${encodeURIComponent(definition.reading)}`; +        } -window.addEventListener('message', e => { -    const {action, params} = e.data, method = window['api_' + action]; -    if (typeof(method) === 'function') { -        method(params); +        for (const key in this.audioCache) { +            this.audioCache[key].pause(); +        } + +        const audio = this.audioCache[url] || new Audio(url); +        audio.currentTime = 0; +        audio.play();      } -}); +} + +window.frame = new Frame(); diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 8e71fefa..d47ab4ae 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -19,25 +19,26 @@  class Popup {      constructor() { -        this.container = null;          this.offset = 10; -    } -    show(rect, content) { -        this.inject(); +        this.container = document.createElement('iframe'); +        this.container.id = 'yomichan-popup'; +        this.container.addEventListener('mousedown', e => e.stopPropagation()); +        this.container.addEventListener('scroll', e => e.stopPropagation()); +        this.container.setAttribute('src', chrome.extension.getURL('fg/frame.html')); + +        document.body.appendChild(this.container); +    } +    showAt(rect) {          this.container.style.left = rect.x + 'px';          this.container.style.top = rect.y + 'px';          this.container.style.height = rect.height + 'px';          this.container.style.width = rect.width + 'px';          this.container.style.visibility = 'visible'; - -        this.setContent(content);      } -    showNextTo(elementRect, content) { -        this.inject(); - +    showNextTo(elementRect) {          const containerStyle = window.getComputedStyle(this.container);          const containerHeight = parseInt(containerStyle.height);          const containerWidth = parseInt(containerStyle.width); @@ -56,48 +57,26 @@ class Popup {              y = elementRect.top - height - this.offset;          } -        this.show({x, y, width, height}, content); -    } - -    visible() { -        return this.container !== null && this.container.style.visibility !== 'hidden'; +        this.showAt({x, y, width, height});      }      hide() { -        if (this.container !== null) { -            this.container.style.visibility = 'hidden'; -        } +        this.container.style.visibility = 'hidden';      } -    setContent(content) { -        if (this.container === null) { -            return; -        } - -        this.container.contentWindow.scrollTo(0, 0); - -        const doc = this.container.contentDocument; -        doc.open(); -        doc.write(content); -        doc.close(); +    isVisible() { +        return this.container.style.visibility !== 'hidden';      } -    invokeApi(action, params) { -        if (this.container !== null) { -            this.container.contentWindow.postMessage({action, params}, '*'); -        } +    showTermDefs(definitions, options) { +        this.invokeApi('showTermDefs', {definitions, options});      } -    inject() { -        if (this.container !== null) { -            return; -        } - -        this.container = document.createElement('iframe'); -        this.container.id = 'yomichan-popup'; -        this.container.addEventListener('mousedown', e => e.stopPropagation()); -        this.container.addEventListener('scroll', e => e.stopPropagation()); +    showKanjiDefs(definitions, options) { +        this.invokeApi('showKanjiDefs', {definitions, options}); +    } -        document.body.appendChild(this.container); +    invokeApi(action, params) { +        this.container.contentWindow.postMessage({action, params}, '*');      }  } diff --git a/ext/fg/js/util.js b/ext/fg/js/util.js index 96007b74..cedfb887 100644 --- a/ext/fg/js/util.js +++ b/ext/fg/js/util.js @@ -17,7 +17,7 @@   */ -function invokeApiBg(action, params) { +function invokeBgApi(action, params) {      return new Promise((resolve, reject) => {          chrome.runtime.sendMessage({action, params}, ({result, error}) => {              if (error) { @@ -29,32 +29,36 @@ function invokeApiBg(action, params) {      });  } -function getEnabled() { -    return invokeApiBg('getEnabled', {}); +function isEnabled() { +    return invokeBgApi('getEnabled', {});  }  function getOptions() { -    return invokeApiBg('getOptions', {}); +    return invokeBgApi('getOptions', {});  }  function findTerm(text) { -    return invokeApiBg('findTerm', {text}); +    return invokeBgApi('findTerm', {text}); +} + +function findTermGrouped(text) { +    return invokeBgApi('findTermGrouped', {text});  }  function findKanji(text) { -    return invokeApiBg('findKanji', {text}); +    return invokeBgApi('findKanji', {text});  }  function renderText(data, template) { -    return invokeApiBg('renderText', {data, template}); +    return invokeBgApi('renderText', {data, template});  }  function canAddDefinitions(definitions, modes) { -    return invokeApiBg('canAddDefinitions', {definitions, modes}).catch(() => null); +    return invokeBgApi('canAddDefinitions', {definitions, modes}).catch(() => null);  }  function addDefinition(definition, mode) { -    return invokeApiBg('addDefinition', {definition, mode}); +    return invokeBgApi('addDefinition', {definition, mode});  }  function textSourceFromPoint(point) { diff --git a/ext/manifest.json b/ext/manifest.json index 09905cd8..90663696 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -35,7 +35,9 @@          "fg/img/add_term_kana.png",          "fg/img/add_term_kanji.png",          "fg/img/play_audio.png", +        "fg/img/spinner.gif",          "fg/js/frame.js", +        "fg/frame.html",          "fg/ttf/kanji-stroke-orders.ttf",          "fg/ttf/vl-gothic-regular.ttf"      ] diff --git a/tmpl/footer.html b/tmpl/footer.html deleted file mode 100644 index 3840600f..00000000 --- a/tmpl/footer.html +++ /dev/null @@ -1,3 +0,0 @@ -    <script src="{{root}}/js/frame.js"></script> -    </body> -</html> diff --git a/tmpl/header.html b/tmpl/header.html deleted file mode 100644 index b7fad070..00000000 --- a/tmpl/header.html +++ /dev/null @@ -1,18 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -    <head> -        <meta charset="UTF-8"> -        <title></title> -        <style> -            @font-face { -                font-family: kanji-stroke-orders; -                src:         url('{{root}}/ttf/kanji-stroke-orders.ttf'); -            } -            @font-face { -                font-family: vl-gothic-regular; -                src:         url('{{root}}/ttf/vl-gothic-regular.ttf'); -            } -        </style> -        <link rel="stylesheet" href="{{root}}/css/frame.css"> -    </head> -    <body> diff --git a/tmpl/kanji-link.html b/tmpl/kanji-link.html deleted file mode 100644 index f4f8dc69..00000000 --- a/tmpl/kanji-link.html +++ /dev/null @@ -1 +0,0 @@ -<a href="#" class="kanji-link">{{kanji}}</a> diff --git a/tmpl/kanji-list.html b/tmpl/kanji-list.html index af38d485..3f6d8084 100644 --- a/tmpl/kanji-list.html +++ b/tmpl/kanji-list.html @@ -1,9 +1,59 @@ -{{> header.html}} +{{#*inline "kanji"}} +<div class="kanji-definition"> +    <div class="action-bar"> +        {{#if addable}} +            <a href="#" title="Add Kanji" class="action-add-note pending disabled" data-mode="kanji" data-index="{{@index}}"><img src="img/add_kanji.png"></a> +        {{/if}} +    </div> + +    <div class="kanji-glyph">{{character}}</div> + +    <div class="kanji-reading"> +        <table> +            <tr> +                <th>Kunyomi:</th> +                <td> +                    {{#each kunyomi}} +                        {{.}}{{#unless @last}}, {{/unless}} +                    {{/each}} +                </td> +            </tr> +            <tr> +                <th>Onyomi:</th> +                <td> +                    {{#each onyomi}} +                        {{.}}{{#unless @last}}, {{/unless}} +                    {{/each}} +                </td> +            </tr> +        </table> +    </div> + +    <div class="kanji-tags"> +        {{#each tags}} +            <span class="tag tag-{{category}}" title="{{notes}}">{{name}}</span> +        {{/each}} +    </div> + +    <div class="kanji-glossary"> +        {{#if glossary.[1]}} +            <ol "kanji-glossary-group"> +                {{#each glossary}} +                    <li><span class="kanji-glossary-item">{{#multiLine}}{{.}}{{/multiLine}}</span></li> +                {{/each}} +            </ol> +        {{else}} +            <div class="kanji-glossary-group kanji-glossary-item">{{#multiLine}}{{glossary.[0]}}{{/multiLine}}</div> +        {{/if}} +    </div> +</div> +{{/inline}} +  {{#if definitions}}      {{#each definitions}} -    {{> kanji.html addable=../addable root=../root options=../options sequence=../sequence}} +        {{#unless @first}}<hr>{{/unless}} +        {{> kanji addable=../addable root=../root}}      {{/each}}  {{else}}      <p>No results found</p>  {{/if}} -{{> footer.html}} diff --git a/tmpl/kanji.html b/tmpl/kanji.html deleted file mode 100644 index e22dd660..00000000 --- a/tmpl/kanji.html +++ /dev/null @@ -1,50 +0,0 @@ -<div class="kanji-definition"> -    <div class="action-bar" data-sequence="{{sequence}}"> -        {{#if addable}} -        <a href="#" title="Add Kanji" class="action-add-note disabled" data-mode="kanji" data-index="{{@index}}"><img src="{{root}}/img/add_kanji.png"></a> -        {{/if}} -    </div> - -    <div class="kanji-glyph">{{character}}</div> - -    <div class="kanji-reading"> -        <table> -            <tr> -                <th>Kunyomi:</th> -                <td> -                    {{#each kunyomi}} -                    {{.}}{{#unless @last}}, {{/unless}} -                    {{/each}} -                </td> -            </tr> -            <tr> -                <th>Onyomi:</th> -                <td> -                    {{#each onyomi}} -                    {{.}}{{#unless @last}}, {{/unless}} -                    {{/each}} -                </td> -            </tr> -        </table> -    </div> - -    <div class="kanji-tags"> -        {{#each tags}} -        <span class="tag tag-{{category}}" title="{{notes}}">{{name}}</span> -        {{/each}} -    </div> - -    <div class="kanji-glossary"> -        {{#if glossary.[1]}} -        <ol> -            {{#each glossary}} -            <li><span>{{.}}</span></li> -            {{/each}} -        </ol> -        {{else}} -        <p> -            {{glossary.[0]}} -        </p> -        {{/if}} -    </div> -</div> diff --git a/tmpl/model.html b/tmpl/model.html index 94772316..acff44a0 100644 --- a/tmpl/model.html +++ b/tmpl/model.html @@ -9,7 +9,7 @@                  </button>                  <ul class="dropdown-menu dropdown-menu-right">                      {{#each markers}} -                    <li><a class="marker-link" href="#">{{.}}</a></li> +                        <li><a class="marker-link" href="#">{{.}}</a></li>                      {{/each}}                  </ul>              </div> diff --git a/tmpl/term-list.html b/tmpl/term-list.html index 2088ac71..aae365c7 100644 --- a/tmpl/term-list.html +++ b/tmpl/term-list.html @@ -1,9 +1,71 @@ -{{> header.html}} +{{#*inline "definition"}} +    {{#if tags}} +        <div class="term-tags"> +            {{#each tags}} +                <span class="tag tag-{{category}}" title="{{notes}}">{{name}}</span> +            {{/each}} +        </div> +    {{/if}} +    {{#if glossary.[1]}} +        <ul class="term-glossary-group"> +            {{#each glossary}} +                <li><span class="term-glossary-item">{{#multiLine}}{{.}}{{/multiLine}}</span></li> +            {{/each}} +        </ul> +    {{else}} +        <div class="term-glossary-group term-glossary-item">{{#multiLine}}{{glossary.[0]}}{{/multiLine}}</div> +    {{/if}} +{{/inline}} + +{{#*inline "term"}} +<div class="term-definition"> +    <div class="action-bar"> +        {{#if addable}} +            <a href="#" title="Add term as expression" class="action-add-note pending disabled" data-mode="term_kanji" data-index="{{@index}}"><img src="img/add_term_kanji.png"></a> +            <a href="#" title="Add term as reading" class="action-add-note pending disabled" data-mode="term_kana" data-index="{{@index}}"><img src="img/add_term_kana.png"></a> +        {{/if}} +        {{#if playback}} +            <a href="#" title="Play audio" class="action-play-audio" data-index="{{@index}}"><img src="img/play_audio.png"></a> +        {{/if}} +    </div> + +    {{#if reading}} +        <div class="term-expression"><ruby>{{#kanjiLinks}}{{expression}}{{/kanjiLinks}}<rt>{{reading}}</rt></ruby></div> +    {{else}} +        <div class="term-expression">{{#kanjiLinks}}{{expression}}{{/kanjiLinks}}</div> +    {{/if}} + +    {{#if reasons}} +        <div class="term-reasons"> +            {{#each reasons}} +                <span class="reasons">{{.}}</span> {{#unless @last}}«{{/unless}} +            {{/each}} +        </div> +    {{/if}} + +    <div class="term-glossary"> +    {{#if grouped}} +        {{#if definitions.[1]}} +        <ol> +            {{#each definitions}} +                <li>{{> definition}}</li> +            {{/each}} +        </ol> +        {{else}} +            {{> definition definitions.[0]}} +        {{/if}} +    {{else}} +        {{> definition}} +    {{/if}} +    </div> +</div> +{{/inline}} +  {{#if definitions}}      {{#each definitions}} -    {{> term.html addable=../addable root=../root options=../options sequence=../sequence}} +        {{#unless @first}}<hr>{{/unless}} +        {{> term grouped=../grouped addable=../addable playback=../playback}}      {{/each}}  {{else}}      <p>No results found</p>  {{/if}} -{{> footer.html}} diff --git a/tmpl/term.html b/tmpl/term.html deleted file mode 100644 index e4a0d02b..00000000 --- a/tmpl/term.html +++ /dev/null @@ -1,41 +0,0 @@ -<div class="term-definition"> -    <div class="action-bar" data-sequence="{{sequence}}"> -        {{#if options.enableAudioPlayback}} -        <a href="#" title="Play audio" class="action-play-audio" data-index="{{@index}}"><img src="{{root}}/img/play_audio.png"></a> -        {{/if}} -        {{#if addable}} -        <a href="#" title="Add term as expression" class="action-add-note disabled" data-mode="term_kanji" data-index="{{@index}}"><img src="{{root}}/img/add_term_kanji.png"></a> -        <a href="#" title="Add term as reading" class="action-add-note disabled" data-mode="term_kana" data-index="{{@index}}"><img src="{{root}}/img/add_term_kana.png"></a> -        {{/if}} -    </div> - -    {{#if reading}} -    <div class="term-expression"><ruby>{{#kanjiLinks}}{{expression}}{{/kanjiLinks}}<rt>{{reading}}</rt></ruby></div> -    {{else}} -    <div class="term-expression">{{#kanjiLinks}}{{expression}}{{/kanjiLinks}}</div> -    {{/if}} - -    <div class="term-reasons"> -        {{#each reasons}} -        <span class="reasons">{{.}}</span> {{#unless @last}}«{{/unless}} -        {{/each}} -    </div> - -    <div class="term-tags"> -        {{#each tags}} -        <span class="tag tag-{{category}}" title="{{notes}}">{{name}}</span> -        {{/each}} -    </div> - -    <div class="term-glossary"> -        {{#if glossary.[1]}} -        <ol> -            {{#each glossary}} -            <li><span>{{.}}</span></li> -            {{/each}} -        </ol> -        {{else}} -        <p>{{glossary.[0]}}</p> -        {{/if}} -    </div> -</div> |