src/dictionary-sync.js
import path from 'path';
import mkdirp from 'mkdirp';
import {getURLForHunspellDictionary} from './node-spellchecker';
import {getInstalledKeyboardLanguages} from 'keyboard-layout';
import {Observable} from 'rx';
import {fs} from './promisify';
import {normalizeLanguageCode} from './utility';
let d = require('debug-electron')('electron-spellchecker:dictionary-sync');
const app = process.type === 'renderer' ?
require('electron').remote.app :
require('electron').app;
const {downloadFileOrUrl} =
require('electron-remote').requireTaskPool(require.resolve('electron-remote/remote-ajax'));
/**
* DictioanrySync handles downloading and saving Hunspell dictionaries. Pass it
* to {{SpellCheckHandler}} to configure a custom cache directory.
*/
export default class DictionarySync {
/**
* Creates a DictionarySync
*
* @param {String} cacheDir The path to a directory to store dictionaries.
* If not given, the Electron user data directory
* will be used.
*/
constructor(cacheDir=null) {
this.cacheDir = cacheDir || path.join(app.getPath('userData'), 'dictionaries');
mkdirp.sync(this.cacheDir);
}
/**
* Override the default logger for this class. You probably want to use
* {{setGlobalLogger}} instead
*
* @param {Function} fn The function which will operate like console.log
*/
static setLogger(fn) {
d = fn;
}
/**
* Loads the dictionary for a given language code, trying first to load a
* local version, then downloading it. You probably don't want this method
* directly, but the wrapped version
* {{loadDictionaryForLanguageWithAlternatives}} which is in {{SpellCheckHandler}}.
*
* @param {String} langCode The language code (i.e. 'en-US')
* @param {Boolean} cacheOnly If true, don't load the file content into
* memory, only download it
*
* @return {Promise<Buffer|String>} A Buffer of the file contents if
* {{cacheOnly}} is False, or the path to
* the file if True.
*/
async loadDictionaryForLanguage(langCode, cacheOnly=false) {
d(`Loading dictionary for language ${langCode}`);
if (process.platform === 'darwin') return new Buffer([]);
let lang = normalizeLanguageCode(langCode);
let target = path.join(this.cacheDir, `${lang}.bdic`);
let fileExists = false;
try {
if (fs.existsSync(target)) {
fileExists = true;
d(`Returning local copy: ${target}`);
let ret = await fs.readFile(target, {});
if (ret.length < 64*1024) {
throw new Error("File exists but is most likely bogus");
}
}
} catch (e) {
d(`Failed to read file ${target}: ${e.message}`);
}
if (fileExists) {
try {
await fs.unlink(target);
} catch (e) {
d("Can't clear out file, bailing");
throw e;
}
}
let url = getURLForHunspellDictionary(lang);
d(`Actually downloading ${url}`);
await downloadFileOrUrl(url, target);
if (cacheOnly) return target;
let ret = await fs.readFile(target, {});
if (ret.length < 64*1024) {
throw new Error("File exists but is most likely bogus");
}
return ret;
}
/**
* Pre-download dictionaries for languages that the user is likely to speak
* (based usually on their keyboard layouts). Note that this method only works
* on Windows currently.
*
* @param {Array<String>} languageList Override the list of languages to
* download, for testing.
*
* @return {Promise<Array<String>>} A list of strings to dictionaries
* that were downloaded.
*/
preloadDictionaries(languageList=null) {
return Observable.from(languageList || getInstalledKeyboardLanguages())
.flatMap((x) => Observable.fromPromise(this.loadDictionaryForLanguage(x, true)))
.reduce((acc,x) => { acc.push(x); return acc; }, [])
.toPromise();
}
}