diff options
-rw-r--r-- | README.md | 86 | ||||
-rw-r--r-- | ext/bg/js/api.js | 8 | ||||
-rw-r--r-- | ext/bg/js/audio.js | 9 | ||||
-rw-r--r-- | ext/bg/js/database.js | 85 | ||||
-rw-r--r-- | ext/bg/js/dictionary.js | 147 | ||||
-rw-r--r-- | ext/bg/js/options.js | 115 | ||||
-rw-r--r-- | ext/bg/js/settings.js | 83 | ||||
-rw-r--r-- | ext/bg/js/templates.js | 254 | ||||
-rw-r--r-- | ext/bg/js/translator.js | 144 | ||||
-rw-r--r-- | ext/bg/js/util.js | 45 | ||||
-rw-r--r-- | ext/bg/settings.html | 28 | ||||
-rw-r--r-- | ext/fg/js/document.js | 4 | ||||
-rw-r--r-- | ext/manifest.json | 2 | ||||
-rw-r--r-- | ext/mixed/css/display.css | 72 | ||||
-rw-r--r-- | ext/mixed/js/display.js | 38 | ||||
-rw-r--r-- | tmpl/dictionary.html | 6 | ||||
-rw-r--r-- | tmpl/terms.html | 74 |
17 files changed, 1006 insertions, 194 deletions
@@ -258,52 +258,72 @@ versions packaged. ## Frequently Asked Questions ## -* **Why does the Kanji results page display "No data found" for several fields?** +**I'm having problems importing dictionaries in Firefox, what do I do?** - You are using data from the KANJIDIC dictionary that was exported for an earlier version of Yomichan. It does not - contain the additional information which newer versions of Yofomichan expect. Unfortunately, since major browser - implementations of IndexedDB do not provide reliable means for selective bulk data deletion, you will need purge - your database and install the latest version of the KANJIDIC to see additional information about characters. +Yomichan uses the cross-browser IndexedDB system for storing imported dictionary data into your user profile. Although +everything "just works" in Chrome, depending on settings, Firefox users can run into problems due browser bugs. +Yomichan catches errors and tries to offer suggestions about how to work around Firefox issues, but in general at least +one of the following solutions should work for you: -* **Can I still create cards without HTML formatting? The option for it is gone!** +* Make sure you have cookies enabled. It appears that disabling them also disables IndexedDB for some reason. You + can still have cookies be disabled on other sites; just make sure to add the Yomichan extension to the whitelist of + whatever tool you are using to restrict cookies. You can get the extension "URL" by looking at the address bar when + you have the search page open. +* Make sure that you have sufficient disk space available on the drive Firefox uses to store your user profile. + Firefox limits the amount of space that can be used by IndexedDB to a small fraction of the disk space actually + available on your computer. +* Make sure that you have history set to "Remember history" enabled in your privacy settings. When this option is + set to "Never remember history", IndexedDB access is once again disabled for an inexplicable reason. +* As a last resort, try using the [Refresh Firefox](https://support.mozilla.org/en-US/kb/reset-preferences-fix-problems) + feature to reset your user profile. It appears that the Firefox profile system can corrupt itself preventing + IndexedDB from being accessible to Yomichan. - Developing Yomichan is a constant balance between including useful features and keeping complexity at a minimum. - With the new user-editable card template system, it is possible to create text-only cards without having to double - the number field of templates in the extension itself. If you would like to stop HTML tags from being added to your - cards, simply copy the contents of the <a href="dl/fields.txt">text-only field template</a> into the template box on - the Anki settings page (make sure you have the *Show advanced options* checkbox ticked), making sure to replace the - existing values. +**Why does the Kanji results page display "No data found" for several fields?** -* **Will you add support for online dictionaries?** +You are using data from the KANJIDIC dictionary that was exported for an earlier version of Yomichan. It does not +contain the additional information which newer versions of Yofomichan expect. Unfortunately, since major browser +implementations of IndexedDB do not provide reliable means for selective bulk data deletion, you will need purge +your database and install the latest version of the KANJIDIC to see additional information about characters. - Online dictionaries will never be implemented because it is impossible to support them in a robust way. In order to - perform Japanese deinflection, Yomichan must execute dozens of database queries per every single word. Factoring in - network latency and the fragility of web scraping, I do not believe that it is possible to realize a good user - experience. +**Can I still create cards without HTML formatting? The option for it is gone!** -* **Is it possible to use Yomichan with files saved locally on my computer with Chrome?** +Developing Yomichan is a constant balance between including useful features and keeping complexity at a minimum. +With the new user-editable card template system, it is possible to create text-only cards without having to double +the number field of templates in the extension itself. If you would like to stop HTML tags from being added to your +cards, simply copy the contents of the <a href="dl/fields.txt">text-only field template</a> into the template box on +the Anki settings page (make sure you have the *Show advanced options* checkbox ticked), making sure to replace the +existing values. - In order to use Yomichan with local files in Chrome, you must first tick the *Allow access to file URLs* checkbox - for Yomichan on the extensions page. Due to the restrictions placed on browser addons in the WebExtensions model, it - will likely never be possible to use Yomichan with PDF files. +**Will you add support for online dictionaries?** -* **Is it possible to delete individual dictionaries without purging the database?** +Online dictionaries will never be implemented because it is impossible to support them in a robust way. In order to +perform Japanese deinflection, Yomichan must execute dozens of database queries per every single word. Factoring in +network latency and the fragility of web scraping, I do not believe that it is possible to realize a good user +experience. - Although it is technically possible to purge specific dictionaries, due to the limitations of the underlying browser - IndexedDB system, this process is *extremely* slow. For example, it can take up to ten minutes to delete a single - moderately-sized term dictionary! Instead of including a borderline unusable feature in Yomichan, I have chosen to - disable dictionary deletion entirely. +**Is it possible to use Yomichan with files saved locally on my computer with Chrome?** -* **Why aren't EPWING dictionaries bundled with Yomichan?** +In order to use Yomichan with local files in Chrome, you must first tick the *Allow access to file URLs* checkbox +for Yomichan on the extensions page. Due to the restrictions placed on browser addons in the WebExtensions model, it +will likely never be possible to use Yomichan with PDF files. - The vast majority of EPWING dictionaries are proprietary, so unfortunately I am unable to legally include them in - this extension for copyright reasons. +**Is it possible to delete individual dictionaries without purging the database?** -* **When are you going to add support for $MYLANGUAGE?** +Although it is technically possible to purge specific dictionaries, due to the limitations of the underlying browser +IndexedDB system, this process is *extremely* slow. For example, it can take up to ten minutes to delete a single +moderately-sized term dictionary! Instead of including a borderline unusable feature in Yomichan, I have chosen to +disable dictionary deletion entirely. - Developing Yomichan requires a significant understanding of Japanese sentence structure and grammar. I have no time - to invest in learning yet another language; therefore other languages will not be supported. I will also not accept - pull request containing this functionality, as I will ultimately be the one maintaining your code. +**Why aren't EPWING dictionaries bundled with Yomichan?** + +The vast majority of EPWING dictionaries are proprietary, so unfortunately I am unable to legally include them in +this extension for copyright reasons. + +**When are you going to add support for $MYLANGUAGE?** + +Developing Yomichan requires a significant understanding of Japanese sentence structure and grammar. I have no time +to invest in learning yet another language; therefore other languages will not be supported. I will also not accept +pull request containing this functionality, as I will ultimately be the one maintaining your code. ## Screenshots ## diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index 9f65bb07..de3ad64e 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -29,9 +29,11 @@ async function apiTermsFind(text) { const options = utilBackend().options; const translator = utilBackend().translator; - const searcher = options.general.groupResults ? - translator.findTermsGrouped.bind(translator) : - translator.findTermsSplit.bind(translator); + const searcher = { + 'merge': translator.findTermsMerged, + 'split': translator.findTermsSplit, + 'group': translator.findTermsGrouped + }[options.general.resultOutputMode].bind(translator); const {definitions, length} = await searcher( text, diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index ce47490c..549288f5 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -140,8 +140,13 @@ async function audioInject(definition, fields, mode) { } try { - const url = await audioBuildUrl(definition, mode); - const filename = audioBuildFilename(definition); + let audioSourceDefinition = definition; + if (definition.hasOwnProperty('expressions')) { + audioSourceDefinition = definition.expressions[0]; + } + + const url = await audioBuildUrl(audioSourceDefinition, mode); + const filename = audioBuildFilename(audioSourceDefinition); if (url && filename) { definition.audio = {url, filename}; diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 6ceb3ec8..3c7f6aab 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -40,6 +40,9 @@ class Database { kanjiMeta: '++,dictionary,character', tagMeta: '++,dictionary,name' }); + this.db.version(4).stores({ + terms: '++id,dictionary,expression,reading,sequence' + }); await this.db.open(); } @@ -68,12 +71,66 @@ class Database { results.push({ expression: row.expression, reading: row.reading, - tags: dictFieldSplit(row.tags), + definitionTags: dictFieldSplit(row.definitionTags || row.tags || ''), + termTags: dictFieldSplit(row.termTags || ''), + rules: dictFieldSplit(row.rules), + glossary: row.glossary, + score: row.score, + dictionary: row.dictionary, + id: row.id, + sequence: typeof row.sequence === 'undefined' ? -1 : row.sequence + }); + } + }); + + return results; + } + + async findTermsExact(term, reading, titles) { + if (!this.db) { + throw 'Database not initialized'; + } + + const results = []; + await this.db.terms.where('expression').equals(term).each(row => { + if (row.reading === reading && titles.includes(row.dictionary)) { + results.push({ + expression: row.expression, + reading: row.reading, + definitionTags: dictFieldSplit(row.definitionTags || row.tags || ''), + termTags: dictFieldSplit(row.termTags || ''), rules: dictFieldSplit(row.rules), glossary: row.glossary, score: row.score, dictionary: row.dictionary, - id: row.id + id: row.id, + sequence: typeof row.sequence === 'undefined' ? -1 : row.sequence + }); + } + }); + + return results; + } + + async findTermsBySequence(sequence, mainDictionary) { + if (!this.db) { + throw 'Database not initialized'; + } + + const results = []; + await this.db.terms.where('sequence').equals(sequence).each(row => { + if (row.dictionary === mainDictionary) { + results.push({ + expression: row.expression, + reading: row.reading, + definitionTags: dictFieldSplit(row.definitionTags || row.tags || ''), + termTags: dictFieldSplit(row.termTags || ''), + rules: dictFieldSplit(row.rules), + glossary: row.glossary, + score: row.score, + dictionary: row.dictionary, + id: row.id, + sequence: typeof row.sequence === 'undefined' ? -1 : row.sequence }); } }); @@ -163,7 +220,7 @@ class Database { return result; } - async getTitles() { + async summarize() { if (this.db) { return this.db.dictionaries.toArray(); } else { @@ -177,7 +234,7 @@ class Database { } const indexDataLoaded = async summary => { - if (summary.version > 2) { + if (summary.version > 3) { throw 'Unsupported dictionary version'; } @@ -196,11 +253,11 @@ class Database { const rows = []; if (summary.version === 1) { - for (const [expression, reading, tags, rules, score, ...glossary] of entries) { + for (const [expression, reading, definitionTags, rules, score, ...glossary] of entries) { rows.push({ expression, reading, - tags, + definitionTags, rules, score, glossary, @@ -208,14 +265,16 @@ class Database { }); } } else { - for (const [expression, reading, tags, rules, score, glossary] of entries) { + for (const [expression, reading, definitionTags, rules, score, glossary, sequence, termTags] of entries) { rows.push({ expression, reading, - tags, + definitionTags, rules, score, glossary, + sequence, + termTags, dictionary: summary.title }); } @@ -300,12 +359,13 @@ class Database { } const rows = []; - for (const [name, category, order, notes] of entries) { + for (const [name, category, order, notes, score] of entries) { const row = dictTagSanitize({ name, category, order, notes, + score, dictionary: summary.title }); @@ -350,12 +410,11 @@ class Database { const summary = { title: index.title, revision: index.revision, + sequenced: index.sequenced, version: index.format || index.version }; - if (indexDataLoaded) { - await indexDataLoaded(summary); - } + await indexDataLoaded(summary); const buildTermBankName = index => `term_bank_${index + 1}.json`; const buildTermMetaBankName = index => `term_meta_bank_${index + 1}.json`; @@ -390,7 +449,7 @@ class Database { const bank = []; for (const name in index.tagMeta) { const tag = index.tagMeta[name]; - bank.push([name, tag.category, tag.order, tag.notes]); + bank.push([name, tag.category, tag.order, tag.notes, tag.score]); } tagDataLoaded(summary, bank, ++bankTotalCount, bankLoadedCount++); diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index 57acbe5e..fea5f3e5 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -89,7 +89,7 @@ function dictTermsSort(definitions, dictionaries=null) { return 1; } - return v2.expression.localeCompare(v1.expression); + return v2.expression.toString().localeCompare(v1.expression.toString()); }); } @@ -110,6 +110,33 @@ function dictTermsUndupe(definitions) { return definitionsUnique; } +function dictTermsCompressTags(definitions) { + let lastDictionary = ''; + let lastPartOfSpeech = ''; + + for (const definition of definitions) { + const dictionary = JSON.stringify(definition.definitionTags.filter(tag => tag.category === 'dictionary').map(tag => tag.name).sort()); + const partOfSpeech = JSON.stringify(definition.definitionTags.filter(tag => tag.category === 'partOfSpeech').map(tag => tag.name).sort()); + + const filterOutCategories = []; + + if (lastDictionary === dictionary) { + filterOutCategories.push('dictionary'); + } else { + lastDictionary = dictionary; + lastPartOfSpeech = ''; + } + + if (lastPartOfSpeech === partOfSpeech) { + filterOutCategories.push('partOfSpeech'); + } else { + lastPartOfSpeech = partOfSpeech; + } + + definition.definitionTags = definition.definitionTags.filter(tag => !filterOutCategories.includes(tag.category)); + } +} + function dictTermsGroup(definitions, dictionaries) { const groups = {}; for (const definition of definitions) { @@ -136,6 +163,7 @@ function dictTermsGroup(definitions, dictionaries) { expression: firstDef.expression, reading: firstDef.reading, reasons: firstDef.reasons, + termTags: groupDefs[0].termTags, score: groupDefs.reduce((p, v) => v.score > p ? v.score : p, Number.MIN_SAFE_INTEGER), source: firstDef.source }); @@ -144,6 +172,116 @@ function dictTermsGroup(definitions, dictionaries) { return dictTermsSort(results); } +function dictTermsMergeBySequence(definitions, mainDictionary) { + const definitionsBySequence = {'-1': []}; + for (const definition of definitions) { + if (mainDictionary === definition.dictionary && definition.sequence >= 0) { + if (!definitionsBySequence[definition.sequence]) { + definitionsBySequence[definition.sequence] = { + reasons: definition.reasons, + score: Number.MIN_SAFE_INTEGER, + expression: new Set(), + reading: new Set(), + expressions: new Map(), + source: definition.source, + dictionary: definition.dictionary, + definitions: [] + }; + } + const score = Math.max(definitionsBySequence[definition.sequence].score, definition.score); + definitionsBySequence[definition.sequence].score = score; + } else { + definitionsBySequence['-1'].push(definition); + } + } + + return definitionsBySequence; +} + +function dictTermsMergeByGloss(result, definitions, appendTo, mergedIndices) { + const definitionsByGloss = appendTo || {}; + for (const [index, definition] of definitions.entries()) { + if (appendTo) { + let match = false; + for (const expression of result.expressions.keys()) { + if (definition.expression === expression) { + for (const reading of result.expressions.get(expression).keys()) { + if (definition.reading === reading) { + match = true; + break; + } + } + } + if (match) { + break; + } + } + + if (!match) { + continue; + } else if (mergedIndices) { + mergedIndices.add(index); + } + } + + const gloss = JSON.stringify(definition.glossary.concat(definition.dictionary)); + if (!definitionsByGloss[gloss]) { + definitionsByGloss[gloss] = { + expression: new Set(), + reading: new Set(), + definitionTags: [], + glossary: definition.glossary, + source: result.source, + reasons: [], + score: definition.score, + id: definition.id, + dictionary: definition.dictionary + }; + } + + definitionsByGloss[gloss].expression.add(definition.expression); + definitionsByGloss[gloss].reading.add(definition.reading); + + result.expression.add(definition.expression); + result.reading.add(definition.reading); + + // result->expressions[ Expression1[ Reading1[ Tag1, Tag2 ] ], Expression2, ... ] + if (!result.expressions.has(definition.expression)) { + result.expressions.set(definition.expression, new Map()); + } + if (!result.expressions.get(definition.expression).has(definition.reading)) { + result.expressions.get(definition.expression).set(definition.reading, new Set()); + } + + for (const tag of definition.definitionTags) { + if (!definitionsByGloss[gloss].definitionTags.find(existingTag => existingTag.name === tag.name)) { + definitionsByGloss[gloss].definitionTags.push(tag); + } + } + + for (const tag of definition.termTags) { + result.expressions.get(definition.expression).get(definition.reading).add(tag); + } + } + + for (const gloss in definitionsByGloss) { + const definition = definitionsByGloss[gloss]; + definition.only = []; + if (!utilSetEqual(definition.expression, result.expression)) { + for (const expression of utilSetIntersection(definition.expression, result.expression)) { + definition.only.push(expression); + } + } + if (!utilSetEqual(definition.reading, result.reading)) { + for (const reading of utilSetIntersection(definition.reading, result.reading)) { + definition.only.push(reading); + } + } + } + + return definitionsByGloss; +} + function dictTagBuildSource(name) { return dictTagSanitize({name, category: 'dictionary', order: 100}); } @@ -153,6 +291,7 @@ function dictTagSanitize(tag) { tag.category = tag.category || 'default'; tag.notes = tag.notes || ''; tag.order = tag.order || 0; + tag.score = tag.score || 0; return tag; } @@ -207,10 +346,12 @@ async function dictFieldFormat(field, definition, mode, options) { const data = { marker, definition, - group: options.general.groupResults, + group: options.general.resultOutputMode === 'group', + merge: options.general.resultOutputMode === 'merge', modeTermKanji: mode === 'term-kanji', modeTermKana: mode === 'term-kana', - modeKanji: mode === 'kanji' + modeKanji: mode === 'kanji', + compactGlossaries: options.general.compactGlossaries }; const html = await apiTemplateRender(options.anki.fieldTemplates, data, true); diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 36ab7694..9f1414ad 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -19,12 +19,26 @@ function optionsFieldTemplates() { return ` +<style> +.expression-popular { + color: #0275d8; +} + +.expression-rare { + color: #999; +} +</style> {{#*inline "glossary-single"}} {{~#unless brief~}} - {{~#if tags~}}<i>({{#each tags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}} + {{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}} + {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} {{~/unless~}} {{~#if glossary.[1]~}} - <ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul> + {{~#if compactGlossaries~}} + {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} + {{~else~}} + <ul>{{#each glossary}}<li>{{#multiLine}}{{.}}{{/multiLine}}</li>{{/each}}</ul> + {{~/if~}} {{~else~}} {{~#multiLine}}{{glossary.[0]}}{{/multiLine~}} {{~/if~}} @@ -41,23 +55,56 @@ function optionsFieldTemplates() { {{/inline}} {{#*inline "expression"}} - {{~#if modeTermKana~}} - {{~#if definition.reading~}} - {{definition.reading}} + {{~#if merge~}} + {{~#if modeTermKana~}} + {{~#each definition.reading~}} + {{{.}}} + {{~#unless @last}}、{{/unless~}} + {{~else~}} + {{~#each definition.expression~}} + {{{.}}} + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~/each~}} {{~else~}} - {{definition.expression}} + {{~#each definition.expression~}} + {{{.}}} + {{~#unless @last}}、{{/unless~}} + {{~/each~}} {{~/if~}} {{~else~}} - {{definition.expression}} + {{~#if modeTermKana~}} + {{~#if definition.reading~}} + {{definition.reading}} + {{~else~}} + {{definition.expression}} + {{~/if~}} + {{~else~}} + {{definition.expression}} + {{~/if~}} {{~/if~}} {{/inline}} {{#*inline "furigana"}} - {{#furigana}}{{{definition}}}{{/furigana}} + {{~#if merge~}} + {{~#each definition.expressions~}} + <span class="expression-{{termFrequency}}">{{~#furigana}}{{{.}}}{{/furigana~}}</span> + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~else~}} + {{#furigana}}{{{definition}}}{{/furigana}} + {{~/if~}} {{/inline}} {{#*inline "furigana-plain"}} - {{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}} + {{~#if merge~}} + {{~#each definition.expressions~}} + <span class="expression-{{termFrequency}}">{{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}}</span> + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~else~}} + {{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}} + {{~/if~}} {{/inline}} {{#*inline "glossary"}} @@ -71,12 +118,18 @@ function optionsFieldTemplates() { {{~else~}} {{~#if group~}} {{~#if definition.definitions.[1]~}} - <ol>{{#each definition.definitions}}<li>{{> glossary-single brief=../brief}}</li>{{/each}}</ol> + <ol>{{#each definition.definitions}}<li>{{> glossary-single brief=../brief compactGlossaries=../compactGlossaries}}</li>{{/each}}</ol> + {{~else~}} + {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries~}} + {{~/if~}} + {{~else if merge~}} + {{~#if definition.definitions.[1]~}} + <ol>{{#each definition.definitions}}<li>{{> glossary-single brief=../brief compactGlossaries=../compactGlossaries}}</li>{{/each}}</ol> {{~else~}} - {{~> glossary-single definition.definitions.[0] brief=brief~}} + {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries~}} {{~/if~}} {{~else~}} - {{~> glossary-single definition brief=brief~}} + {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries~}} {{~/if~}} {{~/if~}} </div> @@ -95,7 +148,16 @@ function optionsFieldTemplates() { {{/inline}} {{#*inline "reading"}} - {{~#unless modeTermKana}}{{definition.reading}}{{/unless~}} + {{~#unless modeTermKana~}} + {{~#if merge~}} + {{~#each definition.reading~}} + {{{.}}} + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~else~}} + {{~definition.reading~}} + {{~/if~}} + {{~/unless~}} {{/inline}} {{#*inline "sentence"}} @@ -115,7 +177,7 @@ function optionsFieldTemplates() { {{/inline}} {{#*inline "tags"}} - {{~#each definition.tags}}{{name}}{{#unless @last}}, {{/unless}}{{/each~}} + {{~#each definition.definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each~}} {{/inline}} {{#*inline "url"}} @@ -132,14 +194,17 @@ function optionsSetDefaults(options) { enable: true, audioSource: 'jpod101', audioVolume: 100, - groupResults: true, + resultOutputMode: 'group', debugInfo: false, maxResults: 32, showAdvanced: false, popupWidth: 400, popupHeight: 250, popupOffset: 10, - showGuide: true + showGuide: true, + compactTags: false, + compactGlossaries: false, + mainDictionary: '' }, scanning: { @@ -205,6 +270,24 @@ function optionsVersion(options) { } else { options.scanning.modifier = 'none'; } + }, + () => { + if (options.general.groupResults) { + options.general.resultOutputMode = 'group'; + } else { + options.general.resultOutputMode = 'split'; + } + if (utilStringHashCode(options.anki.fieldTemplates) !== -805327496) { // a3c8508031a1073629803d0616a2ee416cd3cccc + options.anki.fieldTemplates = ` +{{#if merge}} +${optionsFieldTemplates()} +{{else}} +${options.anki.fieldTemplates} +{{/if}} +`.trim(); + } else { + options.anki.fieldTemplates = optionsFieldTemplates(); + } } ]; diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index a2f22371..dcc9c43d 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -22,9 +22,11 @@ async function formRead() { const optionsNew = $.extend(true, {}, optionsOld); optionsNew.general.showGuide = $('#show-usage-guide').prop('checked'); + optionsNew.general.compactTags = $('#compact-tags').prop('checked'); + optionsNew.general.compactGlossaries = $('#compact-glossaries').prop('checked'); + optionsNew.general.resultOutputMode = $('#result-output-mode').val(); optionsNew.general.audioSource = $('#audio-playback-source').val(); optionsNew.general.audioVolume = parseFloat($('#audio-playback-volume').val()); - optionsNew.general.groupResults = $('#group-terms-results').prop('checked'); optionsNew.general.debugInfo = $('#show-debug-info').prop('checked'); optionsNew.general.showAdvanced = $('#show-advanced-options').prop('checked'); optionsNew.general.maxResults = parseInt($('#max-displayed-results').val(), 10); @@ -55,12 +57,14 @@ async function formRead() { optionsNew.anki.kanji.fields = ankiFieldsToDict($('#kanji .anki-field-value')); } + optionsNew.general.mainDictionary = $('#dict-main').val(); $('.dict-group').each((index, element) => { const dictionary = $(element); - const title = dictionary.data('title'); - const priority = parseInt(dictionary.find('.dict-priority').val(), 10); - const enabled = dictionary.find('.dict-enabled').prop('checked'); - optionsNew.dictionaries[title] = {priority, enabled}; + optionsNew.dictionaries[dictionary.data('title')] = { + priority: parseInt(dictionary.find('.dict-priority').val(), 10), + enabled: dictionary.find('.dict-enabled').prop('checked'), + allowSecondarySearches: dictionary.find('.dict-allow-secondary-searches').prop('checked') + }; }); return {optionsNew, optionsOld}; @@ -81,6 +85,13 @@ function formUpdateVisibility(options) { advanced.hide(); } + const mainGroup = $('#dict-main-group'); + if (options.general.resultOutputMode === 'merge') { + mainGroup.show(); + } else { + mainGroup.hide(); + } + const debug = $('#debug'); if (options.general.debugInfo) { const temp = utilIsolate(options); @@ -93,17 +104,33 @@ function formUpdateVisibility(options) { } } -async function onFormOptionsChanged(e) { - try { - if (!e.originalEvent && !e.isTrigger) { - return; +async function formMainDictionaryOptionsPopulate(options) { + const select = $('#dict-main').empty(); + select.append($('<option class="text-muted" value="">Not selected</option>')); + + let mainDictionary = ''; + for (const dictRow of await utilDatabaseSummarize()) { + if (dictRow.sequenced) { + select.append($(`<option value="${dictRow.title}">${dictRow.title}</option>`)); + if (dictRow.title === options.general.mainDictionary) { + mainDictionary = dictRow.title; + } } + } - const {optionsNew, optionsOld} = await formRead(); - await optionsSave(optionsNew); + select.val(mainDictionary); +} + +async function onFormOptionsChanged(e) { + if (!e.originalEvent && !e.isTrigger) { + return; + } - formUpdateVisibility(optionsNew); + const {optionsNew, optionsOld} = await formRead(); + await optionsSave(optionsNew); + formUpdateVisibility(optionsNew); + try { const ankiUpdated = optionsNew.anki.enable !== optionsOld.anki.enable || optionsNew.anki.server !== optionsOld.anki.server; @@ -124,9 +151,11 @@ async function onReady() { const options = await optionsLoad(); $('#show-usage-guide').prop('checked', options.general.showGuide); + $('#compact-tags').prop('checked', options.general.compactTags); + $('#compact-glossaries').prop('checked', options.general.compactGlossaries); + $('#result-output-mode').val(options.general.resultOutputMode); $('#audio-playback-source').val(options.general.audioSource); $('#audio-playback-volume').val(options.general.audioVolume); - $('#group-terms-results').prop('checked', options.general.groupResults); $('#show-debug-info').prop('checked', options.general.debugInfo); $('#show-advanced-options').prop('checked', options.general.showAdvanced); $('#max-displayed-results').val(options.general.maxResults); @@ -156,6 +185,7 @@ async function onReady() { try { await dictionaryGroupsPopulate(options); + await formMainDictionaryOptionsPopulate(options); } catch (e) { dictionaryErrorShow(e); } @@ -241,19 +271,26 @@ async function dictionaryGroupsPopulate(options) { const dictGroups = $('#dict-groups').empty(); const dictWarning = $('#dict-warning').hide(); - const dictRows = await utilDatabaseGetTitles(); + const dictRows = await utilDatabaseSummarize(); if (dictRows.length === 0) { dictWarning.show(); } for (const dictRow of dictRowsSort(dictRows, options)) { - const dictOptions = options.dictionaries[dictRow.title] || {enabled: false, priority: 0}; + const dictOptions = options.dictionaries[dictRow.title] || { + enabled: false, + priority: 0, + allowSecondarySearches: false + }; + const dictHtml = await apiTemplateRender('dictionary.html', { + enabled: dictOptions.enabled, + priority: dictOptions.priority, + allowSecondarySearches: dictOptions.allowSecondarySearches, title: dictRow.title, version: dictRow.version, revision: dictRow.revision, - priority: dictOptions.priority, - enabled: dictOptions.enabled + outdated: dictRow.version < 3 }); dictGroups.append($(dictHtml)); @@ -261,7 +298,7 @@ async function dictionaryGroupsPopulate(options) { formUpdateVisibility(options); - $('.dict-enabled, .dict-priority').change(e => { + $('.dict-enabled, .dict-priority, .dict-allow-secondary-searches').change(e => { dictionaryGroupsSort(); onFormOptionsChanged(e); }); @@ -270,7 +307,7 @@ async function dictionaryGroupsPopulate(options) { async function onDictionaryPurge(e) { e.preventDefault(); - const dictControls = $('#dict-importer, #dict-groups').hide(); + const dictControls = $('#dict-importer, #dict-groups, #dict-main-group').hide(); const dictProgress = $('#dict-purge').show(); try { @@ -280,9 +317,11 @@ async function onDictionaryPurge(e) { await utilDatabasePurge(); const options = await optionsLoad(); options.dictionaries = {}; + options.general.mainDictionary = ''; await optionsSave(options); await dictionaryGroupsPopulate(options); + await formMainDictionaryOptionsPopulate(options); } catch (e) { dictionaryErrorShow(e); } finally { @@ -308,10 +347,14 @@ async function onDictionaryImport(e) { const options = await optionsLoad(); const summary = await utilDatabaseImport(e.target.files[0], updateProgress); - options.dictionaries[summary.title] = {enabled: true, priority: 0}; + options.dictionaries[summary.title] = {enabled: true, priority: 0, allowSecondarySearches: false}; + if (summary.sequenced && options.general.mainDictionary === '') { + options.general.mainDictionary = summary.title; + } await optionsSave(options); await dictionaryGroupsPopulate(options); + await formMainDictionaryOptionsPopulate(options); } catch (e) { dictionaryErrorShow(e); } finally { diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js index b5d352fc..680ec742 100644 --- a/ext/bg/js/templates.js +++ b/ext/bg/js/templates.js @@ -1,6 +1,8 @@ (function() { var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; templates['dictionary.html'] = template({"1":function(container,depth0,helpers,partials,data) { + return " <p class=\"text-warning\">This dictionary is outdated and may not support new extension features; please import the latest version.</p>\n"; +},"3":function(container,depth0,helpers,partials,data) { return "checked"; },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; @@ -11,9 +13,13 @@ templates['dictionary.html'] = template({"1":function(container,depth0,helpers,p + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper))) + " <small>rev." + alias4(((helper = (helper = helpers.revision || (depth0 != null ? depth0.revision : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"revision","hash":{},"data":data}) : helper))) - + "</small></h4>\n\n <div class=\"checkbox\">\n <label><input type=\"checkbox\" class=\"dict-enabled\" " - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.enabled : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + "> Enable search</label>\n </div>\n <div class=\"form-group options-advanced\">\n <label for=\"dict-" + + "</small></h4>\n" + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.outdated : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + "\n <div class=\"checkbox\">\n <label><input type=\"checkbox\" class=\"dict-enabled\" " + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.enabled : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + "> Enable search</label>\n </div>\n <div class=\"checkbox options-advanced\">\n <label><input type=\"checkbox\" class=\"dict-allow-secondary-searches\" " + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.allowSecondarySearches : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + "> Allow secondary searches</label>\n </div>\n <div class=\"form-group options-advanced\">\n <label for=\"dict-" + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper))) + "\">Result priority</label>\n <input type=\"number\" value=\"" + alias4(((helper = (helper = helpers.priority || (depth0 != null ? depth0.priority : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"priority","hash":{},"data":data}) : helper))) @@ -204,15 +210,20 @@ templates['model.html'] = template({"1":function(container,depth0,helpers,partia 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.tags : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.program(9, data, 0),"data":data})) != null ? stack1 : ""); + 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 : ""); },"2":function(container,depth0,helpers,partials,data) { - var stack1; + var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); - return "<div>\n" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.tags : depth0),{"name":"each","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(3, 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\""; +},"5":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-" @@ -222,88 +233,192 @@ 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"; -},"5":function(container,depth0,helpers,partials,data) { +},"7":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) { var stack1; - return "<ul>\n" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? 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 : "") + + "\n"; +},"9":function(container,depth0,helpers,partials,data) { + return ", "; +},"11":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 : "") + + ">\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"; -},"6":function(container,depth0,helpers,partials,data) { +},"12":function(container,depth0,helpers,partials,data) { + return "class=\"compact-glossary\""; +},"14":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(7, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); + 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)); if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { buffer += stack1; } return buffer + "</span></li>\n"; -},"7":function(container,depth0,helpers,partials,data) { +},"15":function(container,depth0,helpers,partials,data) { return container.escapeExpression(container.lambda(depth0, depth0)); -},"9":function(container,depth0,helpers,partials,data) { - var stack1, helper, options, buffer = - "<div class=\"glossary-item\">"; - stack1 = ((helper = (helper = helpers.multiLine || (depth0 != null ? depth0.multiLine : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"multiLine","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); +},"17":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 : "") + + "\">"; + 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)); if (!helpers.multiLine) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { buffer += stack1; } return buffer + "</div>\n"; -},"10":function(container,depth0,helpers,partials,data) { +},"18":function(container,depth0,helpers,partials,data) { + return "compact-glossary"; +},"20":function(container,depth0,helpers,partials,data) { var stack1; return container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0)); -},"12":function(container,depth0,helpers,partials,data) { - var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer = - "<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(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + " <img src=\"/mixed/img/entry-current.png\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n </div>\n\n <div class=\"expression\">"; - stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(17, 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\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(20, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +},"22":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 : "") + + " <img src=\"/mixed/img/entry-current.png\" 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(28, data, 0, blockParams, depths),"inverse":container.program(43, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + "\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(24, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(47, 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(51, 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(27, data, 0),"inverse":container.program(33, data, 0),"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(54, data, 0, blockParams, depths),"inverse":container.program(60, 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(35, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(63, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</div>\n"; -},"13":function(container,depth0,helpers,partials,data) { +},"23":function(container,depth0,helpers,partials,data) { return " <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.png\" 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.png\" 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.png\" title=\"Add reading (Alt + R)\" alt></a>\n"; -},"15":function(container,depth0,helpers,partials,data) { +},"25":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.png\" title=\"Play audio (Alt + P)\" alt></a>\n"; -},"17":function(container,depth0,helpers,partials,data) { +},"28":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(29, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"29":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\">\n <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(30, 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>\n <div class=\"peek-wrapper\">" + + ((stack1 = helpers["if"].call(alias1,(depths[1] != null ? depths[1].playback : depths[1]),{"name":"if","hash":{},"fn":container.program(33, 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(35, 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(38, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + "</div>\n <span class=\"" + + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(41, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + "\">、</span>\n </div>"; +},"30":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(18, 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(31, 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 ''; } -},"18":function(container,depth0,helpers,partials,data) { +},"31":function(container,depth0,helpers,partials,data) { var stack1; return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : ""); -},"20":function(container,depth0,helpers,partials,data) { +},"33":function(container,depth0,helpers,partials,data) { + return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.png\" title=\"Play audio\" alt></a>"; +},"35":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(36, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + "</div>"; +},"36":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"; +},"38":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(39, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + "</div>"; +},"39":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\">" + + alias4(((helper = (helper = helpers.dictionary || (depth0 != null ? depth0.dictionary : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"dictionary","hash":{},"data":data}) : helper))) + + ":" + + 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"; +},"41":function(container,depth0,helpers,partials,data) { + return "invisible"; +},"43":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(30, 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(44, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"44":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(45, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + " </div>\n"; +},"45":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"; +},"47":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(21, 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(48, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </div>\n"; -},"21":function(container,depth0,helpers,partials,data) { +},"48":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(22, 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(49, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n"; -},"22":function(container,depth0,helpers,partials,data) { +},"49":function(container,depth0,helpers,partials,data) { return "«"; -},"24":function(container,depth0,helpers,partials,data) { +},"51":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(25, 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(52, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </div>\n"; -},"25":function(container,depth0,helpers,partials,data) { +},"52":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\">" @@ -311,62 +426,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"; -},"27":function(container,depth0,helpers,partials,data) { +},"54":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(28, data, 0),"inverse":container.program(31, data, 0),"data":data})) != null ? stack1 : ""); -},"28":function(container,depth0,helpers,partials,data) { + 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(55, data, 0, blockParams, depths),"inverse":container.program(58, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +},"55":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(29, data, 0),"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(56, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </ol>\n"; -},"29":function(container,depth0,helpers,partials,data) { +},"56":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; return " <li>" - + ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") + + ((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"; -},"31":function(container,depth0,helpers,partials,data) { +},"58":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","data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"33":function(container,depth0,helpers,partials,data) { + 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 : ""); +},"60":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; - return ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"35":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(54, data, 0, blockParams, depths),"inverse":container.program(61, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +},"61":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 : "") + + " "; +},"63":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(18, 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(31, 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"; -},"37":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"65":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(38, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"38":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(66, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"66":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(39, 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(67, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n" - + ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"playback":(depths[1] != null ? depths[1].playback : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"grouped":(depths[1] != null ? depths[1].grouped : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"39":function(container,depth0,helpers,partials,data) { + + ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"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 : ""); +},"67":function(container,depth0,helpers,partials,data) { return "<hr>"; -},"41":function(container,depth0,helpers,partials,data) { +},"69":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(37, data, 0, blockParams, depths),"inverse":container.program(41, 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(65, data, 0, blockParams, depths),"inverse":container.program(69, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); },"main_d": function(fn, props, container, depth0, data, blockParams, depths) { var decorators = container.decorators; fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"args":["definition"],"data":data}) || fn; - fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(12, data, 0, blockParams, depths),"inverse":container.noop,"args":["term"],"data":data}) || fn; + 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; return fn; } diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index c915dbc0..005dd5de 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -37,6 +37,7 @@ class Translator { } async findTermsGrouped(text, dictionaries, alphanumeric) { + const options = await apiOptionsGet(); const titles = Object.keys(dictionaries); const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric); @@ -45,9 +46,118 @@ class Translator { await this.buildTermFrequencies(definition, titles); } + if (options.general.compactTags) { + for (const definition of definitionsGrouped) { + dictTermsCompressTags(definition.definitions); + } + } + return {length, definitions: definitionsGrouped}; } + async findTermsMerged(text, dictionaries, alphanumeric) { + const options = await apiOptionsGet(); + 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 definitionsBySequence = dictTermsMergeBySequence(definitions, options.general.mainDictionary); + + const definitionsMerged = []; + const mergedByTermIndices = new Set(); + for (const sequence in definitionsBySequence) { + if (sequence < 0) { + continue; + } + + const result = definitionsBySequence[sequence]; + + const rawDefinitionsBySequence = await this.database.findTermsBySequence(Number(sequence), options.general.mainDictionary); + + for (const definition of rawDefinitionsBySequence) { + const tags = await this.expandTags(definition.definitionTags, definition.dictionary); + tags.push(dictTagBuildSource(definition.dictionary)); + definition.definitionTags = tags; + } + + 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 tags = await this.expandTags(definition.definitionTags, definition.dictionary); + tags.push(dictTagBuildSource(definition.dictionary)); + definition.definitionTags = tags; + secondarySearchResults.push(definition); + } + } + } + } + + dictTermsMergeByGloss(result, definitionsBySequence['-1'].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 tags = await this.expandTags(result.expressions.get(expression).get(reading), result.dictionary); + expressions.push({ + expression: expression, + reading: reading, + termTags: dictTagsSort(tags), + termFrequency: (score => { + if (score > 0) { + return 'popular'; + } else if (score < 0) { + return 'rare'; + } else { + return 'normal'; + } + })(tags.map(tag => tag.score).reduce((p, v) => p + v, 0)) + }); + } + } + + result.expressions = expressions; + + result.expression = Array.from(result.expression); + result.reading = Array.from(result.reading); + + definitionsMerged.push(result); + } + + const strayDefinitions = definitionsBySequence['-1'].filter((definition, index) => !mergedByTermIndices.has(index)); + for (const groupedDefinition of dictTermsGroup(strayDefinitions, dictionaries)) { + groupedDefinition.expressions = [{expression: groupedDefinition.expression, reading: groupedDefinition.reading}]; + definitionsMerged.push(groupedDefinition); + } + + for (const definition of definitionsMerged) { + await this.buildTermFrequencies(definition, titles); + } + + if (options.general.compactTags) { + for (const definition of definitionsMerged) { + dictTermsCompressTags(definition.definitions); + } + } + + return {length, definitions: dictTermsSort(definitionsMerged)}; + } + async findTermsSplit(text, dictionaries, alphanumeric) { const titles = Object.keys(dictionaries); const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric); @@ -78,8 +188,9 @@ class Translator { let definitions = []; for (const deinflection of deinflections) { for (const definition of deinflection.definitions) { - const tags = await this.expandTags(definition.tags, definition.dictionary); - tags.push(dictTagBuildSource(definition.dictionary)); + const definitionTags = await this.expandTags(definition.definitionTags, definition.dictionary); + definitionTags.push(dictTagBuildSource(definition.dictionary)); + const termTags = await this.expandTags(definition.termTags, definition.dictionary); definitions.push({ source: deinflection.source, @@ -90,7 +201,9 @@ class Translator { expression: definition.expression, reading: definition.reading, glossary: definition.glossary, - tags: dictTagsSort(tags) + definitionTags: dictTagsSort(definitionTags), + termTags: dictTagsSort(termTags), + sequence: definition.sequence }); } } @@ -158,14 +271,23 @@ class Translator { } async buildTermFrequencies(definition, titles) { - definition.frequencies = []; - for (const meta of await this.database.findTermMeta(definition.expression, titles)) { - if (meta.mode === 'freq') { - definition.frequencies.push({ - expression: meta.expression, - frequency: meta.data, - dictionary: meta.dictionary - }); + let terms = []; + if (definition.expressions) { + terms = terms.concat(definition.expressions); + } else { + terms.push(definition); + } + + for (const term of terms) { + term.frequencies = []; + for (const meta of await this.database.findTermMeta(term.expression, titles)) { + if (meta.mode === 'freq') { + term.frequencies.push({ + expression: meta.expression, + frequency: meta.data, + dictionary: meta.dictionary + }); + } } } } diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index f44582eb..216cef3f 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -26,6 +26,43 @@ function utilIsolate(data) { return JSON.parse(JSON.stringify(data)); } +function utilSetEqual(setA, setB) { + if (setA.size !== setB.size) { + return false; + } + + for (const value of setA) { + if (!setB.has(value)) { + return false; + } + } + + return true; +} + +function utilSetIntersection(setA, setB) { + return new Set( + [...setA].filter(value => setB.has(value)) + ); +} + +function utilSetDifference(setA, setB) { + return new Set( + [...setA].filter(value => !setB.has(value)) + ); +} + +function utilStringHashCode(string) { + let hashCode = 0; + + for (let i = 0, charCode = string.charCodeAt(i); i < string.length; charCode = string.charCodeAt(++i)) { + hashCode = ((hashCode << 5) - hashCode) + charCode; + hashCode |= 0; + } + + return hashCode; +} + function utilBackend() { return chrome.extension.getBackgroundPage().yomichan_backend; } @@ -38,12 +75,12 @@ function utilAnkiGetDeckNames() { return utilBackend().anki.getDeckNames(); } -function utilAnkiGetModelFieldNames(modelName) { - return utilBackend().anki.getModelFieldNames(modelName); +function utilDatabaseSummarize() { + return utilBackend().translator.database.summarize(); } -function utilDatabaseGetTitles() { - return utilBackend().translator.database.getTitles(); +function utilAnkiGetModelFieldNames(modelName) { + return utilBackend().anki.getModelFieldNames(modelName); } function utilDatabasePurge() { diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 4315d74b..65043d44 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -36,7 +36,11 @@ </div> <div class="checkbox"> - <label><input type="checkbox" id="group-terms-results"> Group term results</label> + <label><input type="checkbox" id="compact-tags"> Compact tags</label> + </div> + + <div class="checkbox"> + <label><input type="checkbox" id="compact-glossaries"> Compact glossaries</label> </div> <div class="checkbox"> @@ -48,6 +52,15 @@ </div> <div class="form-group"> + <label for="result-output-mode">Result grouping</label> + <select class="form-control" id="result-output-mode"> + <option value="group">Group results by term-reading pairs</option> + <option value="merge">Group results by main dictionary entry</option> + <option value="split">Split definitions to their own results</option> + </select> + </div> + + <div class="form-group"> <label for="audio-playback-source">Audio playback source</label> <select class="form-control" id="audio-playback-source"> <option value="disabled">Disabled</option> @@ -132,10 +145,14 @@ or you can simply <a href="#" id="dict-purge-link">purge the database</a> to delete everything. </p> <p class="help-block"> - Please visit the <a href="https://foosoft.net/projects/yomichan" target="_blank">Yomichan</a> homepage to download free - dictionaries that you can use with this extension. + Deleting individual dictionaries is not currently feasible due to limitations of browser database technology. </p> + <div class="form-group" id="dict-main-group"> + <label for="dict-main">Main dictionary for merged mode</label> + <select class="form-control" id="dict-main"></select> + </div> + <div class="text-danger" id="dict-purge">Dictionary data is being purged, please be patient...</div> <div class="alert alert-warning" id="dict-warning">No dictionaries have been installed</div> <div class="alert alert-danger" id="dict-error"></div> @@ -150,6 +167,11 @@ </div> <div id="dict-importer"> + <p class="help-block"> + Select a dictionary to import for use below. Please visit the Yomichan homepage to + <a href="https://foosoft.net/projects/yomichan" target="_blank">download free dictionaries</a> + for use with this extension and to learn about importing proprietary EPWING dictionaries. + </p> <input type="file" id="dict-file"> </div> </div> diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index a1806d77..821a279f 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -76,7 +76,7 @@ function docRangeFromPoint(point) { if (!document.caretRangeFromPoint) { document.caretRangeFromPoint = (x, y) => { const position = document.caretPositionFromPoint(x,y); - if (position && position.offsetNode) { + if (position && position.offsetNode && position.offsetNode.nodeType === Node.TEXT_NODE) { const range = document.createRange(); range.setStart(position.offsetNode, position.offset); range.setEnd(position.offsetNode, position.offset); @@ -86,7 +86,7 @@ function docRangeFromPoint(point) { } const range = document.caretRangeFromPoint(point.x, point.y); - if (range && range.startContainer.nodeType === 3 && range.endContainer.nodeType === 3) { + if (range) { return new TextSourceRange(range); } } diff --git a/ext/manifest.json b/ext/manifest.json index b6d0642e..e415ece9 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Yomichan", - "version": "1.4.0", + "version": "1.5.0", "description": "Japanese dictionary with Anki integration", "icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"}, diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css index cdc1be8c..eadb9dae 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -46,6 +46,10 @@ hr { display: none; } +.invisible { + visibility: hidden; +} + /* * Entries @@ -88,6 +92,10 @@ hr { background-color: #5cb85c; } +.tag-partOfSpeech { + background-color: #565656; +} + .actions .disabled { pointer-events: none; cursor: default; @@ -118,21 +126,79 @@ hr { font-size: 24px; } -.expression a { +.expression .kanji-link { border-bottom: 1px #777 dashed; color: #333; text-decoration: none; } +.expression-popular, .expression-popular .kanji-link { + color: #0275d8; +} + +.expression-rare, .expression-rare .kanji-link { + color: #999; +} + +.expression .peek-wrapper { + font-size: 14px; + white-space: nowrap; + display: inline-block; + position: relative; + width: 0px; + height: 0px; + visibility: hidden; +} + +.expression .peek-wrapper .action-play-audio { + position: absolute; + left: 0px; + bottom: 10px; +} + +.expression .peek-wrapper .tags { + position: absolute; + left: 0px; + bottom: -10px; +} + +.expression .peek-wrapper .frequencies { + position: absolute; + left: 0px; + bottom: -30px; +} + +.expression:hover .peek-wrapper { + visibility: visible; +} + .reasons { color: #777; display: inline-block; } +.compact-info { + display: inline-block; +} + .glossary ol, .glossary ul { padding-left: 1.4em; } +.glossary ul.compact-glossary { + display: inline; + list-style: none; + padding-left: 0px; +} + +.glossary .compact-glossary li { + display: inline; +} + +.glossary .compact-glossary li:not(:first-child):before { + content: " | "; +} + .glossary li { color: #777; } @@ -141,6 +207,10 @@ hr { color: #000; } +div.glossary-item.compact-glossary { + display: inline; +} + .glyph { font-family: kanji-stroke-orders; font-size: 120px; diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 75ee339a..5d3c4f2e 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -29,6 +29,7 @@ class Display { this.audioCache = {}; $(document).keydown(this.onKeyDown.bind(this)); + $(document).on('wheel', this.onWheel.bind(this)); } onError(error) { @@ -70,8 +71,10 @@ class Display { onAudioPlay(e) { e.preventDefault(); - const index = Display.entryIndexFind($(e.currentTarget)); - this.audioPlay(this.definitions[index]); + const link = $(e.currentTarget); + const definitionIndex = Display.entryIndexFind(link); + const expressionIndex = link.closest('.entry').find('.expression .action-play-audio').index(link); + this.audioPlay(this.definitions[definitionIndex], expressionIndex); } onNoteAdd(e) { @@ -182,7 +185,8 @@ class Display { 80: /* p */ () => { if (e.altKey) { if ($('.entry').eq(this.index).data('type') === 'term') { - this.audioPlay(this.definitions[this.index]); + const expressionIndex = this.options.general.resultOutputMode === 'merge' ? 0 : -1; + this.audioPlay(this.definitions[this.index], expressionIndex); } return true; @@ -202,6 +206,25 @@ class Display { } } + onWheel(e) { + const event = e.originalEvent; + const handler = () => { + if (event.altKey) { + if (event.deltaY < 0) { // scroll up + this.entryScrollIntoView(this.index - 1, true); + return true; + } else if (event.deltaY > 0) { // scroll down + this.entryScrollIntoView(this.index + 1, true); + return true; + } + } + }; + + if (handler()) { + event.preventDefault(); + } + } + async termsShow(definitions, options, context) { try { window.focus(); @@ -214,8 +237,10 @@ class Display { const params = { definitions, addable: options.anki.enable, - grouped: options.general.groupResults, + grouped: options.general.resultOutputMode === 'group', + merged: options.general.resultOutputMode === 'merge', playback: options.general.audioSource !== 'disabled', + compactGlossaries: options.general.compactGlossaries, debug: options.general.debugInfo }; @@ -359,11 +384,12 @@ class Display { } } - async audioPlay(definition) { + async audioPlay(definition, expressionIndex) { try { this.spinner.show(); - let url = await apiAudioGetUrl(definition, this.options.general.audioSource); + const expression = expressionIndex === -1 ? definition : definition.expressions[expressionIndex]; + let url = await apiAudioGetUrl(expression, this.options.general.audioSource); if (!url) { url = '/mixed/mp3/button.mp3'; } diff --git a/tmpl/dictionary.html b/tmpl/dictionary.html index f2f7f687..61bb79ac 100644 --- a/tmpl/dictionary.html +++ b/tmpl/dictionary.html @@ -1,9 +1,15 @@ <div class="dict-group well well-sm" data-title="{{title}}"> <h4><span class="text-muted glyphicon glyphicon-book"></span> {{title}} <small>rev.{{revision}}</small></h4> + {{#if outdated}} + <p class="text-warning">This dictionary is outdated and may not support new extension features; please import the latest version.</p> + {{/if}} <div class="checkbox"> <label><input type="checkbox" class="dict-enabled" {{#if enabled}}checked{{/if}}> Enable search</label> </div> + <div class="checkbox options-advanced"> + <label><input type="checkbox" class="dict-allow-secondary-searches" {{#if allowSecondarySearches}}checked{{/if}}> Allow secondary searches</label> + </div> <div class="form-group options-advanced"> <label for="dict-{{title}}">Result priority</label> <input type="number" value="{{priority}}" id="dict-{{title}}" class="form-control dict-priority"> diff --git a/tmpl/terms.html b/tmpl/terms.html index a130e775..35dedb71 100644 --- a/tmpl/terms.html +++ b/tmpl/terms.html @@ -1,19 +1,28 @@ {{#*inline "definition"}} -{{#if tags}} -<div> - {{#each tags}} +{{#if definitionTags}} +<div {{#if compactGlossaries}}class="compact-info"{{/if}}> + {{#each definitionTags}} <span class="label label-default tag-{{category}}" title="{{notes}}">{{name}}</span> {{/each}} </div> {{/if}} +{{#if only}} +<div {{#if compactGlossaries}}class="compact-info"{{/if}}> + ( + {{~#each only~}} + {{{.}}}{{#unless @last}}, {{/unless}} + {{/each}} + only) +</div> +{{/if}} {{#if glossary.[1]}} -<ul> +<ul {{#if compactGlossaries}}class="compact-glossary"{{/if}}> {{#each glossary}} <li><span class="glossary-item">{{#multiLine}}{{.}}{{/multiLine}}</span></li> {{/each}} </ul> {{else}} -<div class="glossary-item">{{#multiLine}}{{glossary.[0]}}{{/multiLine}}</div> +<div class="glossary-item {{#if compactGlossaries}}compact-glossary{{/if}}">{{#multiLine}}{{glossary.[0]}}{{/multiLine}}</div> {{/if}} {{/inline}} @@ -25,13 +34,50 @@ <a href="#" class="action-add-note pending disabled" data-mode="term-kanji"><img src="/mixed/img/add-term-kanji.png" title="Add expression (Alt + E)" alt></a> <a href="#" class="action-add-note pending disabled" data-mode="term-kana"><img src="/mixed/img/add-term-kana.png" title="Add reading (Alt + R)" alt></a> {{/if}} + {{#unless merged}} {{#if playback}} <a href="#" class="action-play-audio"><img src="/mixed/img/play-audio.png" title="Play audio (Alt + P)" alt></a> {{/if}} + {{/unless}} <img src="/mixed/img/entry-current.png" class="current" title="Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)" alt> </div> + {{#if merged}} + {{~#each expressions~}} + <div class="expression"> + <span class="expression-{{termFrequency}}">{{#kanjiLinks}}{{#furigana}}{{{.}}}{{/furigana}}{{/kanjiLinks}}</span> + <div class="peek-wrapper"> + {{~#if ../playback~}} + <a href="#" class="action-play-audio"><img src="/mixed/img/play-audio.png" title="Play audio" alt></a> + {{~/if~}} + {{~#if termTags~}} + <div class="tags"> + {{~#each termTags}} + <span class="label label-default tag-{{category}}" title="{{notes}}">{{name}}</span> + {{/each~}} + </div> + {{~/if~}} + {{~#if frequencies~}} + <div class="frequencies"> + {{~#each frequencies}} + <span class="label label-default tag-frequency">{{dictionary}}:{{frequency}}</span> + {{/each~}} + </div> + {{~/if~}} + </div> + <span class="{{#if @last}}invisible{{/if}}">、</span> + </div> + {{~/each~}} + {{else}} <div class="expression">{{#kanjiLinks}}{{#furigana}}{{{.}}}{{/furigana}}{{/kanjiLinks}}</div> + {{#if termTags}} + <div style="display: inline-block;"> + {{#each termTags}} + <span class="label label-default tag-{{category}}" title="{{notes}}">{{name}}</span> + {{/each}} + </div> + {{/if}} + {{/if}} {{#if reasons}} <div class="reasons"> @@ -54,14 +100,24 @@ {{#if definitions.[1]}} <ol> {{#each definitions}} - <li>{{> definition}}</li> + <li>{{> definition compactGlossaries=../compactGlossaries}}</li> + {{/each}} + </ol> + {{else}} + {{> definition definitions.[0] compactGlossaries=compactGlossaries}} + {{/if}} + {{else if merged}} + {{#if definitions.[1]}} + <ol> + {{#each definitions}} + <li>{{> definition compactGlossaries=../compactGlossaries}}</li> {{/each}} </ol> {{else}} - {{> definition definitions.[0]}} + {{> definition definitions.[0] compactGlossaries=compactGlossaries}} {{/if}} {{else}} - {{> definition}} + {{> definition compactGlossaries=compactGlossaries}} {{/if}} </div> @@ -74,7 +130,7 @@ {{#if definitions}} {{#each definitions}} {{#unless @first}}<hr>{{/unless}} -{{> term debug=../debug grouped=../grouped addable=../addable playback=../playback}} +{{> term debug=../debug grouped=../grouped merged=../merged addable=../addable playback=../playback compactGlossaries=../compactGlossaries}} {{/each}} {{else}} <p class="note">No results found.</p> |