Home Reference Source Test Repository

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();
  }
}