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..fcf8ef3f 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 + 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, + 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 }); } }); @@ -171,12 +228,27 @@ class Database { } } + async getTitlesWithSequences() { + if (!this.db) { + throw 'Database not initialized'; + } + + const titles = []; + await this.db.dictionaries.each(row => { + if (row.hasSequences) { + titles.push(row.title); + } + }); + + return titles; + } + async importDictionary(archive, callback) { if (!this.db) { throw 'Database not initialized'; } - const indexDataLoaded = async summary => { + const indexDataValid = async summary => { if (summary.version > 2) { throw 'Unsupported dictionary version'; } @@ -185,7 +257,9 @@ class Database { if (count > 0) { throw 'Dictionary is already imported'; } + }; + const indexDataLoaded = async summary => { await this.db.dictionaries.add(summary); }; @@ -196,11 +270,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,19 +282,23 @@ 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 }); } } + summary.hasSequences = rows.every(row => row.sequence >= 0); + await this.db.terms.bulkAdd(rows); }; @@ -300,12 +378,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 }); @@ -317,6 +396,7 @@ class Database { return await Database.importDictionaryZip( archive, + indexDataValid, indexDataLoaded, termDataLoaded, termMetaDataLoaded, @@ -328,6 +408,7 @@ class Database { static async importDictionaryZip( archive, + indexDataValid, indexDataLoaded, termDataLoaded, termMetaDataLoaded, @@ -353,9 +434,7 @@ class Database { version: index.format || index.version }; - if (indexDataLoaded) { - await indexDataLoaded(summary); - } + await indexDataValid(summary); const buildTermBankName = index => `term_bank_${index + 1}.json`; const buildTermMetaBankName = index => `term_meta_bank_${index + 1}.json`; @@ -390,7 +469,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++); @@ -412,6 +491,10 @@ class Database { await loadBank(summary, buildKanjiMetaBankName, kanjiMetaBankCount, kanjiMetaDataLoaded); await loadBank(summary, buildTagBankName, tagBankCount, tagDataLoaded); + if (indexDataLoaded) { + await indexDataLoaded(summary); + } + return summary; } } 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 ` + {{#*inline "glossary-single"}} {{~#unless brief~}} - {{~#if tags~}}({{#each tags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}) {{/if~}} + {{~#if definitionTags~}}({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}) {{/if~}} + {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} {{~/unless~}} {{~#if glossary.[1]~}} -
"; - 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 + "\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 "
No results found.
\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..091137ed 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; } @@ -46,6 +83,10 @@ function utilDatabaseGetTitles() { return utilBackend().translator.database.getTitles(); } +function utilDatabaseGetTitlesWithSequences() { + return utilBackend().translator.database.getTitlesWithSequences(); +} + function utilDatabasePurge() { return utilBackend().translator.database.purge(); } diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 4315d74b..f430d5e2 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -36,7 +36,11 @@Yomichan can import and use a variety of dictionary formats. Unneeded dictionaries can be disabled, or you can simply purge the database to delete everything. 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 302a6280..41fe85eb 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -71,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) { @@ -183,7 +185,7 @@ class Display { 80: /* p */ () => { if (e.altKey) { if ($('.entry').eq(this.index).data('type') === 'term') { - this.audioPlay(this.definitions[this.index]); + this.audioPlay(this.definitions[this.index], this.options.general.resultOutputMode === 'merge' ? 0 : -1); } return true; @@ -234,8 +236,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 }; @@ -379,11 +383,11 @@ class Display { } } - async audioPlay(definition) { + async audioPlay(definition, expressionIndex) { try { this.spinner.show(); - let url = await apiAudioGetUrl(definition, this.options.general.audioSource); + let url = await apiAudioGetUrl(expressionIndex === -1 ? definition : definition.expressions[expressionIndex], this.options.general.audioSource); if (!url) { url = '/mixed/mp3/button.mp3'; } diff --git a/tmpl/dictionary.html b/tmpl/dictionary.html index f2f7f687..1dc04f0f 100644 --- a/tmpl/dictionary.html +++ b/tmpl/dictionary.html @@ -4,6 +4,9 @@