diff --git a/ext/css/settings.css b/ext/css/settings.css
index f05ab5e0..cedd9f40 100644
--- a/ext/css/settings.css
+++ b/ext/css/settings.css
@@ -2259,10 +2259,14 @@ input[type=number].dictionary-priority {
}
.horizontal-flex.horizontal-flex-nowrap {
flex-wrap: nowrap;
+ margin-left: 0;
}
.horizontal-flex>* {
margin-left: 0.375em;
}
+.horizontal-flex.horizontal-flex-nowrap>*:first-child {
+ margin-left: 0;
+}
.horizontal-flex-fill {
flex-grow: 1;
}
diff --git a/ext/data/schemas/options-schema.json b/ext/data/schemas/options-schema.json
index d1fb28ad..2dd3981d 100644
--- a/ext/data/schemas/options-schema.json
+++ b/ext/data/schemas/options-schema.json
@@ -116,7 +116,9 @@
"popupActionBarVisibility",
"popupActionBarLocation",
"frequencyDisplayMode",
- "termDisplayMode"
+ "termDisplayMode",
+ "sortFrequencyDictionary",
+ "sortFrequencyDictionaryOrder"
],
"properties": {
"enable": {
@@ -284,6 +286,15 @@
"type": "string",
"enum": ["ruby", "ruby-and-reading", "term-and-reading"],
"default": "ruby"
+ },
+ "sortFrequencyDictionary": {
+ "type": ["string", "null"],
+ "default": null
+ },
+ "sortFrequencyDictionaryOrder": {
+ "type": "string",
+ "enum": ["ascending", "descending"],
+ "default": "descending"
}
}
},
diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js
index b9e1f51b..e76f4cfe 100644
--- a/ext/js/background/backend.js
+++ b/ext/js/background/backend.js
@@ -125,7 +125,8 @@ class Backend {
['triggerDatabaseUpdated', {async: false, contentScript: true, handler: this._onApiTriggerDatabaseUpdated.bind(this)}],
['testMecab', {async: true, contentScript: true, handler: this._onApiTestMecab.bind(this)}],
['textHasJapaneseCharacters', {async: false, contentScript: true, handler: this._onApiTextHasJapaneseCharacters.bind(this)}],
- ['documentStart', {async: false, contentScript: true, handler: this._onApiDocumentStart.bind(this)}]
+ ['documentStart', {async: false, contentScript: true, handler: this._onApiDocumentStart.bind(this)}],
+ ['getTermFrequencies', {async: true, contentScript: true, handler: this._onApiGetTermFrequencies.bind(this)}]
]);
this._messageHandlersWithProgress = new Map([
]);
@@ -748,6 +749,10 @@ class Backend {
this._updateTabAccessibility(url, tab, frameId);
}
+ async _onApiGetTermFrequencies({termReadingList, dictionaries}) {
+ return await this._translator.getTermFrequencies(termReadingList, dictionaries);
+ }
+
// Command handlers
async _onCommandOpenSearchPage(params) {
@@ -1953,7 +1958,7 @@ class Backend {
const {wildcard} = details;
const enabledDictionaryMap = this._getTranslatorEnabledDictionaryMap(options);
const {
- general: {mainDictionary},
+ general: {mainDictionary, sortFrequencyDictionary, sortFrequencyDictionaryOrder},
scanning: {alphanumeric},
translation: {
convertHalfWidthCharacters,
@@ -1979,6 +1984,8 @@ class Backend {
return {
wildcard,
mainDictionary,
+ sortFrequencyDictionary,
+ sortFrequencyDictionaryOrder,
removeNonJapaneseCharacters: !alphanumeric,
convertHalfWidthCharacters,
convertNumericCharacters,
diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js
index 3fa7c92b..cb2fef85 100644
--- a/ext/js/comm/api.js
+++ b/ext/js/comm/api.js
@@ -168,6 +168,10 @@ class API {
return this._invoke('textHasJapaneseCharacters', {text});
}
+ getTermFrequencies(termReadingList, dictionaries) {
+ return this._invoke('getTermFrequencies', {termReadingList, dictionaries});
+ }
+
// Utilities
_createActionPort(timeout=5000) {
diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js
index 30ffadb1..c8ab2d01 100644
--- a/ext/js/data/options-util.js
+++ b/ext/js/data/options-util.js
@@ -463,7 +463,8 @@ class OptionsUtil {
{async: false, update: this._updateVersion11.bind(this)},
{async: true, update: this._updateVersion12.bind(this)},
{async: true, update: this._updateVersion13.bind(this)},
- {async: false, update: this._updateVersion14.bind(this)}
+ {async: false, update: this._updateVersion14.bind(this)},
+ {async: false, update: this._updateVersion15.bind(this)}
];
if (typeof targetVersion === 'number' && targetVersion < result.length) {
result.splice(targetVersion);
@@ -876,4 +877,15 @@ class OptionsUtil {
}
return options;
}
+
+ _updateVersion15(options) {
+ // Version 15 changes:
+ // Added general.sortFrequencyDictionary.
+ // Added general.sortFrequencyDictionaryOrder.
+ for (const profile of options.profiles) {
+ profile.options.general.sortFrequencyDictionary = null;
+ profile.options.general.sortFrequencyDictionaryOrder = 'descending';
+ }
+ return options;
+ }
}
diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js
index 641c9d57..1abf9f4e 100644
--- a/ext/js/language/translator.js
+++ b/ext/js/language/translator.js
@@ -64,6 +64,8 @@ class Translator {
* {
* wildcard: (enum: null, 'prefix', 'suffix'),
* mainDictionary: (string),
+ * sortFrequencyDictionary: (null or string),
+ * sortFrequencyDictionaryOrder: (enum: 'ascending', 'descending'),
* removeNonJapaneseCharacters: (boolean),
* convertHalfWidthCharacters: (enum: 'false', 'true', 'variant'),
* convertNumericCharacters: (enum: 'false', 'true', 'variant'),
@@ -92,7 +94,7 @@ class Translator {
* @returns An object of the structure `{dictionaryEntries, originalTextLength}`.
*/
async findTerms(mode, text, options) {
- const {enabledDictionaryMap, excludeDictionaryDefinitions} = options;
+ const {enabledDictionaryMap, excludeDictionaryDefinitions, sortFrequencyDictionary, sortFrequencyDictionaryOrder} = options;
let {dictionaryEntries, originalTextLength} = await this._findTermsInternal(text, enabledDictionaryMap, options);
switch (mode) {
@@ -115,6 +117,9 @@ class Translator {
await this._expandTermTags(dictionaryEntries);
}
+ if (sortFrequencyDictionary !== null) {
+ this._updateSortFrequencies(dictionaryEntries, sortFrequencyDictionary, sortFrequencyDictionaryOrder === 'ascending');
+ }
if (dictionaryEntries.length > 1) {
this._sortTermDictionaryEntries(dictionaryEntries);
}
@@ -176,6 +181,48 @@ class Translator {
return dictionaryEntries;
}
+ /**
+ * Gets a list of frequency information for a given list of term-reading pairs
+ * and a list of dictionaries.
+ * @param termReadingList An array of `{term, reading}` pairs. If reading is null,
+ * the reading won't be compared.
+ * @param dictionaries An array of dictionary names.
+ * @returns An array of objects with the format
+ * `{term, reading, dictionary, hasReading, frequency}`.
+ */
+ async getTermFrequencies(termReadingList, dictionaries) {
+ const dictionarySet = new Set();
+ for (const dictionary of dictionaries) {
+ dictionarySet.add(dictionary);
+ }
+
+ const termList = termReadingList.map(({term}) => term);
+ const metas = await this._database.findTermMetaBulk(termList, dictionarySet);
+
+ const results = [];
+ for (const {mode, data, dictionary, index} of metas) {
+ if (mode !== 'freq') { continue; }
+ let {term, reading} = termReadingList[index];
+ let frequency = data;
+ const hasReading = (data !== null && typeof data === 'object');
+ if (hasReading) {
+ if (data.reading !== reading) {
+ if (reading !== null) { continue; }
+ reading = data.reading;
+ }
+ frequency = data.frequency;
+ }
+ results.push({
+ term,
+ reading,
+ dictionary,
+ hasReading,
+ frequency
+ });
+ }
+ return results;
+ }
+
// Find terms internal implementation
async _findTermsInternal(text, enabledDictionaryMap, options) {
@@ -1035,7 +1082,20 @@ class Translator {
}
_createTermDefinition(index, headwordIndices, dictionary, dictionaryIndex, dictionaryPriority, id, score, sequences, isPrimary, tags, entries) {
- return {index, headwordIndices, dictionary, dictionaryIndex, dictionaryPriority, id, score, sequences, isPrimary, tags, entries};
+ return {
+ index,
+ headwordIndices,
+ dictionary,
+ dictionaryIndex,
+ dictionaryPriority,
+ id,
+ score,
+ frequencyOrder: 0,
+ sequences,
+ isPrimary,
+ tags,
+ entries
+ };
}
_createTermPronunciation(index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pitches) {
@@ -1052,6 +1112,7 @@ class Translator {
isPrimary,
inflections,
score,
+ frequencyOrder: 0,
dictionaryIndex,
dictionaryPriority,
sourceTermExactMatchCount,
@@ -1314,6 +1375,10 @@ class Translator {
i = v2.dictionaryPriority - v1.dictionaryPriority;
if (i !== 0) { return i; }
+ // Sort by frequency order
+ i = v1.frequencyOrder - v2.frequencyOrder;
+ if (i !== 0) { return i; }
+
// Sort by term score
i = v2.score - v1.score;
if (i !== 0) { return i; }
@@ -1345,6 +1410,10 @@ class Translator {
let i = v2.dictionaryPriority - v1.dictionaryPriority;
if (i !== 0) { return i; }
+ // Sort by frequency order
+ i = v1.frequencyOrder - v2.frequencyOrder;
+ if (i !== 0) { return i; }
+
// Sort by term score
i = v2.score - v1.score;
if (i !== 0) { return i; }
@@ -1416,4 +1485,43 @@ class Translator {
frequencies.sort(compare);
}
}
+
+ _updateSortFrequencies(dictionaryEntries, dictionary, ascending) {
+ const frequencyMap = new Map();
+ for (const dictionaryEntry of dictionaryEntries) {
+ const {definitions, frequencies} = dictionaryEntry;
+ let frequencyMin = Number.MAX_SAFE_INTEGER;
+ let frequencyMax = Number.MIN_SAFE_INTEGER;
+ for (const item of frequencies) {
+ if (item.dictionary !== dictionary) { continue; }
+ const {headwordIndex, frequency} = item;
+ if (typeof frequency !== 'number') { continue; }
+ frequencyMap.set(headwordIndex, frequency);
+ frequencyMin = Math.min(frequencyMin, frequency);
+ frequencyMax = Math.max(frequencyMax, frequency);
+ }
+ dictionaryEntry.frequencyOrder = (
+ frequencyMin <= frequencyMax ?
+ (ascending ? frequencyMin : -frequencyMax) :
+ (ascending ? Number.MAX_SAFE_INTEGER : 0)
+ );
+ for (const definition of definitions) {
+ frequencyMin = Number.MAX_SAFE_INTEGER;
+ frequencyMax = Number.MIN_SAFE_INTEGER;
+ const {headwordIndices} = definition;
+ for (const headwordIndex of headwordIndices) {
+ const frequency = frequencyMap.get(headwordIndex);
+ if (typeof frequency !== 'number') { continue; }
+ frequencyMin = Math.min(frequencyMin, frequency);
+ frequencyMax = Math.max(frequencyMax, frequency);
+ }
+ definition.frequencyOrder = (
+ frequencyMin <= frequencyMax ?
+ (ascending ? frequencyMin : -frequencyMax) :
+ (ascending ? Number.MAX_SAFE_INTEGER : 0)
+ );
+ }
+ frequencyMap.clear();
+ }
+ }
}
diff --git a/ext/js/pages/settings/settings-main.js b/ext/js/pages/settings/settings-main.js
index e8092112..73b5c22c 100644
--- a/ext/js/pages/settings/settings-main.js
+++ b/ext/js/pages/settings/settings-main.js
@@ -42,6 +42,7 @@
* SentenceTerminationCharactersController
* SettingsController
* SettingsDisplayController
+ * SortFrequencyDictionaryController
* StatusFooter
* StorageController
* TranslationTextReplacementsController
@@ -167,6 +168,9 @@ async function setupGenericSettingsController(genericSettingController) {
const collapsibleDictionaryController = new CollapsibleDictionaryController(settingsController);
collapsibleDictionaryController.prepare();
+ const sortFrequencyDictionaryController = new SortFrequencyDictionaryController(settingsController);
+ sortFrequencyDictionaryController.prepare();
+
await Promise.all(preparePromises);
document.documentElement.dataset.loaded = 'true';
diff --git a/ext/js/pages/settings/sort-frequency-dictionary-controller.js b/ext/js/pages/settings/sort-frequency-dictionary-controller.js
new file mode 100644
index 00000000..9f167ec1
--- /dev/null
+++ b/ext/js/pages/settings/sort-frequency-dictionary-controller.js
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2021 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ Enabling this option will sort search results using a specific dictionary. + This can be beneficial when using multiple dictionaries which may not have + consistent sorting information. +
++ Less… +
++ Dictionary frequency data can be represented in one of two ways: +
++ The correct mode can be determined based on the contents of the dictionary; + the Auto button attempts to auto-detect the correct value. +
++ Less… +
+