Source: lib/tools.js

const { formatDate, parse } = require('sesajs-date')
const util = require('util')
const { hasProp } = require('sesajstools')

/**
 * Vérifie qu'une valeur est entière dans l'intervalle donné et recadre sinon (avec un message dans le log d'erreur)
 * @memberOf tools
 * @param int La valeur à contrôler
 * @param min Le minimum exigé
 * @param max Le maximum exigé
 * @param label Un label pour le message d'erreur (qui indique ce qui a été recadré)
 * @returns {Integer}
 */
function encadre (int, min, max, label) {
  let value = parseInt(int)
  if (value < min) {
    log.error(label + ' trop petit (' + value + '), on le fixe à ' + min)
    value = min
  }
  if (value > max) {
    log.error(label + ' trop grand (' + value + '), on le fixe à ' + max)
    value = max
  }
  return value
}

/**
 * Garanti que le retour sera du type demandé, mis à defaultValue (lui ne sera pas casté) si falsy
 * @param {*} value
 * @param {string} type string|number|integer|boolean
 * @param {*} defaultValue
 * @return {*}
 */
function ensure (value, type, defaultValue) {
  const needToCast = typeof value !== type // eslint-disable-line valid-typeof
  if (needToCast) {
    switch (type) {
      case 'string':
        value = (value && String(value)) || defaultValue || ''
        break
      case 'number':
        value = (value && Number(value)) || defaultValue || 0
        break
      case 'integer':
        value = (value && Math.round(Number(value))) || defaultValue || 0
        break
      case 'boolean':
        value = Boolean(value)
        break
      default:
        console.error(new Error(`type ${type} non géré`))
    }
  } else if (!value && defaultValue) {
    value = defaultValue
  }
  return value
}

/**
 * Transforme une liste en tableau de mots valides pour des ids ([A-Za-z0-9_\-])
 * @memberOf tools
 * @param {string} list
 * @returns {string[]} la liste des ids récupérés (tous les caractères autres que lettres, chiffres, tiret et underscore ont été virés)
 */
function idListToArray (list) {
  let retour = []
  if (typeof list === 'string') retour = list.match(/([-A-Za-z0-9_]+)/g)
  else log.error(new TypeError('faut me donner un type string'))

  return retour
}

/**
 * Retourne true si l'url contient /api/
 * @memberOf tools
 * @param url
 * @returns {boolean}
 */
function isApi (url) {
  return /^\/api\//.test(url)
}

/**
 * Retourne true si ar1 et ar2 ont autant d'élément tous égaux (comparaison ===)
 * @todo déplacer ça dans sesajstools
 * @param {Array} ar1
 * @param {Array} ar2
 * @return {boolean}
 */
function isSameSimpleArray (ar1, ar2) {
  if (!Array.isArray(ar1) || !Array.isArray(ar2)) throw Error('Array expected')
  if (ar1.length !== ar2.length) return false
  return ar1.every((elt, i) => elt === ar2[i])
}

/**
 * Vérif basique que obj est bien une Entity entityName (si entityName n'est pas fourni
 * ça renvoie true si obje est une entity Lassi)
 * @param {Object} obj
 * @param {string} [entityName]
 * @return {boolean}
 */
function isEntity (obj, entityName) {
  if (
    !obj ||
    !obj.definition ||
    !obj.constructor ||
    obj.constructor.name !== 'Entity'
  ) return false
  if (entityName) return obj.definition.name === entityName
  // si on voulait juste savoir si c'était une Entity sans préciser laquelle,
  // avoir un constructor nommé Entity nous suffit
  return true
}

/**
 * Retourne true si l'url concerne un fichier statique
 * (statique i.e. les extensions susceptibles d'exister dans sesatheque, c'est pas exaustif)
 * @memberOf tools
 * @param url
 * @returns {boolean}
 */
function isStatic (url) {
  return /\.(js|css|png|ico|jpg|jpeg|gif)(\?.*)?$/.test(url)
}

/**
 * Retourne true si l'url concerne une url publique (avec /public/ dedans, html ou json)
 * @memberOf tools
 * @param url
 * @returns {boolean}
 */
function isPublic (url) {
  return /\/public\//.test(url)
}
/**
 * Génère le code html d'un lien
 * @memberOf tools
 * @param path Le path (absolu ou relatif)
 * @param texte Le texte à afficher
 * @param {string|Array} [args] Des arguments à ajouter au path (séparateur slash)
 * @returns {string} Le code html du tag a
 */
