diff options
author | Alex Yatskov <alex@foosoft.net> | 2019-10-20 11:23:20 -0700 |
---|---|---|
committer | Alex Yatskov <alex@foosoft.net> | 2019-10-20 11:23:20 -0700 |
commit | 438498435227cfa59cf9ed3430045b288cd2a7c0 (patch) | |
tree | 6a05520e5d6fa8d26d372673a9ed3e5d2da7e3fd /ext/bg | |
parent | 06d7713189be9eb51669d3842b78278371e6cfa4 (diff) | |
parent | d32fd1381b6cd5141a21c22f9ef639b2fe9774fb (diff) |
Merge branch 'master' into testing
Diffstat (limited to 'ext/bg')
-rw-r--r-- | ext/bg/context.html | 97 | ||||
-rw-r--r-- | ext/bg/css/settings.css | 40 | ||||
-rw-r--r-- | ext/bg/js/api.js | 174 | ||||
-rw-r--r-- | ext/bg/js/audio.js | 38 | ||||
-rw-r--r-- | ext/bg/js/backend.js | 11 | ||||
-rw-r--r-- | ext/bg/js/context.js | 47 | ||||
-rw-r--r-- | ext/bg/js/database.js | 167 | ||||
-rw-r--r-- | ext/bg/js/dictionary.js | 4 | ||||
-rw-r--r-- | ext/bg/js/handlebars.js | 5 | ||||
-rw-r--r-- | ext/bg/js/options.js | 10 | ||||
-rw-r--r-- | ext/bg/js/search-frontend.js | 5 | ||||
-rw-r--r-- | ext/bg/js/search.js | 71 | ||||
-rw-r--r-- | ext/bg/js/settings-popup-preview.js | 183 | ||||
-rw-r--r-- | ext/bg/js/settings.js | 233 | ||||
-rw-r--r-- | ext/bg/js/templates.js | 223 | ||||
-rw-r--r-- | ext/bg/js/translator.js | 306 | ||||
-rw-r--r-- | ext/bg/js/util.js | 2 | ||||
-rw-r--r-- | ext/bg/lang/deinflect.json | 6 | ||||
-rw-r--r-- | ext/bg/search.html | 4 | ||||
-rw-r--r-- | ext/bg/settings-popup-preview.html | 125 | ||||
-rw-r--r-- | ext/bg/settings.html | 104 |
21 files changed, 1421 insertions, 434 deletions
diff --git a/ext/bg/context.html b/ext/bg/context.html index 51346838..48fa463f 100644 --- a/ext/bg/context.html +++ b/ext/bg/context.html @@ -9,24 +9,107 @@ <style type="text/css"> body { padding: 10px; - text-align: center; } + h3 { + margin-top: 10px; + } + label { + font-weight: normal; + } + + #mini { + text-align: center; + } + #full { + display: none; + } .btn-group { display: flex; + justify-content: center; margin-top: 10px; white-space: nowrap; } + + html:root[data-mode=full] #mini { + display: none; + } + html:root[data-mode=full] #full { + display: initial; + } + + .link-group { + display: block; + line-height: 1.5em; + margin: 0 -10px; + padding: 0.5em 10px; + cursor: pointer; + color: #333; + text-decoration: none; + background-color: transparent; + transition: background-color 0.125s linear 0s; + max-width: none; + } + .link-group:hover, + .link-group:active { + color: #333; + text-decoration: none; + } + .link-group:hover>.link-group-label, + .link-group:active>.link-group-label { + text-decoration: underline; + } + .link-group:hover { + background-color: rgba(0, 0, 0, 0.05); + } + .link-group:active { + background-color: rgba(0, 0, 0, 0.1); + } + .link-group-icon { + width: 16px; + height: 16px; + text-align: center; + vertical-align: middle; + display: inline-block; + margin-right: 0.25em; + } + .link-group-icon>input { + margin: 0; + padding: 0; + } + .link-group-icon>.glyphicon { + top: 0; + } + .link-group-label { + vertical-align: middle; + } </style> </head> <body> - <div> - <input type="checkbox" id="enable-search"> + <div id="mini"> + <div> + <input type="checkbox" id="enable-search"> + </div> + <div class="btn-group"> + <a title="Search (Alt + Insert) (Middle click to open in new tab)" class="btn btn-default btn-xs action-open-search"><span class="glyphicon glyphicon-search"></span></a> + <a title="Options (Middle click to open in new tab)" class="btn btn-default btn-xs action-open-options"><span class="glyphicon glyphicon-wrench"></span></a> + <a title="Help" class="btn btn-default btn-xs action-open-help"><span class="glyphicon glyphicon-question-sign"></span></a> + </div> </div> - <div class="btn-group"> - <button type="button" id="open-search" title="Search (Alt + Insert)" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-search"></span></button> - <button type="button" id="open-options" title="Options" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-wrench"></span></button> - <button type="button" id="open-help" title="Help" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-question-sign"></span></button> + <div id="full"> + <h3 id="extension-info">Yomichan</h3> + <label class="link-group"> + <span class="link-group-icon"><input type="checkbox" id="enable-search2" /></span><span class="link-group-label">Enable content scanning</span> + </label> + <a class="link-group action-open-options"> + <span class="link-group-icon"><span class="glyphicon glyphicon-chevron-right"></span></span><span class="link-group-label">Options</span> + </a> + <a class="link-group action-open-search"> + <span class="link-group-icon"><span class="glyphicon glyphicon-chevron-right"></span></span><span class="link-group-label">Search</span> + </a> + <a class="link-group action-open-help"> + <span class="link-group-icon"><span class="glyphicon glyphicon-chevron-right"></span></span><span class="link-group-label">Help</span> + </a> </div> <script src="/mixed/lib/jquery.min.js"></script> diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index 6284058a..b3d5b884 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -128,7 +128,8 @@ content: counter(audio-source-id); } -#custom-popup-css { +#custom-popup-css, +#custom-popup-outer-css { width: 100%; min-height: 34px; height: 96px; @@ -137,14 +138,41 @@ white-space: pre; } -[data-show-for-browser] { +.btn-inner-middle { + vertical-align: middle; +} +.storage-button-inner { + pointer-events: none; +} +input[type=checkbox].storage-button-checkbox { + margin: 0 0.375em 0 0; + padding: 0; +} + +#settings-popup-preview-frame { + background-color: transparent; + border: none; + margin: 0; + padding: 0; + width: 100%; + height: 320px; +} + +[data-show-for-browser], +[data-show-for-operating-system] { display: none; } -[data-browser=edge] [data-show-for-browser~=edge], -[data-browser=chrome] [data-show-for-browser~=chrome], -[data-browser=firefox] [data-show-for-browser~=firefox], -[data-browser=firefox-mobile] [data-show-for-browser~=firefox-mobile] { +html:root[data-browser=edge] [data-show-for-browser~=edge], +html:root[data-browser=chrome] [data-show-for-browser~=chrome], +html:root[data-browser=firefox] [data-show-for-browser~=firefox], +html:root[data-browser=firefox-mobile] [data-show-for-browser~=firefox-mobile], +html:root[data-operating-system=mac] [data-show-for-operating-system~=mac], +html:root[data-operating-system=win] [data-show-for-operating-system~=win], +html:root[data-operating-system=android] [data-show-for-operating-system~=android], +html:root[data-operating-system=cros] [data-show-for-operating-system~=cros], +html:root[data-operating-system=linux] [data-show-for-operating-system~=linux], +html:root[data-operating-system=openbsd] [data-show-for-operating-system~=openbsd] { display: initial; } diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index f768e6f9..93d9c155 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -144,24 +144,46 @@ async function apiTemplateRender(template, data, dynamic) { } } -async function apiCommandExec(command) { +async function apiCommandExec(command, params) { const handlers = apiCommandExec.handlers; if (handlers.hasOwnProperty(command)) { const handler = handlers[command]; - handler(); + handler(params); } } apiCommandExec.handlers = { - search: () => { - chrome.tabs.create({url: chrome.extension.getURL('/bg/search.html')}); + search: async (params) => { + const url = chrome.extension.getURL('/bg/search.html'); + if (!(params && params.newTab)) { + try { + const tab = await apiFindTab(1000, (url2) => ( + url2 !== null && + url2.startsWith(url) && + (url2.length === url.length || url2[url.length] === '?' || url2[url.length] === '#') + )); + if (tab !== null) { + await apiFocusTab(tab); + return; + } + } catch (e) { + // NOP + } + } + chrome.tabs.create({url}); }, help: () => { chrome.tabs.create({url: 'https://foosoft.net/projects/yomichan/'}); }, - options: () => { - chrome.runtime.openOptionsPage(); + options: (params) => { + if (!(params && params.newTab)) { + chrome.runtime.openOptionsPage(); + } else { + const manifest = chrome.runtime.getManifest(); + const url = chrome.extension.getURL(manifest.options_ui.page); + chrome.tabs.create({url}); + } }, toggle: async () => { @@ -176,7 +198,7 @@ apiCommandExec.handlers = { }; async function apiAudioGetUrl(definition, source, optionsContext) { - return audioBuildUrl(definition, source, optionsContext); + return audioGetUrl(definition, source, optionsContext); } async function apiInjectScreenshot(definition, fields, screenshot) { @@ -241,3 +263,141 @@ function apiFrameInformationGet(sender) { const frameId = sender.frameId; return Promise.resolve({frameId}); } + +function apiInjectStylesheet(css, sender) { + if (!sender.tab) { + return Promise.reject(new Error('Invalid tab')); + } + + const tabId = sender.tab.id; + const frameId = sender.frameId; + const details = { + code: css, + runAt: 'document_start', + cssOrigin: 'user', + allFrames: false + }; + if (typeof frameId === 'number') { + details.frameId = frameId; + } + + return new Promise((resolve, reject) => { + chrome.tabs.insertCSS(tabId, details, () => { + const e = chrome.runtime.lastError; + if (e) { + reject(new Error(e.message)); + } else { + resolve(); + } + }); + }); +} + +async function apiGetEnvironmentInfo() { + const browser = await apiGetBrowser(); + const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve)); + return { + browser, + platform: { + os: platform.os + } + }; +} + +async function apiGetBrowser() { + if (EXTENSION_IS_BROWSER_EDGE) { + return 'edge'; + } + if (typeof browser !== 'undefined') { + try { + const info = await browser.runtime.getBrowserInfo(); + if (info.name === 'Fennec') { + return 'firefox-mobile'; + } + } catch (e) { } + return 'firefox'; + } else { + return 'chrome'; + } +} + +function apiGetTabUrl(tab) { + return new Promise((resolve) => { + chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => { + let url = null; + if (!chrome.runtime.lastError) { + url = (response !== null && typeof response === 'object' && !Array.isArray(response) ? response.url : null); + if (url !== null && typeof url !== 'string') { + url = null; + } + } + resolve({tab, url}); + }); + }); +} + +async function apiFindTab(timeout, checkUrl) { + // This function works around the need to have the "tabs" permission to access tab.url. + const tabs = await new Promise((resolve) => chrome.tabs.query({}, resolve)); + let matchPromiseResolve = null; + const matchPromise = new Promise((resolve) => { matchPromiseResolve = resolve; }); + + const checkTabUrl = ({tab, url}) => { + if (checkUrl(url, tab)) { + matchPromiseResolve(tab); + } + }; + + const promises = []; + for (const tab of tabs) { + const promise = apiGetTabUrl(tab); + promise.then(checkTabUrl); + promises.push(promise); + } + + const racePromises = [ + matchPromise, + Promise.all(promises).then(() => null) + ]; + if (typeof timeout === 'number') { + racePromises.push(new Promise((resolve) => setTimeout(() => resolve(null), timeout))); + } + + return await Promise.race(racePromises); +} + +async function apiFocusTab(tab) { + await new Promise((resolve, reject) => { + chrome.tabs.update(tab.id, {active: true}, () => { + const e = chrome.runtime.lastError; + if (e) { reject(e); } + else { resolve(); } + }); + }); + + if (!(typeof chrome.windows === 'object' && chrome.windows !== null)) { + // Windows not supported (e.g. on Firefox mobile) + return; + } + + try { + const tabWindow = await new Promise((resolve) => { + chrome.windows.get(tab.windowId, {}, (tabWindow) => { + const e = chrome.runtime.lastError; + if (e) { reject(e); } + else { resolve(tabWindow); } + }); + }); + if (!tabWindow.focused) { + await new Promise((resolve, reject) => { + chrome.windows.update(tab.windowId, {focused: true}, () => { + const e = chrome.runtime.lastError; + if (e) { reject(e); } + else { resolve(); } + }); + }); + } + } catch (e) { + // Edge throws exception for no reason here. + } +} diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index 9e0ae67c..3efcce46 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -86,6 +86,24 @@ const audioUrlBuilders = { throw new Error('Failed to find audio URL'); }, + 'text-to-speech': async (definition, optionsContext) => { + const options = await apiOptionsGet(optionsContext); + const voiceURI = options.audio.textToSpeechVoice; + if (!voiceURI) { + throw new Error('No voice'); + } + + return `tts:?text=${encodeURIComponent(definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; + }, + 'text-to-speech-reading': async (definition, optionsContext) => { + const options = await apiOptionsGet(optionsContext); + const voiceURI = options.audio.textToSpeechVoice; + if (!voiceURI) { + throw new Error('No voice'); + } + + return `tts:?text=${encodeURIComponent(definition.reading || definition.expression)}&voice=${encodeURIComponent(voiceURI)}`; + }, 'custom': async (definition, optionsContext) => { const options = await apiOptionsGet(optionsContext); const customSourceUrl = options.audio.customSourceUrl; @@ -93,20 +111,14 @@ const audioUrlBuilders = { } }; -async function audioBuildUrl(definition, mode, optionsContext, cache={}) { - const cacheKey = `${mode}:${definition.expression}`; - if (cache.hasOwnProperty(cacheKey)) { - return Promise.resolve(cache[cacheKey]); - } - +async function audioGetUrl(definition, mode, optionsContext, download) { if (audioUrlBuilders.hasOwnProperty(mode)) { const handler = audioUrlBuilders[mode]; - return handler(definition, optionsContext).then( - (url) => { - cache[cacheKey] = url; - return url; - }, - () => null); + try { + return await handler(definition, optionsContext, download); + } catch (e) { + // NOP + } } return null; } @@ -163,7 +175,7 @@ async function audioInject(definition, fields, sources, optionsContext) { audioSourceDefinition = definition.expressions[0]; } - const {url} = await audioGetFromSources(audioSourceDefinition, sources, optionsContext, false); + const {url} = await audioGetFromSources(audioSourceDefinition, sources, optionsContext, true); if (url !== null) { const filename = audioBuildFilename(audioSourceDefinition); if (filename !== null) { diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 453f4282..f29230a2 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -73,9 +73,10 @@ class Backend { if (handlers.hasOwnProperty(action)) { const handler = handlers[action]; const promise = handler(params, sender); - promise - .then(result => callback({result})) - .catch(error => callback(errorToJson(error))); + promise.then( + result => callback({result}), + error => callback({error: errorToJson(error)}) + ); } return true; @@ -180,11 +181,13 @@ Backend.messageHandlers = { definitionsAddable: ({definitions, modes, optionsContext}) => apiDefinitionsAddable(definitions, modes, optionsContext), noteView: ({noteId}) => apiNoteView(noteId), templateRender: ({template, data, dynamic}) => apiTemplateRender(template, data, dynamic), - commandExec: ({command}) => apiCommandExec(command), + commandExec: ({command, params}) => apiCommandExec(command, params), audioGetUrl: ({definition, source, optionsContext}) => apiAudioGetUrl(definition, source, optionsContext), screenshotGet: ({options}, sender) => apiScreenshotGet(options, sender), forward: ({action, params}, sender) => apiForward(action, params, sender), frameInformationGet: (params, sender) => apiFrameInformationGet(sender), + injectStylesheet: ({css}, sender) => apiInjectStylesheet(css, sender), + getEnvironmentInfo: () => apiGetEnvironmentInfo() }; window.yomichan_backend = new Backend(); diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js index 0f88e9c0..8e1dbce6 100644 --- a/ext/bg/js/context.js +++ b/ext/bg/js/context.js @@ -17,10 +17,47 @@ */ +function showExtensionInfo() { + const node = document.getElementById('extension-info'); + if (node === null) { return; } + + const manifest = chrome.runtime.getManifest(); + node.textContent = `${manifest.name} v${manifest.version}`; +} + +function setupButtonEvents(selector, command, url) { + const node = $(selector); + node.on('click', (e) => { + if (e.button !== 0) { return; } + apiCommandExec(command, {newTab: e.ctrlKey}); + e.preventDefault(); + }) + .on('auxclick', (e) => { + if (e.button !== 1) { return; } + apiCommandExec(command, {newTab: true}); + e.preventDefault(); + }); + + if (typeof url === 'string') { + node.attr('href', url); + node.attr('target', '_blank'); + node.attr('rel', 'noopener'); + } +} + $(document).ready(utilAsync(() => { - $('#open-search').click(() => apiCommandExec('search')); - $('#open-options').click(() => apiCommandExec('options')); - $('#open-help').click(() => apiCommandExec('help')); + showExtensionInfo(); + + apiGetEnvironmentInfo().then(({browser}) => { + // Firefox mobile opens this page as a full webpage. + document.documentElement.dataset.mode = (browser === 'firefox-mobile' ? 'full' : 'mini'); + }); + + const manifest = chrome.runtime.getManifest(); + + setupButtonEvents('.action-open-search', 'search', chrome.extension.getURL('/bg/search.html')); + setupButtonEvents('.action-open-options', 'options', chrome.extension.getURL(manifest.options_ui.page)); + setupButtonEvents('.action-open-help', 'help'); const optionsContext = { depth: 0, @@ -31,5 +68,9 @@ $(document).ready(utilAsync(() => { toggle.prop('checked', options.general.enable).change(); toggle.bootstrapToggle(); toggle.change(() => apiCommandExec('toggle')); + + const toggle2 = $('#enable-search2'); + toggle2.prop('checked', options.general.enable).change(); + toggle2.change(() => apiCommandExec('toggle')); }); })); diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 771a71c9..9f477b24 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -20,7 +20,6 @@ class Database { constructor() { this.db = null; - this.tagCache = {}; } async prepare() { @@ -53,33 +52,20 @@ class Database { this.db.close(); await this.db.delete(); this.db = null; - this.tagCache = {}; await this.prepare(); } - async findTerms(term, titles) { + async findTermsBulk(termList, titles) { this.validate(); - const results = []; - await this.db.terms.where('expression').equals(term).or('reading').equals(term).each(row => { - if (titles.includes(row.dictionary)) { - results.push(Database.createTerm(row)); - } - }); - - return results; - } - - async findTermsBulk(terms, titles) { const promises = []; const visited = {}; const results = []; - const createResult = Database.createTerm; const processRow = (row, index) => { if (titles.includes(row.dictionary) && !visited.hasOwnProperty(row.id)) { visited[row.id] = true; - results.push(createResult(row, index)); + results.push(Database.createTerm(row, index)); } }; @@ -89,8 +75,8 @@ class Database { const dbIndex1 = dbTerms.index('expression'); const dbIndex2 = dbTerms.index('reading'); - for (let i = 0; i < terms.length; ++i) { - const only = IDBKeyRange.only(terms[i]); + for (let i = 0; i < termList.length; ++i) { + const only = IDBKeyRange.only(termList[i]); promises.push( Database.getAll(dbIndex1, only, i, processRow), Database.getAll(dbIndex2, only, i, processRow) @@ -102,66 +88,50 @@ class Database { return results; } - async findTermsExact(term, reading, titles) { + async findTermsExactBulk(termList, readingList, titles) { this.validate(); + const promises = []; const results = []; - await this.db.terms.where('expression').equals(term).each(row => { - if (row.reading === reading && titles.includes(row.dictionary)) { - results.push(Database.createTerm(row)); + const processRow = (row, index) => { + if (row.reading === readingList[index] && titles.includes(row.dictionary)) { + results.push(Database.createTerm(row, index)); } - }); + }; - return results; - } + const db = this.db.backendDB(); + const dbTransaction = db.transaction(['terms'], 'readonly'); + const dbTerms = dbTransaction.objectStore('terms'); + const dbIndex = dbTerms.index('expression'); - async findTermsBySequence(sequence, mainDictionary) { - this.validate(); + for (let i = 0; i < termList.length; ++i) { + const only = IDBKeyRange.only(termList[i]); + promises.push(Database.getAll(dbIndex, only, i, processRow)); + } - const results = []; - await this.db.terms.where('sequence').equals(sequence).each(row => { - if (row.dictionary === mainDictionary) { - results.push(Database.createTerm(row)); - } - }); + await Promise.all(promises); return results; } - async findTermMeta(term, titles) { + async findTermsBySequenceBulk(sequenceList, mainDictionary) { this.validate(); - const results = []; - await this.db.termMeta.where('expression').equals(term).each(row => { - if (titles.includes(row.dictionary)) { - results.push({ - mode: row.mode, - data: row.data, - dictionary: row.dictionary - }); - } - }); - - return results; - } - - async findTermMetaBulk(terms, titles) { const promises = []; const results = []; - const createResult = Database.createTermMeta; const processRow = (row, index) => { - if (titles.includes(row.dictionary)) { - results.push(createResult(row, index)); + if (row.dictionary === mainDictionary) { + results.push(Database.createTerm(row, index)); } }; const db = this.db.backendDB(); - const dbTransaction = db.transaction(['termMeta'], 'readonly'); - const dbTerms = dbTransaction.objectStore('termMeta'); - const dbIndex = dbTerms.index('expression'); + const dbTransaction = db.transaction(['terms'], 'readonly'); + const dbTerms = dbTransaction.objectStore('terms'); + const dbIndex = dbTerms.index('sequence'); - for (let i = 0; i < terms.length; ++i) { - const only = IDBKeyRange.only(terms[i]); + for (let i = 0; i < sequenceList.length; ++i) { + const only = IDBKeyRange.only(sequenceList[i]); promises.push(Database.getAll(dbIndex, only, i, processRow)); } @@ -170,67 +140,59 @@ class Database { return results; } - async findKanji(kanji, titles) { - this.validate(); + async findTermMetaBulk(termList, titles) { + return this.findGenericBulk('termMeta', 'expression', termList, titles, Database.createMeta); + } - const results = []; - await this.db.kanji.where('character').equals(kanji).each(row => { - if (titles.includes(row.dictionary)) { - results.push({ - character: row.character, - onyomi: dictFieldSplit(row.onyomi), - kunyomi: dictFieldSplit(row.kunyomi), - tags: dictFieldSplit(row.tags), - glossary: row.meanings, - stats: row.stats, - dictionary: row.dictionary - }); - } - }); + async findKanjiBulk(kanjiList, titles) { + return this.findGenericBulk('kanji', 'character', kanjiList, titles, Database.createKanji); + } - return results; + async findKanjiMetaBulk(kanjiList, titles) { + return this.findGenericBulk('kanjiMeta', 'character', kanjiList, titles, Database.createMeta); } - async findKanjiMeta(kanji, titles) { + async findGenericBulk(tableName, indexName, indexValueList, titles, createResult) { this.validate(); + const promises = []; const results = []; - await this.db.kanjiMeta.where('character').equals(kanji).each(row => { + const processRow = (row, index) => { if (titles.includes(row.dictionary)) { - results.push({ - mode: row.mode, - data: row.data, - dictionary: row.dictionary - }); + results.push(createResult(row, index)); } - }); + }; - return results; - } + const db = this.db.backendDB(); + const dbTransaction = db.transaction([tableName], 'readonly'); + const dbTerms = dbTransaction.objectStore(tableName); + const dbIndex = dbTerms.index(indexName); - findTagForTitleCached(name, title) { - if (this.tagCache.hasOwnProperty(title)) { - const cache = this.tagCache[title]; - if (cache.hasOwnProperty(name)) { - return cache[name]; - } + for (let i = 0; i < indexValueList.length; ++i) { + const only = IDBKeyRange.only(indexValueList[i]); + promises.push(Database.getAll(dbIndex, only, i, processRow)); } + + await Promise.all(promises); + + return results; } async findTagForTitle(name, title) { this.validate(); - const cache = (this.tagCache.hasOwnProperty(title) ? this.tagCache[title] : (this.tagCache[title] = {})); - let result = null; - await this.db.tagMeta.where('name').equals(name).each(row => { + const db = this.db.backendDB(); + const dbTransaction = db.transaction(['tagMeta'], 'readonly'); + const dbTerms = dbTransaction.objectStore('tagMeta'); + const dbIndex = dbTerms.index('name'); + const only = IDBKeyRange.only(name); + await Database.getAll(dbIndex, only, null, row => { if (title === row.dictionary) { result = row; } }); - cache[name] = result; - return result; } @@ -522,7 +484,20 @@ class Database { }; } - static createTermMeta(row, index) { + static createKanji(row, index) { + return { + index, + character: row.character, + onyomi: dictFieldSplit(row.onyomi), + kunyomi: dictFieldSplit(row.kunyomi), + tags: dictFieldSplit(row.tags), + glossary: row.meanings, + stats: row.stats, + dictionary: row.dictionary + }; + } + + static createMeta(row, index) { return { index, mode: row.mode, diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index 498eafcd..191058c1 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -342,10 +342,10 @@ async function dictFieldFormat(field, definition, mode, options) { 'kunyomi', 'onyomi', 'reading', + 'screenshot', 'sentence', 'tags', - 'url', - 'screenshot' + 'url' ]; for (const marker of markers) { diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js index 66d5fa2b..92764a20 100644 --- a/ext/bg/js/handlebars.js +++ b/ext/bg/js/handlebars.js @@ -75,6 +75,10 @@ function handlebarsMultiLine(options) { return options.fn(this).split('\n').join('<br>'); } +function handlebarsSanitizeCssClass(options) { + return options.fn(this).replace(/[^_a-z0-9\u00a0-\uffff]/ig, '_'); +} + function handlebarsRegisterHelpers() { if (Handlebars.partials !== Handlebars.templates) { Handlebars.partials = Handlebars.templates; @@ -83,6 +87,7 @@ function handlebarsRegisterHelpers() { Handlebars.registerHelper('furiganaPlain', handlebarsFuriganaPlain); Handlebars.registerHelper('kanjiLinks', handlebarsKanjiLinks); Handlebars.registerHelper('multiLine', handlebarsMultiLine); + Handlebars.registerHelper('sanitizeCssClass', handlebarsSanitizeCssClass); } } diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index d0aa6fd3..4854cd65 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -276,15 +276,19 @@ function profileOptionsCreateDefaults() { compactTags: false, compactGlossaries: false, mainDictionary: '', - customPopupCss: '' + popupTheme: 'default', + popupOuterTheme: 'default', + customPopupCss: '', + customPopupOuterCss: '' }, audio: { enabled: true, - sources: ['jpod101', 'jpod101-alternate', 'jisho', 'custom'], + sources: ['jpod101'], volume: 100, autoPlay: false, - customSourceUrl: '' + customSourceUrl: '', + textToSpeechVoice: '' }, scanning: { diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js index 0c1a61ea..b21dac17 100644 --- a/ext/bg/js/search-frontend.js +++ b/ext/bg/js/search-frontend.js @@ -25,11 +25,14 @@ async function searchFrontendSetup() { const options = await apiOptionsGet(optionsContext); if (!options.scanning.enableOnSearchPage) { return; } + window.frontendInitializationData = {depth: 1, proxy: false}; + const scriptSrcs = [ '/fg/js/frontend-api-receiver.js', '/fg/js/popup.js', '/fg/js/popup-proxy-host.js', - '/fg/js/frontend.js' + '/fg/js/frontend.js', + '/fg/js/frontend-initialize.js' ]; for (const src of scriptSrcs) { const script = document.createElement('script'); diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index ead9ba6f..431478c9 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -31,25 +31,37 @@ class DisplaySearch extends Display { this.intro = document.querySelector('#intro'); this.introVisible = true; this.introAnimationTimer = null; + } - this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract}); + static create() { + const instance = new DisplaySearch(); + instance.prepare(); + return instance; + } - if (this.search !== null) { - this.search.addEventListener('click', (e) => this.onSearch(e), false); - } - if (this.query !== null) { - this.query.addEventListener('input', () => this.onSearchInput(), false); + async prepare() { + try { + await this.initialize(); - const query = DisplaySearch.getSearchQueryFromLocation(window.location.href); - if (query !== null) { - this.query.value = window.wanakana.toKana(query); - this.onSearchQueryUpdated(query, false); + if (this.search !== null) { + this.search.addEventListener('click', (e) => this.onSearch(e), false); } + if (this.query !== null) { + this.query.addEventListener('input', () => this.onSearchInput(), false); - window.wanakana.bind(this.query); - } + const query = DisplaySearch.getSearchQueryFromLocation(window.location.href); + if (query !== null) { + this.query.value = window.wanakana.toKana(query); + this.onSearchQueryUpdated(query, false); + } - this.updateSearchButton(); + window.wanakana.bind(this.query); + } + + this.updateSearchButton(); + } catch (e) { + this.onError(e); + } } onError(error) { @@ -89,7 +101,11 @@ class DisplaySearch extends Display { this.updateSearchButton(); if (valid) { const {definitions} = await apiTermsFind(query, this.optionsContext); - this.termsShow(definitions, await apiOptionsGet(this.optionsContext)); + this.setContentTerms(definitions, { + focus: false, + sentence: null, + url: window.location.href + }); } else { this.container.textContent = ''; } @@ -98,6 +114,25 @@ class DisplaySearch extends Display { } } + onRuntimeMessage({action, params}, sender, callback) { + const handlers = DisplaySearch.runtimeMessageHandlers; + if (handlers.hasOwnProperty(action)) { + const handler = handlers[action]; + const result = handler(this, params); + callback(result); + } else { + return super.onRuntimeMessage({action, params}, sender, callback); + } + } + + getOptionsContext() { + return this.optionsContext; + } + + setCustomCss() { + // No custom CSS + } + setIntroVisible(visible, animate) { if (this.introVisible === visible) { return; @@ -164,4 +199,10 @@ class DisplaySearch extends Display { } } -window.yomichan_search = new DisplaySearch(); +DisplaySearch.runtimeMessageHandlers = { + getUrl: () => { + return {url: window.location.href}; + } +}; + +window.yomichan_search = DisplaySearch.create(); diff --git a/ext/bg/js/settings-popup-preview.js b/ext/bg/js/settings-popup-preview.js new file mode 100644 index 00000000..b12fb726 --- /dev/null +++ b/ext/bg/js/settings-popup-preview.js @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2019 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 SettingsPopupPreview { + constructor() { + this.frontend = null; + this.apiOptionsGetOld = apiOptionsGet; + this.popupInjectOuterStylesheetOld = Popup.injectOuterStylesheet; + this.popupShown = false; + this.themeChangeTimeout = null; + } + + static create() { + const instance = new SettingsPopupPreview(); + instance.prepare(); + return instance; + } + + async prepare() { + // Setup events + window.addEventListener('resize', (e) => this.onWindowResize(e), false); + window.addEventListener('message', (e) => this.onMessage(e), false); + + const themeDarkCheckbox = document.querySelector('#theme-dark-checkbox'); + if (themeDarkCheckbox !== null) { + themeDarkCheckbox.addEventListener('change', () => this.onThemeDarkCheckboxChanged(themeDarkCheckbox), false); + } + + // Overwrite API functions + window.apiOptionsGet = (...args) => this.apiOptionsGet(...args); + + // Overwrite frontend + this.frontend = Frontend.create(); + window.yomichan_frontend = this.frontend; + + this.frontend.setEnabled = function () {}; + this.frontend.searchClear = function () {}; + + this.frontend.popup.childrenSupported = false; + this.frontend.popup.interactive = false; + + await this.frontend.isPrepared(); + + // Overwrite popup + Popup.injectOuterStylesheet = (...args) => this.popupInjectOuterStylesheet(...args); + + // Update search + this.updateSearch(); + } + + async apiOptionsGet(...args) { + const options = await this.apiOptionsGetOld(...args); + options.general.enable = true; + options.general.debugInfo = false; + options.general.popupWidth = 400; + options.general.popupHeight = 250; + options.general.popupHorizontalOffset = 0; + options.general.popupVerticalOffset = 10; + options.general.popupHorizontalOffset2 = 10; + options.general.popupVerticalOffset2 = 0; + options.general.popupHorizontalTextPosition = 'below'; + options.general.popupVerticalTextPosition = 'before'; + options.scanning.selectText = false; + return options; + } + + popupInjectOuterStylesheet(...args) { + // This simulates the stylesheet priorities when injecting using the web extension API. + const result = this.popupInjectOuterStylesheetOld(...args); + + const outerStylesheet = Popup.outerStylesheet; + const node = document.querySelector('#client-css'); + if (node !== null && outerStylesheet !== null) { + node.parentNode.insertBefore(outerStylesheet, node); + } + + return result; + } + + onWindowResize() { + if (this.frontend === null) { return; } + const textSource = this.frontend.textSourceLast; + if (textSource === null) { return; } + + const elementRect = textSource.getRect(); + const writingMode = textSource.getWritingMode(); + this.frontend.popup.showContent(elementRect, writingMode); + } + + onMessage(e) { + const {action, params} = e.data; + const handlers = SettingsPopupPreview.messageHandlers; + if (handlers.hasOwnProperty(action)) { + const handler = handlers[action]; + handler(this, params); + } + } + + onThemeDarkCheckboxChanged(node) { + document.documentElement.classList.toggle('dark', node.checked); + if (this.themeChangeTimeout !== null) { + clearTimeout(this.themeChangeTimeout); + } + this.themeChangeTimeout = setTimeout(() => { + this.themeChangeTimeout = null; + this.frontend.popup.updateTheme(); + }, 300); + } + + setText(text) { + const exampleText = document.querySelector('#example-text'); + if (exampleText === null) { return; } + + exampleText.textContent = text; + this.updateSearch(); + } + + setInfoVisible(visible) { + const node = document.querySelector('.placeholder-info'); + if (node === null) { return; } + + node.classList.toggle('placeholder-info-visible', visible); + } + + setCustomCss(css) { + if (this.frontend === null) { return; } + this.frontend.popup.setCustomCss(css); + } + + setCustomOuterCss(css) { + if (this.frontend === null) { return; } + this.frontend.popup.setCustomOuterCss(css, true); + } + + async updateSearch() { + const exampleText = document.querySelector('#example-text'); + if (exampleText === null) { return; } + + const textNode = exampleText.firstChild; + if (textNode === null) { return; } + + const range = document.createRange(); + range.selectNode(textNode); + const source = new TextSourceRange(range, range.toString(), null); + + this.frontend.textSourceLast = null; + await this.frontend.searchSource(source, 'script'); + await this.frontend.lastShowPromise; + + if (this.frontend.popup.isVisible()) { + this.popupShown = true; + } + + this.setInfoVisible(!this.popupShown); + } +} + +SettingsPopupPreview.messageHandlers = { + setText: (self, {text}) => self.setText(text), + setCustomCss: (self, {css}) => self.setCustomCss(css), + setCustomOuterCss: (self, {css}) => self.setCustomOuterCss(css) +}; + +SettingsPopupPreview.instance = SettingsPopupPreview.create(); + + + diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index f3b5ff16..05a0604a 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -39,12 +39,16 @@ async function formRead(options) { options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10); options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0); options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10); + options.general.popupTheme = $('#popup-theme').val(); + options.general.popupOuterTheme = $('#popup-outer-theme').val(); options.general.customPopupCss = $('#custom-popup-css').val(); + options.general.customPopupOuterCss = $('#custom-popup-outer-css').val(); options.audio.enabled = $('#audio-playback-enabled').prop('checked'); options.audio.autoPlay = $('#auto-play-audio').prop('checked'); options.audio.volume = parseFloat($('#audio-playback-volume').val()); options.audio.customSourceUrl = $('#audio-custom-source').val(); + options.audio.textToSpeechVoice = $('#text-to-speech-voice').val(); options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked'); options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked'); @@ -107,12 +111,16 @@ async function formWrite(options) { $('#popup-vertical-offset').val(options.general.popupVerticalOffset); $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2); $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2); + $('#popup-theme').val(options.general.popupTheme); + $('#popup-outer-theme').val(options.general.popupOuterTheme); $('#custom-popup-css').val(options.general.customPopupCss); + $('#custom-popup-outer-css').val(options.general.customPopupOuterCss); $('#audio-playback-enabled').prop('checked', options.audio.enabled); $('#auto-play-audio').prop('checked', options.audio.autoPlay); $('#audio-playback-volume').val(options.audio.volume); $('#audio-custom-source').val(options.audio.customSourceUrl); + $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice); $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled); @@ -248,6 +256,7 @@ async function onReady() { showExtensionInformation(); formSetupEventListeners(); + appearanceInitialize(); await audioSettingsInitialize(); await profileOptionsSetup(); @@ -260,6 +269,55 @@ $(document).ready(utilAsync(onReady)); /* + * Appearance + */ + +function appearanceInitialize() { + let previewVisible = false; + $('#settings-popup-preview-button').on('click', () => { + if (previewVisible) { return; } + showAppearancePreview(); + previewVisible = true; + }); +} + +function showAppearancePreview() { + const container = $('#settings-popup-preview-container'); + const buttonContainer = $('#settings-popup-preview-button-container'); + const settings = $('#settings-popup-preview-settings'); + const text = $('#settings-popup-preview-text'); + const customCss = $('#custom-popup-css'); + const customOuterCss = $('#custom-popup-outer-css'); + + const frame = document.createElement('iframe'); + frame.src = '/bg/settings-popup-preview.html'; + frame.id = 'settings-popup-preview-frame'; + + window.wanakana.bind(text[0]); + + text.on('input', () => { + const action = 'setText'; + const params = {text: text.val()}; + frame.contentWindow.postMessage({action, params}, '*'); + }); + customCss.on('input', () => { + const action = 'setCustomCss'; + const params = {css: customCss.val()}; + frame.contentWindow.postMessage({action, params}, '*'); + }); + customOuterCss.on('input', () => { + const action = 'setCustomOuterCss'; + const params = {css: customOuterCss.val()}; + frame.contentWindow.postMessage({action, params}, '*'); + }); + + container.append(frame); + buttonContainer.remove(); + settings.css('display', ''); +} + + +/* * Audio */ @@ -270,6 +328,81 @@ async function audioSettingsInitialize() { const options = await apiOptionsGet(optionsContext); audioSourceUI = new AudioSourceUI.Container(options.audio.sources, $('.audio-source-list'), $('.audio-source-add')); audioSourceUI.save = () => apiOptionsSave(); + + textToSpeechInitialize(); +} + +function textToSpeechInitialize() { + if (typeof speechSynthesis === 'undefined') { return; } + + speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false); + updateTextToSpeechVoices(); + + $('#text-to-speech-voice-test').on('click', () => textToSpeechTest()); +} + +function updateTextToSpeechVoices() { + const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index})); + voices.sort(textToSpeechVoiceCompare); + if (voices.length > 0) { + $('#text-to-speech-voice-container').css('display', ''); + } + + const select = $('#text-to-speech-voice'); + select.empty(); + select.append($('<option>').val('').text('None')); + for (const {voice} of voices) { + select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`)); + } + + select.val(select.attr('data-value')); +} + +function languageTagIsJapanese(languageTag) { + return ( + languageTag.startsWith('ja-') || + languageTag.startsWith('jpn-') + ); +} + +function textToSpeechVoiceCompare(a, b) { + const aIsJapanese = languageTagIsJapanese(a.voice.lang); + const bIsJapanese = languageTagIsJapanese(b.voice.lang); + if (aIsJapanese) { + if (!bIsJapanese) { return -1; } + } else { + if (bIsJapanese) { return 1; } + } + + const aIsDefault = a.voice.default; + const bIsDefault = b.voice.default; + if (aIsDefault) { + if (!bIsDefault) { return -1; } + } else { + if (bIsDefault) { return 1; } + } + + if (a.index < b.index) { return -1; } + if (a.index > b.index) { return 1; } + return 0; +} + +function textToSpeechTest() { + try { + const text = $('#text-to-speech-voice-test').attr('data-speech-text') || ''; + const voiceURI = $('#text-to-speech-voice').val(); + const voice = audioGetTextToSpeechVoice(voiceURI); + if (voice === null) { return; } + + const utterance = new SpeechSynthesisUtterance(text); + utterance.lang = 'ja-JP'; + utterance.voice = voice; + utterance.volume = 1.0; + + speechSynthesis.speak(utterance); + } catch (e) { + // NOP + } } @@ -297,9 +430,14 @@ async function onOptionsUpdate({source}) { await formWrite(options); } -function onMessage({action, params}) { - if (action === 'optionsUpdate') { - onOptionsUpdate(params); +function onMessage({action, params}, sender, callback) { + switch (action) { + case 'optionsUpdate': + onOptionsUpdate(params); + break; + case 'getUrl': + callback({url: window.location.href}); + break; } } @@ -607,10 +745,10 @@ async function ankiFieldsPopulate(element, options) { 'glossary', 'glossary-brief', 'reading', + 'screenshot', 'sentence', 'tags', - 'url', - 'screenshot' + 'url' ], 'kanji': [ 'character', @@ -618,6 +756,7 @@ async function ankiFieldsPopulate(element, options) { 'glossary', 'kunyomi', 'onyomi', + 'screenshot', 'sentence', 'tags', 'url' @@ -685,32 +824,15 @@ async function onAnkiFieldTemplatesReset(e) { * Storage */ -async function getBrowser() { - if (EXTENSION_IS_BROWSER_EDGE) { - return 'edge'; - } - if (typeof browser !== 'undefined') { - try { - const info = await browser.runtime.getBrowserInfo(); - if (info.name === 'Fennec') { - return 'firefox-mobile'; - } - } catch (e) { } - return 'firefox'; - } else { - return 'chrome'; - } -} - function storageBytesToLabeledString(size) { const base = 1000; - const labels = ['bytes', 'KB', 'MB', 'GB']; + const labels = [' bytes', 'KB', 'MB', 'GB']; let labelIndex = 0; while (size >= base) { size /= base; ++labelIndex; } - const label = size.toFixed(1); + const label = labelIndex === 0 ? `${size}` : size.toFixed(1); return `${label}${labels[labelIndex]}`; } @@ -722,15 +844,21 @@ async function storageEstimate() { } storageEstimate.mostRecent = null; +async function isStoragePeristent() { + try { + return await navigator.storage.persisted(); + } catch (e) { } + return false; +} + async function storageInfoInitialize() { - const browser = await getBrowser(); - const container = document.querySelector('#storage-info'); - container.setAttribute('data-browser', browser); + storagePersistInitialize(); + const {browser, platform} = await apiGetEnvironmentInfo(); + document.documentElement.dataset.browser = browser; + document.documentElement.dataset.operatingSystem = platform.os; await storageShowInfo(); - container.classList.remove('storage-hidden'); - document.querySelector('#storage-refresh').addEventListener('click', () => storageShowInfo(), false); } @@ -741,8 +869,14 @@ async function storageUpdateStats() { const valid = (estimate !== null); if (valid) { - document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage); - document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota); + // Firefox reports usage as 0 when persistent storage is enabled. + const finite = (estimate.usage > 0 || !(await isStoragePeristent())); + if (finite) { + document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage); + document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota); + } + document.querySelector('#storage-use-finite').classList.toggle('storage-hidden', !finite); + document.querySelector('#storage-use-infinite').classList.toggle('storage-hidden', finite); } storageUpdateStats.isUpdating = false; @@ -769,6 +903,43 @@ function storageSpinnerShow(show) { } } +async function storagePersistInitialize() { + if (!(navigator.storage && navigator.storage.persist)) { + // Not supported + return; + } + + const info = document.querySelector('#storage-persist-info'); + const button = document.querySelector('#storage-persist-button'); + const checkbox = document.querySelector('#storage-persist-button-checkbox'); + + info.classList.remove('storage-hidden'); + button.classList.remove('storage-hidden'); + + let persisted = await isStoragePeristent(); + checkbox.checked = persisted; + + button.addEventListener('click', async () => { + if (persisted) { + return; + } + let result = false; + try { + result = await navigator.storage.persist(); + } catch (e) { + // NOP + } + + if (result) { + persisted = true; + checkbox.checked = true; + storageShowInfo(); + } else { + $('.storage-persist-fail-warning').removeClass('storage-hidden'); + } + }, false); +} + /* * Information diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js index c61f5d7f..59516d97 100644 --- a/ext/bg/js/templates.js +++ b/ext/bg/js/templates.js @@ -208,148 +208,157 @@ templates['model.html'] = template({"1":function(container,depth0,helpers,partia + " </ul>\n </div>\n </div>\n </td>\n</tr>\n"; },"useData":true}); templates['terms.html'] = template({"1":function(container,depth0,helpers,partials,data) { - var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); - - return ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.definitionTags : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.only : depth0),{"name":"if","hash":{},"fn":container.program(7, 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(11, data, 0),"inverse":container.program(17, data, 0),"data":data})) != null ? stack1 : ""); + var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer = + "<div class=\"dict-"; + stack1 = ((helper = (helper = helpers.sanitizeCssClass || (depth0 != null ? depth0.sanitizeCssClass : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"sanitizeCssClass","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper)); + if (!helpers.sanitizeCssClass) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} + if (stack1 != null) { buffer += stack1; } + return buffer + "\">\n" + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.definitionTags : depth0),{"name":"if","hash":{},"fn":container.program(4, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.only : depth0),{"name":"if","hash":{},"fn":container.program(9, 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(13, data, 0),"inverse":container.program(19, data, 0),"data":data})) != null ? stack1 : "") + + "</div>\n"; },"2":function(container,depth0,helpers,partials,data) { + var helper; + + return container.escapeExpression(((helper = (helper = helpers.dictionary || (depth0 != null ? depth0.dictionary : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"dictionary","hash":{},"data":data}) : helper))); +},"4":function(container,depth0,helpers,partials,data) { var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); - return "<div " - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + return " <div " + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ">\n" - + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.definitionTags : depth0),{"name":"each","hash":{},"fn":container.program(5, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + "</div>\n"; -},"3":function(container,depth0,helpers,partials,data) { - return "class=\"compact-info\""; + + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.definitionTags : depth0),{"name":"each","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + " </div>\n"; },"5":function(container,depth0,helpers,partials,data) { + return "class=\"compact-info\""; +},"7":function(container,depth0,helpers,partials,data) { var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; - return " <span class=\"label label-default tag-" + return " <span class=\"label label-default 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"; -},"7":function(container,depth0,helpers,partials,data) { +},"9":function(container,depth0,helpers,partials,data) { var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); - return "<div " - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ">\n (" - + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.only : depth0),{"name":"each","hash":{},"fn":container.program(8, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + " only)\n</div>\n"; -},"8":function(container,depth0,helpers,partials,data) { + return " <div " + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ">\n (" + + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.only : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + " only)\n </div>\n"; +},"10":function(container,depth0,helpers,partials,data) { var stack1; return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : "") - + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(9, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(11, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n"; -},"9":function(container,depth0,helpers,partials,data) { - return ", "; },"11":function(container,depth0,helpers,partials,data) { + return ", "; +},"13":function(container,depth0,helpers,partials,data) { var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); - return "<ul " - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(12, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + return " <ul " + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(14, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ">\n" - + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(14, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + "</ul>\n"; -},"12":function(container,depth0,helpers,partials,data) { - return "class=\"compact-glossary\""; + + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + " </ul>\n"; },"14":function(container,depth0,helpers,partials,data) { + return "class=\"compact-glossary\""; +},"16":function(container,depth0,helpers,partials,data) { var stack1, helper, options, buffer = - " <li><span class=\"glossary-item\">"; - stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); + " <li><span class=\"glossary-item\">"; + stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(17, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { buffer += stack1; } return buffer + "</span></li>\n"; -},"15":function(container,depth0,helpers,partials,data) { - return container.escapeExpression(container.lambda(depth0, depth0)); },"17":function(container,depth0,helpers,partials,data) { + return container.escapeExpression(container.lambda(depth0, depth0)); +},"19":function(container,depth0,helpers,partials,data) { var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer = - "<div class=\"glossary-item " - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(18, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " <div class=\"glossary-item " + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.compactGlossaries : depth0),{"name":"if","hash":{},"fn":container.program(20, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\">"; - stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(20, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper)); + stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(22, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper)); if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { buffer += stack1; } return buffer + "</div>\n"; -},"18":function(container,depth0,helpers,partials,data) { - return "compact-glossary"; },"20":function(container,depth0,helpers,partials,data) { + return "compact-glossary"; +},"22":function(container,depth0,helpers,partials,data) { var stack1; return container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0)); -},"22":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"24":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); return "<div class=\"entry\" data-type=\"term\">\n <div class=\"actions\">\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(23, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"unless","hash":{},"fn":container.program(25, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(28, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(25, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"unless","hash":{},"fn":container.program(27, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(30, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " <img src=\"/mixed/img/entry-current.svg\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n </div>\n\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(30, data, 0, blockParams, depths),"inverse":container.program(45, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.program(47, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + "\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(49, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(50, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(53, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(54, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n <div class=\"glossary\">\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(56, data, 0, blockParams, depths),"inverse":container.program(62, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(63, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + " </div>\n\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(65, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(66, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</div>\n"; -},"23":function(container,depth0,helpers,partials,data) { - return " <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.svg\" title=\"View added note (Alt + V)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kanji\"><img src=\"/mixed/img/add-term-kanji.svg\" title=\"Add expression (Alt + E)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kana\"><img src=\"/mixed/img/add-term-kana.svg\" title=\"Add reading (Alt + R)\" alt></a>\n"; },"25":function(container,depth0,helpers,partials,data) { + return " <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.svg\" title=\"View added note (Alt + V)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kanji\"><img src=\"/mixed/img/add-term-kanji.svg\" title=\"Add expression (Alt + E)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kana\"><img src=\"/mixed/img/add-term-kana.svg\" title=\"Add reading (Alt + R)\" alt></a>\n"; +},"27":function(container,depth0,helpers,partials,data) { var stack1; - return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(26, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"26":function(container,depth0,helpers,partials,data) { - return " <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio (Alt + P)\" alt></a>\n"; + return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(28, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); },"28":function(container,depth0,helpers,partials,data) { + return " <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio (Alt + P)\" alt></a>\n"; +},"30":function(container,depth0,helpers,partials,data) { return " <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n"; -},"30":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"32":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; - return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.expressions : depth0),{"name":"each","hash":{},"fn":container.program(31, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"31":function(container,depth0,helpers,partials,data,blockParams,depths) { + return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.expressions : depth0),{"name":"each","hash":{},"fn":container.program(33, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"33":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", buffer = "<div class=\"expression\"><span class=\"expression-" + container.escapeExpression(((helper = (helper = helpers.termFrequency || (depth0 != null ? depth0.termFrequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"termFrequency","hash":{},"data":data}) : helper))) + "\">"; - stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper)); + stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(34, data, 0, blockParams, depths),"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 + "</span><div class=\"peek-wrapper\">" - + ((stack1 = helpers["if"].call(alias1,(depths[1] != null ? depths[1].playback : depths[1]),{"name":"if","hash":{},"fn":container.program(35, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(40, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depths[1] != null ? depths[1].playback : depths[1]),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(39, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(42, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</div><span class=\"" - + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(43, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(45, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\">、</span></div>"; -},"32":function(container,depth0,helpers,partials,data) { +},"34":function(container,depth0,helpers,partials,data) { var stack1, helper, options; - stack1 = ((helper = (helper = helpers.furigana || (depth0 != null ? depth0.furigana : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"furigana","hash":{},"fn":container.program(33, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); + stack1 = ((helper = (helper = helpers.furigana || (depth0 != null ? depth0.furigana : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"furigana","hash":{},"fn":container.program(35, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); if (!helpers.furigana) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { return stack1; } else { return ''; } -},"33":function(container,depth0,helpers,partials,data) { +},"35":function(container,depth0,helpers,partials,data) { var stack1; return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : ""); -},"35":function(container,depth0,helpers,partials,data) { - return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio\" alt></a>"; },"37":function(container,depth0,helpers,partials,data) { + return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio\" alt></a>"; +},"39":function(container,depth0,helpers,partials,data) { var stack1; return "<div class=\"tags\">" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(38, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(40, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</div>"; -},"38":function(container,depth0,helpers,partials,data) { +},"40":function(container,depth0,helpers,partials,data) { var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; return " <span class=\"label label-default tag-" @@ -359,13 +368,13 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia + "\">" + 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"; -},"40":function(container,depth0,helpers,partials,data) { +},"42":function(container,depth0,helpers,partials,data) { var stack1; return "<div class=\"frequencies\">" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(41, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(43, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</div>"; -},"41":function(container,depth0,helpers,partials,data) { +},"43":function(container,depth0,helpers,partials,data) { var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; return " <span class=\"label label-default tag-frequency\">" @@ -373,55 +382,45 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia + ":" + alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper))) + "</span>\n"; -},"43":function(container,depth0,helpers,partials,data) { - return "invisible"; },"45":function(container,depth0,helpers,partials,data) { + return "invisible"; +},"47":function(container,depth0,helpers,partials,data) { var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer = " <div class=\"expression\">"; - stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(32, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper)); + stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(34, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper)); if (!helpers.kanjiLinks) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { buffer += stack1; } return buffer + "</div>\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(46, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"46":function(container,depth0,helpers,partials,data) { + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(48, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"48":function(container,depth0,helpers,partials,data) { var stack1; return " <div style=\"display: inline-block;\">\n" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(47, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </div>\n"; -},"47":function(container,depth0,helpers,partials,data) { - var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; - - return " <span class=\"label label-default 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"; -},"49":function(container,depth0,helpers,partials,data) { +},"50":function(container,depth0,helpers,partials,data) { var stack1; return " <div class=\"reasons\">\n" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(50, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(51, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </div>\n"; -},"50":function(container,depth0,helpers,partials,data) { +},"51":function(container,depth0,helpers,partials,data) { var stack1; return " <span class=\"reasons\">" + container.escapeExpression(container.lambda(depth0, depth0)) + "</span> " - + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(51, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(52, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n"; -},"51":function(container,depth0,helpers,partials,data) { +},"52":function(container,depth0,helpers,partials,data) { return "«"; -},"53":function(container,depth0,helpers,partials,data) { +},"54":function(container,depth0,helpers,partials,data) { var stack1; return " <div>\n" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(54, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(55, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </div>\n"; -},"54":function(container,depth0,helpers,partials,data) { +},"55":function(container,depth0,helpers,partials,data) { var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; return " <span class=\"label label-default tag-frequency\">" @@ -429,67 +428,67 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia + ":" + alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper))) + "</span>\n"; -},"56":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"57":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; - return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(60, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); -},"57":function(container,depth0,helpers,partials,data,blockParams,depths) { + return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(58, data, 0, blockParams, depths),"inverse":container.program(61, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +},"58":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; return " <ol>\n" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(58, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(59, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </ol>\n"; -},"58":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"59":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; return " <li>" + ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","hash":{"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") + "</li>\n"; -},"60":function(container,depth0,helpers,partials,data) { +},"61":function(container,depth0,helpers,partials,data) { var stack1; return ((stack1 = container.invokePartial(partials.definition,((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["0"] : stack1),{"name":"definition","hash":{"compactGlossaries":(depth0 != null ? depth0.compactGlossaries : depth0)},"data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"62":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"63":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; - return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(56, data, 0, blockParams, depths),"inverse":container.program(63, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); -},"63":function(container,depth0,helpers,partials,data) { + return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(64, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +},"64":function(container,depth0,helpers,partials,data) { var stack1; return ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","hash":{"compactGlossaries":(depth0 != null ? depth0.compactGlossaries : depth0)},"data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") + " "; -},"65":function(container,depth0,helpers,partials,data) { +},"66":function(container,depth0,helpers,partials,data) { var stack1, helper, options, buffer = " <pre>"; - stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(33, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); + stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(35, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); if (!helpers.dumpObject) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { buffer += stack1; } return buffer + "</pre>\n"; -},"67":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"68":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; - return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(68, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"68":function(container,depth0,helpers,partials,data,blockParams,depths) { + return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(69, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"69":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; - return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(69, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(70, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n" + ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"source":(depths[1] != null ? depths[1].source : depths[1]),"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1]),"playback":(depths[1] != null ? depths[1].playback : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"merged":(depths[1] != null ? depths[1].merged : depths[1]),"grouped":(depths[1] != null ? depths[1].grouped : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"69":function(container,depth0,helpers,partials,data) { +},"70":function(container,depth0,helpers,partials,data) { return "<hr>"; -},"71":function(container,depth0,helpers,partials,data) { +},"72":function(container,depth0,helpers,partials,data) { return "<p class=\"note\">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 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(67, data, 0, blockParams, depths),"inverse":container.program(71, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); + + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(68, data, 0, blockParams, depths),"inverse":container.program(72, 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(22, data, 0, blockParams, depths),"inverse":container.noop,"args":["term"],"data":data}) || fn; + fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(24, data, 0, blockParams, depths),"inverse":container.noop,"args":["term"],"data":data}) || fn; return fn; } diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 601ee30c..ee012d96 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -21,6 +21,7 @@ class Translator { constructor() { this.database = null; this.deinflector = null; + this.tagCache = {}; } async prepare() { @@ -36,6 +37,11 @@ class Translator { } } + async purgeDatabase() { + this.tagCache = {}; + await this.database.purge(); + } + async findTermsGrouped(text, dictionaries, alphanumeric, options) { const titles = Object.keys(dictionaries); const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric); @@ -52,94 +58,121 @@ class Translator { return {length, definitions: definitionsGrouped}; } - async findTermsMerged(text, dictionaries, alphanumeric, options) { - const secondarySearchTitles = Object.keys(options.dictionaries).filter(dict => options.dictionaries[dict].allowSecondarySearches); - const titles = Object.keys(dictionaries); - const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric); + async getSequencedDefinitions(definitions, mainDictionary) { + const definitionsBySequence = dictTermsMergeBySequence(definitions, mainDictionary); + const defaultDefinitions = definitionsBySequence['-1']; - const definitionsBySequence = dictTermsMergeBySequence(definitions, options.general.mainDictionary); + const sequenceList = Object.keys(definitionsBySequence).map(v => Number(v)).filter(v => v >= 0); + const sequencedDefinitions = sequenceList.map((key) => ({ + definitions: definitionsBySequence[key], + rawDefinitions: [] + })); - const definitionsMerged = []; - const mergedByTermIndices = new Set(); - for (const sequence in definitionsBySequence) { - if (sequence < 0) { - continue; - } + for (const definition of await this.database.findTermsBySequenceBulk(sequenceList, mainDictionary)) { + sequencedDefinitions[definition.index].rawDefinitions.push(definition); + } - const result = definitionsBySequence[sequence]; + return {sequencedDefinitions, defaultDefinitions}; + } - const rawDefinitionsBySequence = await this.database.findTermsBySequence(Number(sequence), options.general.mainDictionary); + async getMergedSecondarySearchResults(text, expressionsMap, secondarySearchTitles) { + if (secondarySearchTitles.length === 0) { + return []; + } - for (const definition of rawDefinitionsBySequence) { - const definitionTags = await this.expandTags(definition.definitionTags, definition.dictionary); - definitionTags.push(dictTagBuildSource(definition.dictionary)); - definition.definitionTags = definitionTags; - const termTags = await this.expandTags(definition.termTags, definition.dictionary); - definition.termTags = termTags; + const expressionList = []; + const readingList = []; + for (const expression of expressionsMap.keys()) { + if (expression === text) { continue; } + for (const reading of expressionsMap.get(expression).keys()) { + expressionList.push(expression); + readingList.push(reading); } + } - const definitionsByGloss = dictTermsMergeByGloss(result, rawDefinitionsBySequence); - - const secondarySearchResults = []; - if (secondarySearchTitles.length > 0) { - for (const expression of result.expressions.keys()) { - if (expression === text) { - continue; - } - - for (const reading of result.expressions.get(expression).keys()) { - for (const definition of await this.database.findTermsExact(expression, reading, secondarySearchTitles)) { - const definitionTags = await this.expandTags(definition.definitionTags, definition.dictionary); - definitionTags.push(dictTagBuildSource(definition.dictionary)); - definition.definitionTags = definitionTags; - const termTags = await this.expandTags(definition.termTags, definition.dictionary); - definition.termTags = termTags; - secondarySearchResults.push(definition); - } - } - } - } + const definitions = await this.database.findTermsExactBulk(expressionList, readingList, secondarySearchTitles); + for (const definition of definitions) { + const definitionTags = await this.expandTags(definition.definitionTags, definition.dictionary); + definitionTags.push(dictTagBuildSource(definition.dictionary)); + definition.definitionTags = definitionTags; + const termTags = await this.expandTags(definition.termTags, definition.dictionary); + definition.termTags = termTags; + } - dictTermsMergeByGloss(result, definitionsBySequence['-1'].concat(secondarySearchResults), definitionsByGloss, mergedByTermIndices); + if (definitions.length > 1) { + definitions.sort((a, b) => a.index - b.index); + } - for (const gloss in definitionsByGloss) { - const definition = definitionsByGloss[gloss]; - dictTagsSort(definition.definitionTags); - result.definitions.push(definition); - } + return definitions; + } - dictTermsSort(result.definitions, dictionaries); - - const expressions = []; - for (const expression of result.expressions.keys()) { - for (const reading of result.expressions.get(expression).keys()) { - const termTags = result.expressions.get(expression).get(reading); - expressions.push({ - expression: expression, - reading: reading, - termTags: dictTagsSort(termTags), - termFrequency: (score => { - if (score > 0) { - return 'popular'; - } else if (score < 0) { - return 'rare'; - } else { - return 'normal'; - } - })(termTags.map(tag => tag.score).reduce((p, v) => p + v, 0)) - }); - } + async getMergedDefinition(text, dictionaries, sequencedDefinition, defaultDefinitions, secondarySearchTitles, mergedByTermIndices) { + const result = sequencedDefinition.definitions; + const rawDefinitionsBySequence = sequencedDefinition.rawDefinitions; + + for (const definition of rawDefinitionsBySequence) { + const definitionTags = await this.expandTags(definition.definitionTags, definition.dictionary); + definitionTags.push(dictTagBuildSource(definition.dictionary)); + definition.definitionTags = definitionTags; + const termTags = await this.expandTags(definition.termTags, definition.dictionary); + definition.termTags = termTags; + } + + const definitionsByGloss = dictTermsMergeByGloss(result, rawDefinitionsBySequence); + const secondarySearchResults = await this.getMergedSecondarySearchResults(text, result.expressions, secondarySearchTitles); + + dictTermsMergeByGloss(result, defaultDefinitions.concat(secondarySearchResults), definitionsByGloss, mergedByTermIndices); + + for (const gloss in definitionsByGloss) { + const definition = definitionsByGloss[gloss]; + dictTagsSort(definition.definitionTags); + result.definitions.push(definition); + } + + dictTermsSort(result.definitions, dictionaries); + + const expressions = []; + for (const expression of result.expressions.keys()) { + for (const reading of result.expressions.get(expression).keys()) { + const termTags = result.expressions.get(expression).get(reading); + const score = termTags.map(tag => tag.score).reduce((p, v) => p + v, 0); + expressions.push({ + expression: expression, + reading: reading, + termTags: dictTagsSort(termTags), + termFrequency: Translator.scoreToTermFrequency(score) + }); } + } - result.expressions = expressions; + result.expressions = expressions; + result.expression = Array.from(result.expression); + result.reading = Array.from(result.reading); - result.expression = Array.from(result.expression); - result.reading = Array.from(result.reading); + return result; + } + + async findTermsMerged(text, dictionaries, alphanumeric, options) { + const secondarySearchTitles = Object.keys(options.dictionaries).filter(dict => options.dictionaries[dict].allowSecondarySearches); + const titles = Object.keys(dictionaries); + const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric); + const {sequencedDefinitions, defaultDefinitions} = await this.getSequencedDefinitions(definitions, options.general.mainDictionary); + const definitionsMerged = []; + const mergedByTermIndices = new Set(); + for (const sequencedDefinition of sequencedDefinitions) { + const result = await this.getMergedDefinition( + text, + dictionaries, + sequencedDefinition, + defaultDefinitions, + secondarySearchTitles, + mergedByTermIndices + ); definitionsMerged.push(result); } - const strayDefinitions = definitionsBySequence['-1'].filter((definition, index) => !mergedByTermIndices.has(index)); + const strayDefinitions = defaultDefinitions.filter((definition, index) => !mergedByTermIndices.has(index)); for (const groupedDefinition of dictTermsGroup(strayDefinitions, dictionaries)) { groupedDefinition.expressions = [{expression: groupedDefinition.expression, reading: groupedDefinition.reading}]; definitionsMerged.push(groupedDefinition); @@ -277,33 +310,44 @@ class Translator { } async findKanji(text, dictionaries) { - let definitions = []; - const processed = {}; const titles = Object.keys(dictionaries); + const kanjiUnique = {}; + const kanjiList = []; for (const c of text) { - if (!processed[c]) { - definitions.push(...await this.database.findKanji(c, titles)); - processed[c] = true; + if (!kanjiUnique.hasOwnProperty(c)) { + kanjiList.push(c); + kanjiUnique[c] = true; } } + const definitions = await this.database.findKanjiBulk(kanjiList, titles); + if (definitions.length === 0) { + return definitions; + } + + if (definitions.length > 1) { + definitions.sort((a, b) => a.index - b.index); + } + + const kanjiList2 = []; for (const definition of definitions) { + kanjiList2.push(definition.character); + const tags = await this.expandTags(definition.tags, definition.dictionary); tags.push(dictTagBuildSource(definition.dictionary)); definition.tags = dictTagsSort(tags); definition.stats = await this.expandStats(definition.stats, definition.dictionary); - definition.frequencies = []; - for (const meta of await this.database.findKanjiMeta(definition.character, titles)) { - if (meta.mode === 'freq') { - definition.frequencies.push({ - character: meta.character, - frequency: meta.data, - dictionary: meta.dictionary - }); - } - } + } + + for (const meta of await this.database.findKanjiMetaBulk(kanjiList2, titles)) { + if (meta.mode !== 'freq') { continue; } + definitions[meta.index].frequencies.push({ + character: meta.character, + frequency: meta.data, + dictionary: meta.dictionary + }); } return definitions; @@ -359,56 +403,76 @@ class Translator { } async expandTags(names, title) { - const tags = []; - for (const name of names) { - const base = Translator.getNameBase(name); - let meta = this.database.findTagForTitleCached(base, title); - if (typeof meta === 'undefined') { - meta = await this.database.findTagForTitle(base, title); - } - - const tag = Object.assign({}, meta !== null ? meta : {}, {name}); - - tags.push(dictTagSanitize(tag)); - } - - return tags; + const tagMetaList = await this.getTagMetaList(names, title); + return tagMetaList.map((meta, index) => { + const name = names[index]; + const tag = dictTagSanitize(Object.assign({}, meta !== null ? meta : {}, {name})); + return dictTagSanitize(tag); + }); } async expandStats(items, title) { - const stats = {}; - for (const name in items) { - const base = Translator.getNameBase(name); - let meta = this.database.findTagForTitleCached(base, title); - if (typeof meta === 'undefined') { - meta = await this.database.findTagForTitle(base, title); - if (meta === null) { - continue; - } - } + const names = Object.keys(items); + const tagMetaList = await this.getTagMetaList(names, title); - const group = stats[meta.category] = stats[meta.category] || []; + const stats = {}; + for (let i = 0; i < names.length; ++i) { + const name = names[i]; + const meta = tagMetaList[i]; + if (meta === null) { continue; } + + const category = meta.category; + const group = ( + stats.hasOwnProperty(category) ? + stats[category] : + (stats[category] = []) + ); const stat = Object.assign({}, meta, {name, value: items[name]}); - group.push(dictTagSanitize(stat)); } + const sortCompare = (a, b) => a.notes - b.notes; for (const category in stats) { - stats[category].sort((a, b) => { - if (a.notes < b.notes) { - return -1; - } else if (a.notes > b.notes) { - return 1; - } else { - return 0; - } - }); + stats[category].sort(sortCompare); } return stats; } + async getTagMetaList(names, title) { + const tagMetaList = []; + const cache = ( + this.tagCache.hasOwnProperty(title) ? + this.tagCache[title] : + (this.tagCache[title] = {}) + ); + + for (const name of names) { + const base = Translator.getNameBase(name); + + if (cache.hasOwnProperty(base)) { + tagMetaList.push(cache[base]); + } else { + const tagMeta = await this.database.findTagForTitle(base, title); + cache[base] = tagMeta; + tagMetaList.push(tagMeta); + } + } + + return tagMetaList; + } + + static scoreToTermFrequency(score) { + if (score > 0) { + return 'popular'; + } else if (score < 0) { + return 'rare'; + } else { + return 'normal'; + } + } + static getNameBase(name) { const pos = name.indexOf(':'); return (pos >= 0 ? name.substr(0, pos) : name); diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 73a8396f..1ca0833b 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -89,7 +89,7 @@ function utilAnkiGetModelFieldNames(modelName) { } function utilDatabasePurge() { - return utilBackend().translator.database.purge(); + return utilBackend().translator.purgeDatabase(); } async function utilDatabaseImport(data, progress, exceptions) { diff --git a/ext/bg/lang/deinflect.json b/ext/bg/lang/deinflect.json index 682093e1..a0b6baa1 100644 --- a/ext/bg/lang/deinflect.json +++ b/ext/bg/lang/deinflect.json @@ -3671,7 +3671,7 @@ "kanaIn": "ておる", "kanaOut": "て", "rulesIn": [ - "v1" + "v5" ], "rulesOut": [ "iru" @@ -3701,7 +3701,7 @@ "kanaIn": "でおる", "kanaOut": "で", "rulesIn": [ - "v1" + "v5" ], "rulesOut": [ "iru" @@ -3711,7 +3711,7 @@ "kanaIn": "とる", "kanaOut": "て", "rulesIn": [ - "v1" + "v5" ], "rulesOut": [ "iru" diff --git a/ext/bg/search.html b/ext/bg/search.html index 3284ed43..9d28b358 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" data-yomichan-page="search"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1" /> @@ -7,6 +7,8 @@ <link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css"> <link rel="stylesheet" type="text/css" href="/mixed/css/display.css"> + <link rel="stylesheet" type="text/css" href="/mixed/css/display-default.css" data-yomichan-theme-name="default"> + <link rel="stylesheet alternate" type="text/css" href="/mixed/css/display-dark.css" data-yomichan-theme-name="dark"> </head> <body> <div class="container"> diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html new file mode 100644 index 00000000..07caa271 --- /dev/null +++ b/ext/bg/settings-popup-preview.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <title>Yomichan Popup Preview</title> + <link rel="stylesheet" type="text/css" href="/fg/css/client.css" id="client-css"> + <style> + html { + transition: background-color 0.25s linear 0s, color 0.25s linear 0s; + color: #333333; + } + html.dark { + background-color: #1e1e1e; + color: #d4d4d4; + } + html, body { + margin: 0; + padding: 0; + border: 0; + overflow: hidden; + width: 100%; + height: 100%; + font-family: "Helvetica Neue", Helvetica, Arial ,sans-serif; + font-size: 14px; + } + iframe.yomichan-float { + resize: none; + } + .vertical-align-outer { + width: 100%; + height: 100%; + white-space: nowrap; + } + .vertical-align-outer::before { + content: ""; + display: inline-block; + vertical-align: middle; + width: 0; + height: 100%; + } + .vertical-align-inner { + display: inline-block; + vertical-align: middle; + white-space: normal; + width: 100%; + } + .horizontal-size { + max-width: 400px; + padding: 15px; + margin: 0 auto; + } + .example-text-container { + font-size: 24px; + line-height: 1.25em; + height: 1.25em; + } + .popup-placeholder { + height: 250px; + padding-top: 10px; + border: 1px solid rgba(0, 0, 0, 0); + } + .placeholder-info { + visibility: hidden; + opacity: 0; + transition: opacity 0.5s linear 0s, visibility 0s linear 0.5s; + } + .placeholder-info.placeholder-info-visible { + visibility: visible; + opacity: 1; + transition: opacity 0.5s linear 0s, visibility 0s linear 0s; + } + + .options { + float: right; + font-size: 14px; + line-height: 30px; + } + .theme-button { + display: inline-block; + margin-left: 0.5em; + text-decoration: none; + cursor: pointer; + white-space: nowrap; + line-height: 0; + } + .theme-button>input { + vertical-align: middle; + margin: 0 0.25em 0 0; + padding: 0; + } + .theme-button>span { + vertical-align: middle; + } + .theme-button:hover>span { + text-decoration: underline; + } + </style> + </head> + <body> + <div class="vertical-align-outer"><div class="vertical-align-inner"><div class="horizontal-size"> + <div class="example-text-container"> + <div class="options"><label class="theme-button"><input type="checkbox" id="theme-dark-checkbox" /><span>dark</span></label></div> + <span id="example-text">読め</span> + </div> + <div class="popup-placeholder"> + <div class="vertical-align-outer"><div class="vertical-align-inner placeholder-info"> + This page uses the dictionaries you have installed in order to show a preview. + If you see this message, make sure you have a dictionary installed. + </div></div> + </div> + </div></div></div> + + <script src="/mixed/js/extension.js"></script> + <script src="/fg/js/api.js"></script> + <script src="/fg/js/document.js"></script> + <script src="/fg/js/frontend-api-receiver.js"></script> + <script src="/fg/js/popup.js"></script> + <script src="/fg/js/source.js"></script> + <script src="/fg/js/util.js"></script> + <script src="/fg/js/popup-proxy-host.js"></script> + <script src="/fg/js/frontend.js"></script> + <script src="/bg/js/settings-popup-preview.js"></script> + </body> +</html> diff --git a/ext/bg/settings.html b/ext/bg/settings.html index e4710283..9b1c4366 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -231,9 +231,49 @@ </div> </div> + <div class="form-group"> + <div class="row"> + <div class="col-xs-6"> + <label for="popup-theme">Popup theme</label> + <select class="form-control" id="popup-theme"> + <option value="default">Light</option> + <option value="dark">Dark</option> + </select> + </div> + <div class="col-xs-6"> + <label for="popup-outer-theme">Popup shadow theme</label> + <select class="form-control" id="popup-outer-theme"> + <option value="auto">Auto-detect</option> + <option value="default">Light</option> + <option value="dark">Dark</option> + </select> + </div> + </div> + </div> + <div class="form-group options-advanced"> - <label for="custom-popup-css">Custom popup CSS</label> - <div><textarea autocomplete="off" spellcheck="false" wrap="soft" id="custom-popup-css" class="form-control"></textarea></div> + <div class="row"> + <div class="col-xs-6"> + <label for="custom-popup-css">Custom popup CSS</label> + <div><textarea autocomplete="off" spellcheck="false" wrap="soft" id="custom-popup-css" class="form-control"></textarea></div> + </div> + <div class="col-xs-6"> + <label for="custom-popup-outer-css">Custom popup outer CSS</label> + <div><textarea autocomplete="off" spellcheck="false" wrap="soft" id="custom-popup-outer-css" class="form-control" placeholder="iframe.yomichan-float { /*styles*/ }"></textarea></div> + </div> + </div> + </div> + + <div class="form-group ignore-form-changes" style="display: none;" id="settings-popup-preview-settings"> + <label for="settings-popup-preview-text">Popup preview text</label> + <input type="text" id="settings-popup-preview-text" class="form-control" value="読め"> + </div> + + <div class="form-group ignore-form-changes"> + <div id="settings-popup-preview-button-container"> + <button class="btn btn-default" id="settings-popup-preview-button">Show popup preview</button> + </div> + <div id="settings-popup-preview-container"></div> </div> </div> @@ -253,6 +293,16 @@ <input type="number" min="0" max="100" id="audio-playback-volume" class="form-control"> </div> + <div class="form-group" style="display: none;" id="text-to-speech-voice-container"> + <label for="text-to-speech-voice">Text-to-speech voice</label> + <div class="input-group"> + <select class="form-control" id="text-to-speech-voice"></select> + <div class="input-group-btn"> + <button class="btn btn-default" id="text-to-speech-voice-test" title="Test voice" data-speech-text="よみちゃん"><span class="glyphicon glyphicon-volume-up"></span></button> + </div> + </div> + </div> + <div class="form-group options-advanced"> <label for="audio-custom-source">Custom audio source <span class="label-light">(URL)</span></label> <input type="text" id="audio-custom-source" class="form-control" placeholder="Example: http://localhost/audio.mp3?expression={expression}&reading={reading}"> @@ -269,8 +319,10 @@ <div class="input-group-addon audio-source-prefix"></div> <select class="form-control audio-source-select"> <option value="jpod101">JapanesePod101</option> - <option value="jpod101-alternate">JapanesePod101 (alternate)</option> + <option value="jpod101-alternate">JapanesePod101 (Alternate)</option> <option value="jisho">Jisho.org</option> + <option value="text-to-speech">Text-to-speech</option> + <option value="text-to-speech-reading">Text-to-speech (Kana reading)</option> <option value="custom">Custom</option> </select> <div class="input-group-btn"><button class="btn btn-danger audio-source-remove" title="Remove"><span class="glyphicon glyphicon-remove"></span></button></div> @@ -395,16 +447,27 @@ </div> </div> - <div id="storage-info" class="storage-hidden"> + <div id="storage-info"> <div> <img src="/mixed/img/spinner.gif" class="pull-right" id="storage-spinner" /> <h3>Storage</h3> </div> - <div id="storage-use" class="storage-hidden"> + <div id="storage-persist-info" class="storage-hidden"> <p class="help-block"> + Web browsers may sometimes clear stored data if the device is running low on storage space. + This can result in the stored dictionary data being deleted unexpectedly, causing Yomichan to stop working for no apparent reason. + In order to prevent this, persistent storage must be enable by clicking the "Persistent Storage" button below. + </p> + </div> + + <div id="storage-use" class="storage-hidden"> + <p class="help-block storage-hidden" id="storage-use-finite"> Yomichan is using approximately <strong id="storage-usage"></strong> of <strong id="storage-quota"></strong>. </p> + <p class="help-block storage-hidden" id="storage-use-infinite"> + Yomichan is permitted <strong>unlimited storage</strong>. + </p> </div> <div id="storage-error" class="storage-hidden"> @@ -425,8 +488,23 @@ </div></div> <div> - <button class="btn btn-default" id="storage-refresh">Refresh</button> + <button class="btn btn-default" id="storage-refresh"><span class="btn-inner-middle">Refresh</span></button> + <button class="btn btn-default storage-hidden ignore-form-changes" id="storage-persist-button"><span class="storage-button-inner"><input type="checkbox" class="btn-inner-middle storage-button-checkbox" id="storage-persist-button-checkbox" readonly /><span class="btn-inner-middle">Persistent Storage</span></span></button> </div> + + <p></p> + + <div data-show-for-browser="firefox-mobile"><div class="alert alert-warning storage-persist-fail-warning storage-hidden"> + <p>It may not be possible to enable Persistent Storage on Firefox for Android.</p> + </div></div> + + <div data-show-for-browser="chrome"><div class="alert alert-warning storage-persist-fail-warning storage-hidden"> + <p> + It may not be possible to enable Persistent Storage on Chrome-based browsers. + However, the Yomichan extension has permission for unlimited storage which should + prevent Chrome from deleting data.<sup><a href="https://bugs.chromium.org/p/chromium/issues/detail?id=680392#c15" target="_blank" rel="noopener">[1]</a></sup> + </p> + </div></div> </div> <div> @@ -441,13 +519,21 @@ <a href="https://foosoft.net/projects/anki-connect/" target="_blank" rel="noopener">AnkiConnect</a> plugin. </p> - <div class="alert alert-danger" id="anki-error"></div> - <div class="checkbox"> <label><input type="checkbox" id="anki-enable"> Enable Anki integration</label> </div> <div id="anki-general"> + <div data-show-for-operating-system="mac"> + <div class="alert alert-warning" id="anki-mac-warning"> + Notice for Mac OS X users: + If Yomichan has issues connecting to AnkiConnect, you may have to tweak some system settings. + See <a href="https://foosoft.net/projects/anki-connect/#notes-for-mac-os-x-users" target="_blank" rel="noopener">this link</a> for more details. + </div> + </div> + + <div class="alert alert-danger" id="anki-error"></div> + <div class="form-group"> <label for="card-tags">Card tags <span class="label-light">(comma or space separated)</span></label> <input type="text" id="card-tags" class="form-control"> @@ -591,6 +677,7 @@ <script src="/mixed/lib/jquery.min.js"></script> <script src="/mixed/lib/bootstrap/js/bootstrap.min.js"></script> <script src="/mixed/lib/handlebars.min.js"></script> + <script src="/mixed/lib/wanakana.min.js"></script> <script src="/mixed/js/extension.js"></script> @@ -605,6 +692,7 @@ <script src="/bg/js/profile-conditions.js"></script> <script src="/bg/js/templates.js"></script> <script src="/bg/js/util.js"></script> + <script src="/mixed/js/audio.js"></script> <script src="/bg/js/settings-profiles.js"></script> <script src="/bg/js/settings.js"></script> |