diff options
Diffstat (limited to 'ext/bg/js/database.js')
-rw-r--r-- | ext/bg/js/database.js | 362 |
1 files changed, 253 insertions, 109 deletions
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 65fc78fb..6ceb3ec8 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -20,44 +20,33 @@ class Database { constructor() { this.db = null; - this.version = 2; this.tagCache = {}; } - async sanitize() { - try { - const db = new Dexie('dict'); - await db.open(); - db.close(); - if (db.verno !== this.version) { - await db.delete(); - } - } catch(e) { - // NOP - } - } - async prepare() { if (this.db) { - throw 'database already initialized'; + throw 'Database already initialized'; } - await this.sanitize(); - this.db = new Dexie('dict'); - this.db.version(this.version).stores({ - terms: '++id,dictionary,expression,reading', - kanji: '++,dictionary,character', - tagMeta: '++,dictionary', + this.db.version(2).stores({ + terms: '++id,dictionary,expression,reading', + kanji: '++,dictionary,character', + tagMeta: '++,dictionary', dictionaries: '++,title,version' }); + this.db.version(3).stores({ + termMeta: '++,dictionary,expression', + kanjiMeta: '++,dictionary,character', + tagMeta: '++,dictionary,name' + }); await this.db.open(); } async purge() { if (!this.db) { - throw 'database not initialized'; + throw 'Database not initialized'; } this.db.close(); @@ -70,7 +59,7 @@ class Database { async findTerms(term, titles) { if (!this.db) { - throw 'database not initialized'; + throw 'Database not initialized'; } const results = []; @@ -89,17 +78,31 @@ class Database { } }); - await this.cacheTagMeta(titles); - for (const result of results) { - result.tagMeta = this.tagCache[result.dictionary] || {}; + return results; + } + + async findTermMeta(term, titles) { + if (!this.db) { + throw 'Database not initialized'; } + const results = []; + await this.db.termMeta.where('expression').equals(term).each(row => { + if (titles.includes(row.dictionary)) { + results.push({ + mode: row.mode, + data: row.data, + dictionary: row.dictionary + }); + } + }); + return results; } async findKanji(kanji, titles) { if (!this.db) { - return Promise.reject('database not initialized'); + throw 'Database not initialized'; } const results = []; @@ -111,163 +114,304 @@ class Database { kunyomi: dictFieldSplit(row.kunyomi), tags: dictFieldSplit(row.tags), glossary: row.meanings, + stats: row.stats, dictionary: row.dictionary }); } }); - await this.cacheTagMeta(titles); - for (const result of results) { - result.tagMeta = this.tagCache[result.dictionary] || {}; + return results; + } + + async findKanjiMeta(kanji, titles) { + if (!this.db) { + throw 'Database not initialized'; } + const results = []; + await this.db.kanjiMeta.where('character').equals(kanji).each(row => { + if (titles.includes(row.dictionary)) { + results.push({ + mode: row.mode, + data: row.data, + dictionary: row.dictionary + }); + } + }); + return results; } - async cacheTagMeta(titles) { + async findTagForTitle(name, title) { if (!this.db) { - throw 'database not initialized'; + throw 'Database not initialized'; } - for (const title of titles) { - if (!this.tagCache[title]) { - const tagMeta = {}; - await this.db.tagMeta.where('dictionary').equals(title).each(row => { - tagMeta[row.name] = {category: row.category, notes: row.notes, order: row.order}; - }); + this.tagCache[title] = this.tagCache[title] || {}; - this.tagCache[title] = tagMeta; - } + let result = this.tagCache[title][name]; + if (!result) { + await this.db.tagMeta.where('name').equals(name).each(row => { + if (title === row.dictionary) { + result = row; + } + }); + + this.tagCache[title][name] = result; } + + return result; } - async getDictionaries() { + async getTitles() { if (this.db) { return this.db.dictionaries.toArray(); } else { - throw 'database not initialized'; + throw 'Database not initialized'; } } async importDictionary(archive, callback) { if (!this.db) { - return Promise.reject('database not initialized'); + throw 'Database not initialized'; } - let summary = null; - const indexLoaded = async (title, version, revision, tagMeta, hasTerms, hasKanji) => { - summary = {title, version, revision, hasTerms, hasKanji}; - - const count = await this.db.dictionaries.where('title').equals(title).count(); - if (count > 0) { - throw `dictionary "${title}" is already imported`; + const indexDataLoaded = async summary => { + if (summary.version > 2) { + throw 'Unsupported dictionary version'; } - await this.db.dictionaries.add({title, version, revision, hasTerms, hasKanji}); - - const rows = []; - for (const tag in tagMeta || {}) { - const meta = tagMeta[tag]; - const row = dictTagSanitize({ - name: tag, - category: meta.category, - notes: meta.notes, - order: meta.order, - dictionary: title - }); - - rows.push(row); + const count = await this.db.dictionaries.where('title').equals(summary.title).count(); + if (count > 0) { + throw 'Dictionary is already imported'; } - await this.db.tagMeta.bulkAdd(rows); + await this.db.dictionaries.add(summary); }; - const termsLoaded = async (title, entries, total, current) => { + const termDataLoaded = async (summary, entries, total, current) => { if (callback) { callback(total, current); } - await this.db.transaction('rw', this.db.terms, async function() { + const rows = []; + if (summary.version === 1) { for (const [expression, reading, tags, rules, score, ...glossary] of entries) { - this.db.terms.add({ + rows.push({ expression, reading, tags, rules, score, glossary, - dictionary: title + dictionary: summary.title }); } - }); + } else { + for (const [expression, reading, tags, rules, score, glossary] of entries) { + rows.push({ + expression, + reading, + tags, + rules, + score, + glossary, + dictionary: summary.title + }); + } + } + + await this.db.terms.bulkAdd(rows); + }; + + const termMetaDataLoaded = async (summary, entries, total, current) => { + if (callback) { + callback(total, current); + } + + const rows = []; + for (const [expression, mode, data] of entries) { + rows.push({ + expression, + mode, + data, + dictionary: summary.title + }); + } + + await this.db.termMeta.bulkAdd(rows); }; - const kanjiLoaded = async (title, entries, total, current) => { + const kanjiDataLoaded = async (summary, entries, total, current) => { if (callback) { callback(total, current); } - await this.db.transaction('rw', this.db.kanji, async function() { + const rows = []; + if (summary.version === 1) { for (const [character, onyomi, kunyomi, tags, ...meanings] of entries) { - this.db.kanji.add({ + rows.push({ character, onyomi, kunyomi, tags, meanings, - dictionary: title + dictionary: summary.title }); } - }); + } else { + for (const [character, onyomi, kunyomi, tags, meanings, stats] of entries) { + rows.push({ + character, + onyomi, + kunyomi, + tags, + meanings, + stats, + dictionary: summary.title + }); + } + } + + await this.db.kanji.bulkAdd(rows); }; - await Database.importDictionaryZip(archive, indexLoaded, termsLoaded, kanjiLoaded); - return summary; - } + const kanjiMetaDataLoaded = async (summary, entries, total, current) => { + if (callback) { + callback(total, current); + } + + const rows = []; + for (const [character, mode, data] of entries) { + rows.push({ + character, + mode, + data, + dictionary: summary.title + }); + } + + await this.db.kanjiMeta.bulkAdd(rows); + }; - static async importDictionaryZip(archive, indexLoaded, termsLoaded, kanjiLoaded) { - const files = (await JSZip.loadAsync(archive)).files; + const tagDataLoaded = async (summary, entries, total, current) => { + if (callback) { + callback(total, current); + } - const indexFile = files['index.json']; + const rows = []; + for (const [name, category, order, notes] of entries) { + const row = dictTagSanitize({ + name, + category, + order, + notes, + dictionary: summary.title + }); + + rows.push(row); + } + + await this.db.tagMeta.bulkAdd(rows); + }; + + return await Database.importDictionaryZip( + archive, + indexDataLoaded, + termDataLoaded, + termMetaDataLoaded, + kanjiDataLoaded, + kanjiMetaDataLoaded, + tagDataLoaded + ); + } + + static async importDictionaryZip( + archive, + indexDataLoaded, + termDataLoaded, + termMetaDataLoaded, + kanjiDataLoaded, + kanjiMetaDataLoaded, + tagDataLoaded + ) { + const zip = await JSZip.loadAsync(archive); + + const indexFile = zip.files['index.json']; if (!indexFile) { - throw 'no dictionary index found in archive'; + throw 'No dictionary index found in archive'; } const index = JSON.parse(await indexFile.async('string')); - if (!index.title || !index.version || !index.revision) { - throw 'unrecognized dictionary format'; + if (!index.title || !index.revision) { + throw 'Unrecognized dictionary format'; } - await indexLoaded( - index.title, - index.version, - index.revision, - index.tagMeta || {}, - index.termBanks > 0, - index.kanjiBanks > 0 - ); + const summary = { + title: index.title, + revision: index.revision, + version: index.format || index.version + }; - const banksTotal = index.termBanks + index.kanjiBanks; - let banksLoaded = 0; + if (indexDataLoaded) { + await indexDataLoaded(summary); + } - for (let i = 1; i <= index.termBanks; ++i) { - const bankFile = files[`term_bank_${i}.json`]; - if (bankFile) { - const bank = JSON.parse(await bankFile.async('string')); - await termsLoaded(index.title, bank, banksTotal, banksLoaded++); - } else { - throw 'missing term bank file'; + const buildTermBankName = index => `term_bank_${index + 1}.json`; + const buildTermMetaBankName = index => `term_meta_bank_${index + 1}.json`; + const buildKanjiBankName = index => `kanji_bank_${index + 1}.json`; + const buildKanjiMetaBankName = index => `kanji_meta_bank_${index + 1}.json`; + const buildTagBankName = index => `tag_bank_${index + 1}.json`; + + const countBanks = namer => { + let count = 0; + while (zip.files[namer(count)]) { + ++count; } - } - for (let i = 1; i <= index.kanjiBanks; ++i) { - const bankFile = files[`kanji_bank_${i}.json`]; - if (bankFile) { - const bank = JSON.parse(await bankFile.async('string')); - await kanjiLoaded(index.title, bank, banksTotal, banksLoaded++); - } else { - throw 'missing kanji bank file'; + return count; + }; + + const termBankCount = countBanks(buildTermBankName); + const termMetaBankCount = countBanks(buildTermMetaBankName); + const kanjiBankCount = countBanks(buildKanjiBankName); + const kanjiMetaBankCount = countBanks(buildKanjiMetaBankName); + const tagBankCount = countBanks(buildTagBankName); + + let bankLoadedCount = 0; + let bankTotalCount = + termBankCount + + termMetaBankCount + + kanjiBankCount + + kanjiMetaBankCount + + tagBankCount; + + if (tagDataLoaded && index.tagMeta) { + const bank = []; + for (const name in index.tagMeta) { + const tag = index.tagMeta[name]; + bank.push([name, tag.category, tag.order, tag.notes]); } + + tagDataLoaded(summary, bank, ++bankTotalCount, bankLoadedCount++); } + + const loadBank = async (summary, namer, count, callback) => { + if (callback) { + for (let i = 0; i < count; ++i) { + const bankFile = zip.files[namer(i)]; + const bank = JSON.parse(await bankFile.async('string')); + await callback(summary, bank, bankTotalCount, bankLoadedCount++); + } + } + }; + + await loadBank(summary, buildTermBankName, termBankCount, termDataLoaded); + await loadBank(summary, buildTermMetaBankName, termMetaBankCount, termMetaDataLoaded); + await loadBank(summary, buildKanjiBankName, kanjiBankCount, kanjiDataLoaded); + await loadBank(summary, buildKanjiMetaBankName, kanjiMetaBankCount, kanjiMetaDataLoaded); + await loadBank(summary, buildTagBankName, tagBankCount, tagDataLoaded); + + return summary; } } |