function link (path, texte, args) {
  if (args) {
    if (Array.isArray(args)) path += args.join('/')
    else path += '/' + args
  }

  return '<a href="' + path + '">' + texte + '</a>'
}

/**
 * Génère le code html d'un lien avec les args en queryString
 * @memberOf tools
 * @param path Le path (absolu ou relatif)
 * @param texte Le texte à afficher
 * @param {Object} [args] Des arguments à ajouter en queryString
 * @returns {string} Le code html du tag a
 */
function linkQs (path, texte, args) {
  if (args) {
    const paires = []
    for (const p in args) {
      if (hasProp(args, p)) paires.push(p + '=' + encodeURIComponent(args[p]))
    }
    if (paires.length) path += '?' + paires.join('&')
  }

  return '<a href="' + path + '">' + texte + '</a>'
}

/**
 * Remplace les espaces par des underscores et vire les caractères de contrôle d'une chaine (\n compris)
 * @see http://unicode-table.com/en/
 * @memberOf tools
 * @param {string} source La chaîne à nettoyer
 * @returns {string} La chaîne nettoyée
 */
function sanitizeHashKey (source) {
  return source.replace(/ /g, '_').replace(/[\x00-\x20\x7F-\xA0]/, '') // eslint-disable-line no-control-regex
}

/**
 * Vire tous les caractères autres que lettres (non accentuées), chiffres, _ et -
 * @memberOf tools
 * @param source
 * @returns {void|*|{value}|string|XML}
 */
function sanitizeStrict (source) {
  return source.replace(/[^-a-zA-Z0-9_]/, '')
}

/**
 * Incorpore des arguments à un message, façon sprintf
 * pas très intéressant si n arguments, util.format fait la même chose, mais tolère un tableau d'arguments en 2e param
 * @memberOf tools
 * @param {string} message
 * @param {string|Array} args Les arguments, en liste ou en tableau
 * @returns {string}
 */
function strFormat (message, args) {
  let retour
  if (Array.isArray(args)) {
    // faut ajouter message en 1er argument et le passer à util.format
    retour = util.format.apply(null, args.unshift(message))
  } else {
    // pas la peine de bosser pour rien
    if (arguments.length < 2) retour = message
    // on transmet tel quel
    else retour = util.format.apply(null, arguments)
  }

  return retour
}

/**
 * Elimine les tags html d'une string
 * @memberOf tools
 * @param {string} source
 * @returns {string}
 */
function stripTags (source) {
  return source.replace(/(<([^>]+)>)/ig, '')
}

/**
 * Converti un timestamp ou un chaine en objet Date
 * @memberOf tools
 * @param {number|string|Date} value Un timestamp (en ms ou s) ou une chaine ('DD/MM/YYYY' ou ISO_8601)
 * @returns {Date} L'objet Date ou undefined si la conversion a échoué (value invalide)
 */
function toDate (value) {
  // Date, rien à faire
  if (value instanceof Date) return value
  // number, supposé timestamp
  if (value > 0) {
    // c'est un timestamp
    let ts = parseInt(value, 10)
    if (ts < 11001001001) ts = ts * 1000 // c'était des s, on passe en ms
    // 11001001001 est arbitraire, correspond à août 1970 en ms et 2318 en s)
    return new Date(ts)
  }
  // sting => parse
  if (typeof value === 'string') {
    try {
      return parse(value)
    } catch (error) {
      console.error(error)
      return undefined
    }
  }

  console.error(Error('valeur invalide pour une transformation en date'), value)
  return undefined
}

/**
 * Formate un objet Date en DD/MM/YYYY
 * @memberOf tools
 * @param {Date} date
 * @returns {string} Le jour au format DD/MM/YYYY
 */
function toJour (date) {
  return formatDate(date, { fr: true })
}

/**
 * Un assemblage de fonctions utilitaires
 * @service tools
 */
module.exports = {
  encadre,
  ensure,
  idListToArray,
  isApi,
  isEntity,
  isSameSimpleArray,
  isStatic,
  isPublic,
  link,
  linkQs,
  sanitizeHashKey,
  sanitizeStrict,
  strFormat,
  stripTags,
  toDate,
  toJour
}