Introduce spell checker context menu and dictionaries managing
This commit is contained in:
parent
a2f3743152
commit
814cbc0ee3
5 changed files with 363 additions and 1 deletions
248
chrome/content/zotero/xpcom/dictionaries.js
Normal file
248
chrome/content/zotero/xpcom/dictionaries.js
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
/*
|
||||||
|
***** BEGIN LICENSE BLOCK *****
|
||||||
|
|
||||||
|
Copyright © 2021 Corporation for Digital Scholarship
|
||||||
|
Vienna, Virginia, USA
|
||||||
|
http://digitalscholar.org/
|
||||||
|
|
||||||
|
This file is part of Zotero.
|
||||||
|
|
||||||
|
Zotero is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Zotero 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 Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
***** END LICENSE BLOCK *****
|
||||||
|
*/
|
||||||
|
|
||||||
|
Zotero.Dictionaries = new function () {
|
||||||
|
let _dictionaries = [];
|
||||||
|
let _spellChecker = Cc['@mozilla.org/spellchecker/engine;1']
|
||||||
|
.getService(Ci.mozISpellCheckingEngine);
|
||||||
|
_spellChecker.QueryInterface(Ci.mozISpellCheckingEngine);
|
||||||
|
|
||||||
|
Zotero.defineProperty(this, 'baseURL', {
|
||||||
|
get: () => {
|
||||||
|
let url = ZOTERO_CONFIG.DICTIONARIES_URL;
|
||||||
|
if (!url.endsWith('/')) {
|
||||||
|
url += '/';
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Zotero.defineProperty(this, 'dictionaries', {
|
||||||
|
get: () => {
|
||||||
|
return _dictionaries;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all dictionaries
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
this.init = async function () {
|
||||||
|
let dictionariesDir = OS.Path.join(Zotero.Profile.dir, 'dictionaries');
|
||||||
|
if (!(await OS.File.exists(dictionariesDir))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let iterator = new OS.File.DirectoryIterator(dictionariesDir);
|
||||||
|
try {
|
||||||
|
await iterator.forEach(async function (entry) {
|
||||||
|
if (entry.name.startsWith('.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let dir = OS.Path.join(dictionariesDir, entry.name);
|
||||||
|
await _loadDictionary(dir);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
iterator.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available dictionaries from server
|
||||||
|
*
|
||||||
|
* @return {Promise<Object>}
|
||||||
|
*/
|
||||||
|
this.fetchDictionariesList = async function () {
|
||||||
|
let url = this.baseURL + 'dictionaries.json';
|
||||||
|
let req = await Zotero.HTTP.request('GET', url, { responseType: 'json' });
|
||||||
|
return req.response;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install the most popular dictionary for specified locale
|
||||||
|
*
|
||||||
|
* @param locale
|
||||||
|
* @return {Promise<Boolean>}
|
||||||
|
*/
|
||||||
|
this.installByLocale = async function (locale) {
|
||||||
|
let dictionaries = await this.fetchDictionariesList();
|
||||||
|
let matched = dictionaries.filter(x => x.locale === locale);
|
||||||
|
if (!matched.length) {
|
||||||
|
matched = dictionaries.filter(x => x.locale === locale.split(/[-_]/)[0]);
|
||||||
|
}
|
||||||
|
if (!matched.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
matched.sort((a, b) => b.users - a.users);
|
||||||
|
await this.install(matched[0].id);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all dictionaries targeting specific locale
|
||||||
|
*
|
||||||
|
* @param locale
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
this.removeByLocale = async function(locale) {
|
||||||
|
for (let dictionary of _dictionaries) {
|
||||||
|
if (dictionary.locales.includes(locale)
|
||||||
|
|| dictionary.locales.some(x => x === locale.split(/[-_]/)[0])) {
|
||||||
|
await this.remove(dictionary.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install dictionary by extension id,
|
||||||
|
* e.g., `en-NZ@dictionaries.addons.mozilla.org`
|
||||||
|
*
|
||||||
|
* @param {String} id - Dictionary extension id
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
this.install = async function (id) {
|
||||||
|
await this.remove(id);
|
||||||
|
let url = this.baseURL + id + '.xpi';
|
||||||
|
let xpiPath = OS.Path.join(Zotero.getTempDirectory().path, id);
|
||||||
|
let dir = OS.Path.join(Zotero.Profile.dir, 'dictionaries', id);
|
||||||
|
let zipReader = Components.classes['@mozilla.org/libjar/zip-reader;1']
|
||||||
|
.createInstance(Components.interfaces.nsIZipReader);
|
||||||
|
try {
|
||||||
|
await Zotero.File.download(url, xpiPath);
|
||||||
|
|
||||||
|
zipReader.open(Zotero.File.pathToFile(xpiPath));
|
||||||
|
zipReader.test(null);
|
||||||
|
|
||||||
|
// Create directories
|
||||||
|
let entries = zipReader.findEntries('*/');
|
||||||
|
while (entries.hasMore()) {
|
||||||
|
let entry = entries.getNext();
|
||||||
|
let destPath = OS.Path.join(dir, entry);
|
||||||
|
await Zotero.File.createDirectoryIfMissingAsync(destPath, { from: Zotero.Profile.dir });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract files
|
||||||
|
entries = zipReader.findEntries('*');
|
||||||
|
while (entries.hasMore()) {
|
||||||
|
let entry = entries.getNext();
|
||||||
|
if (entry.substr(-1) === '/') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let destPath = OS.Path.join(dir, entry);
|
||||||
|
zipReader.extract(entry, Zotero.File.pathToFile(destPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
zipReader.close();
|
||||||
|
await OS.File.remove(xpiPath);
|
||||||
|
await _loadDictionary(dir);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (await OS.File.exists(xpiPath)) {
|
||||||
|
await OS.File.remove(xpiPath);
|
||||||
|
}
|
||||||
|
if (await OS.File.exists(dir)) {
|
||||||
|
await OS.File.removeDir(dir);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove dictionary by extension id
|
||||||
|
*
|
||||||
|
* @param {String} id
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
this.remove = async function (id) {
|
||||||
|
let dictionaryIndex = _dictionaries.findIndex(x => x.id === id);
|
||||||
|
if (dictionaryIndex !== -1) {
|
||||||
|
let dictionary = _dictionaries[dictionaryIndex];
|
||||||
|
try {
|
||||||
|
let manifestPath = OS.Path.join(dictionary.dir, 'manifest.json');
|
||||||
|
let manifest = await Zotero.File.getContentsAsync(manifestPath);
|
||||||
|
manifest = JSON.parse(manifest);
|
||||||
|
for (let locale in manifest.dictionaries) {
|
||||||
|
let dicPath = manifest.dictionaries[locale];
|
||||||
|
let affPath = OS.Path.join(dictionary.dir, dicPath.slice(0, -3) + 'aff');
|
||||||
|
_spellChecker.removeDictionary(locale, Zotero.File.pathToFile(affPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
await OS.File.removeDir(dictionary.dir);
|
||||||
|
_dictionaries.splice(dictionaryIndex, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all dictionaries
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
this.update = async function () {
|
||||||
|
let availableDictionaries = await this.fetchDictionariesList();
|
||||||
|
for (let dictionary of _dictionaries) {
|
||||||
|
let availableDictionary = availableDictionaries.find(x => x.id === dictionary.id);
|
||||||
|
if (availableDictionary && availableDictionary.version > dictionary.version) {
|
||||||
|
await this.install(availableDictionary.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load dictionary from specified dir
|
||||||
|
*
|
||||||
|
* @param {String} dir
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async function _loadDictionary(dir) {
|
||||||
|
let manifestPath = OS.Path.join(dir, 'manifest.json');
|
||||||
|
let manifest = await Zotero.File.getContentsAsync(manifestPath);
|
||||||
|
manifest = JSON.parse(manifest);
|
||||||
|
let id;
|
||||||
|
if (manifest.applications && manifest.applications.gecko) {
|
||||||
|
id = manifest.applications.gecko.id;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
id = manifest.browser_specific_settings.gecko.id;
|
||||||
|
}
|
||||||
|
let version = manifest.version;
|
||||||
|
let locales = [];
|
||||||
|
for (let locale in manifest.dictionaries) {
|
||||||
|
locales.push(locale);
|
||||||
|
let dicPath = manifest.dictionaries[locale];
|
||||||
|
let affPath = OS.Path.join(dir, dicPath.slice(0, -3) + 'aff');
|
||||||
|
_spellChecker.addDictionary(locale, Zotero.File.pathToFile(affPath));
|
||||||
|
}
|
||||||
|
_dictionaries.push({ id, locales, version, dir });
|
||||||
|
}
|
||||||
|
};
|
|
@ -23,6 +23,8 @@
|
||||||
***** END LICENSE BLOCK *****
|
***** END LICENSE BLOCK *****
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm");
|
||||||
|
|
||||||
// Note: TinyMCE is automatically doing some meaningless corrections to
|
// Note: TinyMCE is automatically doing some meaningless corrections to
|
||||||
// note-editor produced HTML. Which might result to more
|
// note-editor produced HTML. Which might result to more
|
||||||
// conflicts, especially in group libraries
|
// conflicts, especially in group libraries
|
||||||
|
@ -73,7 +75,8 @@ class EditorInstance {
|
||||||
});
|
});
|
||||||
this._prefObserverIDs = [
|
this._prefObserverIDs = [
|
||||||
Zotero.Prefs.registerObserver('note.fontSize', this._handleFontChange),
|
Zotero.Prefs.registerObserver('note.fontSize', this._handleFontChange),
|
||||||
Zotero.Prefs.registerObserver('note.fontFamily', this._handleFontChange)
|
Zotero.Prefs.registerObserver('note.fontFamily', this._handleFontChange),
|
||||||
|
Zotero.Prefs.registerObserver('layout.spellcheckDefault', this._handleSpellCheckChange, true)
|
||||||
];
|
];
|
||||||
|
|
||||||
// Run Cut/Copy/Paste with chrome privileges
|
// Run Cut/Copy/Paste with chrome privileges
|
||||||
|
@ -210,6 +213,20 @@ class EditorInstance {
|
||||||
this._postMessage({ action: 'updateFont', font: this._getFont() });
|
this._postMessage({ action: 'updateFont', font: this._getFont() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleSpellCheckChange = () => {
|
||||||
|
try {
|
||||||
|
let spellChecker = this._getSpellChecker();
|
||||||
|
let value = Zotero.Prefs.get('layout.spellcheckDefault', true);
|
||||||
|
if (!value && spellChecker.enabled
|
||||||
|
|| value && !spellChecker.enabled) {
|
||||||
|
spellChecker.toggleEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_showInLibrary(ids) {
|
_showInLibrary(ids) {
|
||||||
if (!Array.isArray(ids)) {
|
if (!Array.isArray(ids)) {
|
||||||
ids = [ids];
|
ids = [ids];
|
||||||
|
@ -788,9 +805,93 @@ class EditorInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
appendItems(this._popup, itemGroups);
|
appendItems(this._popup, itemGroups);
|
||||||
|
|
||||||
|
// TODO: Localize
|
||||||
|
|
||||||
|
// Spell checker
|
||||||
|
let spellChecker = this._getSpellChecker();
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
var separator = this._popup.ownerDocument.createElement('menuseparator');
|
||||||
|
this._popup.appendChild(separator);
|
||||||
|
// Check Spelling
|
||||||
|
var menuitem = this._popup.ownerDocument.createElement('menuitem');
|
||||||
|
menuitem.setAttribute('label', 'Check Spelling');
|
||||||
|
menuitem.setAttribute('checked', spellChecker.enabled);
|
||||||
|
menuitem.addEventListener('command', () => {
|
||||||
|
// Possible values: 0 - off, 1 - only multi-line, 2 - multi and single line input boxes
|
||||||
|
Zotero.Prefs.set('layout.spellcheckDefault', spellChecker.enabled ? 0 : 1, true);
|
||||||
|
});
|
||||||
|
this._popup.append(menuitem);
|
||||||
|
|
||||||
|
if (spellChecker.enabled) {
|
||||||
|
// Languages menu
|
||||||
|
var menu = this._popup.ownerDocument.createElement('menu');
|
||||||
|
menu.setAttribute('label', 'Languages');
|
||||||
|
this._popup.append(menu);
|
||||||
|
// Languages menu popup
|
||||||
|
var menupopup = this._popup.ownerDocument.createElement('menupopup');
|
||||||
|
menu.append(menupopup);
|
||||||
|
|
||||||
|
spellChecker.addDictionaryListToMenu(menupopup, null);
|
||||||
|
|
||||||
|
for (var menuitem of menupopup.children) {
|
||||||
|
// 'spell-check-dictionary-en-US'
|
||||||
|
let locale = menuitem.id.slice(23);
|
||||||
|
let label = Zotero.Locale.availableLocales[locale];
|
||||||
|
if (!label) {
|
||||||
|
for(let key in Zotero.Locale.availableLocales) {
|
||||||
|
if (key.split('-')[0] === locale) {
|
||||||
|
label = Zotero.Locale.availableLocales[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
menuitem.setAttribute('label', label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
var separator = this._popup.ownerDocument.createElement('menuseparator');
|
||||||
|
menupopup.appendChild(separator);
|
||||||
|
// Add Dictionaries
|
||||||
|
var menuitem = this._popup.ownerDocument.createElement('menuitem');
|
||||||
|
menuitem.setAttribute('label', 'Add Dictionaries...');
|
||||||
|
menuitem.addEventListener('command', () => {
|
||||||
|
Zotero.Utilities.Internal.openPreferences('zotero-prefpane-advanced');
|
||||||
|
});
|
||||||
|
menupopup.append(menuitem);
|
||||||
|
|
||||||
|
let selection = this._iframeWindow.getSelection();
|
||||||
|
if (selection) {
|
||||||
|
spellChecker.initFromEvent(
|
||||||
|
selection.anchorNode,
|
||||||
|
selection.anchorOffset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstElementChild = this._popup.firstElementChild;
|
||||||
|
let suggestionCount = spellChecker.addSuggestionsToMenu(this._popup, firstElementChild, 5);
|
||||||
|
if (suggestionCount) {
|
||||||
|
let separator = this._popup.ownerDocument.createElement('menuseparator');
|
||||||
|
this._popup.insertBefore(separator, firstElementChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._popup.openPopupAtScreen(x, y, true);
|
this._popup.openPopupAtScreen(x, y, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getSpellChecker() {
|
||||||
|
let spellChecker = new InlineSpellChecker();
|
||||||
|
let editingSession = this._iframeWindow
|
||||||
|
.getInterface(Ci.nsIWebNavigation)
|
||||||
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||||
|
.getInterface(Ci.nsIEditingSession);
|
||||||
|
spellChecker.init(editingSession.getEditorForWindow(this._iframeWindow));
|
||||||
|
return spellChecker;
|
||||||
|
}
|
||||||
|
|
||||||
async _ensureNoteCreated() {
|
async _ensureNoteCreated() {
|
||||||
if (!this._item.id) {
|
if (!this._item.id) {
|
||||||
return this._item.saveTx();
|
return this._item.saveTx();
|
||||||
|
@ -861,6 +962,16 @@ class EditorInstance {
|
||||||
Zotero.crash(true);
|
Zotero.crash(true);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset spell checker as ProseMirror DOM modifications are
|
||||||
|
// often ignored otherwise
|
||||||
|
try {
|
||||||
|
let spellChecker = this._getSpellChecker();
|
||||||
|
spellChecker.toggleEnabled();
|
||||||
|
spellChecker.toggleEnabled();
|
||||||
|
} catch(e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -725,6 +725,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
yield Zotero.Relations.init();
|
yield Zotero.Relations.init();
|
||||||
yield Zotero.Retractions.init();
|
yield Zotero.Retractions.init();
|
||||||
yield Zotero.NoteBackups.init();
|
yield Zotero.NoteBackups.init();
|
||||||
|
yield Zotero.Dictionaries.init();
|
||||||
|
|
||||||
// Migrate fields from Extra that can be moved to item fields after a schema update
|
// Migrate fields from Extra that can be moved to item fields after a schema update
|
||||||
yield Zotero.Schema.migrateExtraFields();
|
yield Zotero.Schema.migrateExtraFields();
|
||||||
|
|
|
@ -95,6 +95,7 @@ const xpcomFilesLocal = [
|
||||||
'data/searches',
|
'data/searches',
|
||||||
'data/tags',
|
'data/tags',
|
||||||
'db',
|
'db',
|
||||||
|
'dictionaries',
|
||||||
'duplicates',
|
'duplicates',
|
||||||
'editorInstance',
|
'editorInstance',
|
||||||
'feedReader',
|
'feedReader',
|
||||||
|
|
|
@ -28,6 +28,7 @@ var ZOTERO_CONFIG = {
|
||||||
CREDITS_URL: 'https://www.zotero.org/support/credits_and_acknowledgments',
|
CREDITS_URL: 'https://www.zotero.org/support/credits_and_acknowledgments',
|
||||||
LICENSING_URL: 'https://www.zotero.org/support/licensing',
|
LICENSING_URL: 'https://www.zotero.org/support/licensing',
|
||||||
GET_INVOLVED_URL: 'https://www.zotero.org/getinvolved',
|
GET_INVOLVED_URL: 'https://www.zotero.org/getinvolved',
|
||||||
|
DICTIONARIES_URL: 'https://download.zotero.org/dictionaries/',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof process === 'object' && process + '' === '[object process]'){
|
if (typeof process === 'object' && process + '' === '[object process]'){
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue