Source: models/helpers/ml-string.js

'use strict';
// External deps
const decodeHTML = require('entities').decodeHTML;
const stripTags = require('striptags');

// Internal deps
const languages = require('../../locales/languages');
const langKeys = languages.getValidLanguagesAndUndetermined();
const thinky = require('../../db');
const type = thinky.type;

/**
 * Helper methods for handling multilingual strings.
 *
 * @namespace MlString
 */
const mlString = {

  /**
   * Obtain a Thinky type definition for a multilingual string object which
   * permits only strings in the supported languages defined in `locales/`.
   * Language keys like 'en' are used as object keys, so you can use syntax like
   * `label.en`, or `aliases.fr[0]` for arrays.
   *
   * @param {Object} [options]
   *  settings for this type definition
   * @param {Number} options.maxLength
   *  maximum length for any individual string. If not set, no maximum is
   *  enforced.
   * @param {Boolean} options.array=false
   *  Set this to true for strings of the form
   *
    *  ````
   *  {
   *    en: ['something', 'other'],
   *    de: ['something']
   *  }
   *  ````
   *
   *  For arrays of multilingual strings, instead encapsulate getSchema in an
   *  array in the schema definition.
   * @returns {TypeObject}
   *  type definition
   * @memberof MlString
   */
  getSchema({
      maxLength = undefined,
      array = false
    } = {}) {

    let schema = {};
    for (let key of langKeys) {
      let typeDef = type.string();
      if (maxLength)
        typeDef.max(maxLength);

      if (array)
        typeDef = type.array(typeDef);

      schema[key] = typeDef;

    }

    return type
      .object()
      .schema(schema)
      .allowExtra(false);

  },

  /**
   * The result of resolving a multilingual string to a given language.
   *
   * @typedef {Object} ResolveResult
   * @property {Object} result
   * @property {String} result.str
   *  string in the best available language for the original lookup
   * @property {String} result.lang
   *  the language code identifying that language
   */

  /**
   * Find the best fit for a given language from a multilingual string object,
   * taking into account fallbacks.
   *
   * @param {String} lang
   *  the preferred language code of the target string
   * @param {Object} strObj
   *  a multilingual string object
   * @returns {ResolveResult}
   *  or undefined if we can't find any suitable string
   * @memberof MlString
   */
  resolve(lang, strObj) {
    if (strObj === undefined)
      return undefined;

    // We have a string in the specified language
    // Note that emptying a string reverts back to other available languages
    if (strObj[lang] !== undefined && strObj[lang] !== '')
      return {
        str: strObj[lang],
        lang
      };

    // Try specific fallbacks for this language first, e.g. European Portuguese
    // for Brazilian Portuguese. English is a declared fallback for all languages.
    let fallbackLanguages = languages.getFallbacks(lang);
    for (let fallbackLanguage of fallbackLanguages) {
      if (strObj[fallbackLanguage] !== undefined && strObj[fallbackLanguage] !== '')
        return {
          str: strObj[fallbackLanguage],
          lang: fallbackLanguage
        };
    }

    // Pick first available language
    let availableLanguages = Object.keys(strObj);
    for (let availableLanguage of availableLanguages) {
      if (languages.isValid(availableLanguage) &&
        strObj[availableLanguage] !== undefined &&
        strObj[availableLanguage] !== '')
        return {
          str: strObj[availableLanguage],
          lang: availableLanguage
        };
    }

    // This may not be a valid multilingual string object at all, or all strings
    // are empty.
    return undefined;

  },


  /**
   * @param {Object} strObj
   *  a multilingual string object
   * @returns {Object}
   *  string object with HTML entities decoded and HTML elements stripped
   * @memberof MlString
   */
  stripHTML(strObj) {
    if (typeof strObj !== 'object')
      return strObj;

    let rv = {};
    for (let lang in strObj) {
      if (typeof strObj[lang] == 'string')
        rv[lang] = stripTags(decodeHTML(strObj[lang]));
      else
        rv[lang] = strObj[lang];
    }
    return rv;
  },

  /**
   * @param {Object[]} strObjArr
   *  an array of multilingual string objects
   * @returns {Object[]}
   *  string object array with HTML entities decoded and HTML elements stripped
   * @memberof MlString
   */
  stripHTMLFromArray(strObjArr) {
    if (!Array.isArray(strObjArr))
      return strObjArr;
    else
      return strObjArr.map(mlString.stripHTML);
  }

};

module.exports = mlString;