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.gif Binary files differnew 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> |