Source: index.js

/**
 * This file is part of Sesatheque.
 *   Copyright 2014-2015, Association Sésamath
 *
 * Sesatheque is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation.
 *
 * Sesatheque 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 Sesatheque (LICENCE.txt).
 * @see http://www.gnu.org/licenses/agpl.txt
 *
 *
 * Ce fichier fait partie de l'application Sésathèque, créée par l'association Sésamath.
 *
 * Sésathèque est un logiciel libre ; vous pouvez le redistribuer ou le modifier suivant
 * les termes de la GNU Affero General Public License version 3 telle que publiée par la
 * Free Software Foundation.
 * Sésathèque est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE,
 * sans même la garantie tacite de QUALITÉ MARCHANDE ou d'ADÉQUATION à UN BUT PARTICULIER.
 * Consultez la GNU Affero General Public License pour plus de détails.
 * Vous devez avoir reçu une copie de la GNU General Public License en même temps que Sésathèque
 * (cf LICENCE.txt et http://vvlibri.org/fr/Analyse/gnu-affero-general-public-license-v3-analyse
 * pour une explication en français)
 */
'use strict'

// ajoute du forEach sur les Array si le navigateur connait pas ça
if (!Array.prototype.forEach) {
  Array.prototype.forEach = function (fn) { // eslint-disable-line no-extend-native
    for (var i = 0; i < this.length; i++) {
      // on passe en argument (eltDuTableau, index, tableau)
      fn(this[i], i, this)
    }
  }
}

var specialHtmlChars = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;',
  "'": '&#039;'
}

/**
 * Retourne le nb de slash dans la string
 * @param {sting} str
 * @return {number} le nombre de slashes dans str
 * @throws {TypeError} Si str n'était pas une string
 */
function countSlash (str) {
  if (typeof str !== 'string') throw new TypeError('countSlash n’accepte qu’une string en paramètre')
  return str.replace(/[^/]+/g, '').length
}

/**
 * Échappe les caractères spéciaux en les remplaçant par leur équivalent html
 * Merci à jbo5112 https://stackoverflow.com/a/4835406
 * cf https://jsperf.com/escape-html-special-chars/11
 * @param {string} str
 * @param {boolean} [all] passer true pour remplacer les 5 caractères "&<>' (sinon ça ne remplace que <), attention dans ce cas ce n'est plus invariant (il ne faut pas rappeler la fct sur une chaîne qui y serait déjà passée)
 * @return {void|string|*}
 */
function escapeForHtml (str, all) {
  if (!all) return str.replace(/</g, '&lt;')
  return str.replace(/[&<>"']/g, function (char) { return specialHtmlChars[char] })
}

/**
 * Formate une date, en retournant format avec les %x interprétés (si x est connu, laissé tel quel sinon).
 * Attention, pas la même syntaxe que an-format-date (©arNuméral).
 *
 * Formats gérés comme la commande unix date
 * - %% symbole %
 * - %F YYYY-mm-dd
 * - %T HH:MM:SS
 * - %d jour du mois (01 à 31)
 * - %m n° du mois (01 à 12)
 * - %Y année sur 4 chiffres
 * - %y année sur 2 chiffres
 * - %H heure (00 à 23)/
 * - %M minutes (00 à 59)
 * - %S secondes (00 à 59)
 * - %s timestamp (secondes depuis 01/01/1970/)
 * Formats ajoutés (o était la seule lettre dispo)
 * - %o millisecondes (000 à 999)
 * - %O heure avec ms (idem %T.%o ou %H:%M:%S.%o)
 * @param {string} format
 * @param {Date} [date] La date à formater (now si non fourni)
 * @return {string}
 */
function formatDate (format, date) {
  function pad0 (val, nb) {
    return pad(val, '0', nb)
  }
  if (!date) date = new Date()
  else if (!(date instanceof Date)) throw new TypeError('date is not a Date')
  if (typeof format !== 'string') throw new TypeError('format is not a string')
  return format.replace(/%(.)/g, function (capture, formatChar) {
    switch (formatChar) {
      case 'd': return pad0(date.getDate())
      case 'F': return String(date.getFullYear()) + '-' + pad0(date.getMonth() + 1) + '-' + pad0(date.getDate()) // '%Y-%m-%d'
      case 'H': return pad0(date.getHours())
      case 'm': return pad0(date.getMonth() + 1)
      case 'M': return pad0(date.getMinutes())
      case 'o': return pad0(date.getMilliseconds(), 3)
      case 'O': return pad0(date.getHours()) + ':' + pad0(date.getMinutes()) + ':' + pad0(date.getSeconds()) + '.' + pad0(date.getMilliseconds(), 3) // '%H:%M:%S.%o'
      case 's': return pad0(Math.round(date.getTime() / 1000))
      case 'S': return pad0(date.getSeconds())
      case 'T': return pad0(date.getHours()) + ':' + pad0(date.getMinutes()) + ':' + pad0(date.getSeconds()) // '%H:%M:%S'
      case 'y': return String(date.getFullYear()).substr(2)
      case 'Y': return date.getFullYear()
      case '%': return '%' // pour remplacer %% par %
      // On change rien si on connait pas le format demandé
      default: return '%' + formatChar
    }
  })
}

/**
 * Renvoie un token aléatoire de 22 caractères
 * Pas aussi random ni unique que l'usage de crypto ou d'un module uuid
 * mais suffisant dans pas mal de cas (utiliser an-uuid sinon)
 * @returns {string}
 */
function getToken () {
  return Math.random().toString(36).substr(2) + Math.random().toString(36).substr(2)
}

/**
 * Retourne true si object est un objet avec la propriété prop
 * @param object
 * @param prop
 * @return {boolean}
 */
function hasProp (object, prop) {
  if (typeof object === 'object') return Object.prototype.hasOwnProperty.call(object, prop)
  return false
}

/**
 * @param {*} arg
 * @returns {boolean}
 */
function isArray (arg) {
  return Array.isArray(arg)
}

/**
 * true si value est un array vide (mais false s'il contient un élément false|undefined)
 * @param value
 * @returns {boolean}
 */
function isArrayEmpty (value) {
  return Boolean(Array.isArray(value) && value.length === 0)
}

/**
 * true si value est un array non vide (même s'il contient un seul élément false|undefined|null|'')
 * @param value
 * @returns {boolean}
 */
function isArrayNotEmpty (value) {
  return Boolean(Array.isArray(value) && value.length)
}

/**
 * @param arg
 * @returns {boolean}
 */
function isDate (arg) {
  return typeof arg === 'object' && Object.prototype.toString.call(arg) === '[object Date]'
}

/**
 * Thanks to bugsnag-js
 * @param {*} value
 * @return {boolean}
 */
function isError (value) {
  switch (Object.prototype.toString.call(value)) {
    case '[object Error]':
      return true
    case '[object Exception]':
      return true
    case '[object DOMException]':
      return true
    default:
      return value instanceof Error
  }
}

/**
 * Retourne true si l'argument est une fonction
 * cf https://github.com/lodash/lodash/blob/4.11.2/lodash.js#L10826
 * cette implémentation plus simple sur typeof bug dans phandomJs 1.9 qui retourne function pour typeof NodeList
 * @param {*} arg
 * @returns {boolean}
 */
function isFunction (arg) {
  return (typeof arg === 'function')
}

/**
 * Indique si un array contient un élément
 * @param {Array} ar  Le tableau dans lequel chercher
 * @param {*}     elt L'élément à chercher (type natif, marche pas si c'est un objet littéral)
 * @returns {boolean}
 * @throws {Error} si on lui passe pas un Array en 1er argument
 */
function isInArray (ar, elt) {
  if (!Array.isArray(ar)) throw new Error('isInArray veut un tableau en 1er argument')
  return ar.indexOf(elt) !== -1
}

/**
 * Retourne true si c'est un entier (utiliser Number.isInteger si c'est dispo)
 * @param arg
 * @param {boolean} [isStringAllowed=false] Passer true pour accepter les strings comme "42"
 * @returns {boolean}
 */
function isInt (arg, isStringAllowed) {
  switch (typeof arg) {
    case 'number':
      return Math.floor(arg) === arg
    case 'string':
      return Boolean(isStringAllowed && arg && Math.floor(arg) == arg) // eslint-disable-line eqeqeq
    default:
      return false
  }
}

/**
 * Retourne true pour un entier (number) positif
 * @param arg
 * @param {boolean} [strict=false] passer true pour que 0 renvoie false
 * @param {boolean} [isStringAllowed=false] Passer true pour accepter les strings comme "42"
 * @returns {boolean}
 */
function isIntPos (arg, strict, isStringAllowed) {
  return isInt(arg, isStringAllowed) && (strict ? arg > 0 : arg >= 0)
}

/**
 * Retourne true sur un objet (donc aussi une fonction, mais pas null)
 * Piqué à lodash (https://github.com/lodash/lodash/blob/4.11.2/lodash.js#L10921)
 * @param {*} arg
 * @returns {boolean}
 */
function isObject (arg) {
  var type = typeof arg
  return !!arg && (type === 'object' || type === 'function')
}

/**
 * Retourne true pour les objets qui n'ont pas d'autre constructeur qu'object (ni null, ni fonction ni regexp ni date ni array…)
 * @param {*} arg
 * @returns {boolean}
 */
function isObjectPlain (arg) {
  return !!arg && typeof arg === 'object' && Object.prototype.toString.call(arg) === '[object Object]'
}

/**
 * Retourne true pour les RegExp
 * @param arg
 * @returns {boolean}
 */
function isRegExp (arg) {
  return !!arg && typeof arg === 'object' && Object.prototype.toString.call(arg) === '[object RegExp]'
}

/**
 * Retourne true si l'argument est une string
 * @param {*} arg
 * @returns {boolean}
 */
function isString (arg) {
  return (typeof arg === 'string')
}

/**
 * @param arg
 * @returns {boolean}
 */
function isUndefined (arg) {
  return typeof arg === 'undefined'
}

/**
 * Retourne true si l'argument est une url absolue
 * @param {*} arg
 * @returns {boolean}
 */
function isUrlAbsolute (arg) {
  return (typeof arg === 'string' && /^https?:\/\/[a-z0-9\-._]+(:[0-9]+)?(\/|$)/.test(arg))
}

/**
 * Complete la valeur avec des caractères initiaux pour atteindre la longueur voulue
 * @param {number|string} value
 * @param {string} [char=0| ] Le caractère à ajouter au début (par défaut 0 si value est un nombre, espace sinon)
 * @param {number) [nb=2] Nb de caractères finaux voulus
 * @return {string} padded value
 */
function pad (value, char, nb) {
  // check char
  if (char) {
    if (typeof char !== 'string') char = String(char)
    if (char.length !== 1) {
      console.error(new Error('char invalide ' + char))
      char = undefined
    }
  }
  // init char
  if (!char) {
    char = typeof value === 'number' ? '0' : ' '
  }
  // cast value
  if (typeof value !== 'string') value = String(value)
  // default nb
  if (!nb) nb = 2
  // padding
  while (value.length < nb) value = char + value
  return value
}

/**
 * Idem JSON.parse mais renvoie undefined en cas de plantage
 * @param jsonString La string à parser
 * @return {Object}
 */
function parse (jsonString) {
  var obj
  if (typeof jsonString === 'string') {
    try {
      obj = JSON.parse(jsonString)
    } catch (e) {
      if (!isUndefined(console) && console.error) console.error(e)
    }
  }
  return obj
}

/**
 * split une chaine et fait un trim sur chaque élément
 * @param str
 * @param {string|RegExp} [delim=/[,;\s]+/] Délimiteur, par défaut  virgule, point virgule ou n'importe quel espace (incluant tab, \n & co)
 * @returns {*}
 */
function splitAndTrim (str, delim) {
  if (typeof str === 'string') {
    if (!delim) delim = /[,;\s]+/
    // split + trim + filter au cas où il y aurait un délimiteur au début ou à la fin
    // serait plus lisible en es6 mais on peut être utilisé par qqun sans babel…
    return str.split(delim).map(function (elt) { return elt.trim() }).filter(function (elt) { return elt })
  }
  return []
}

/**
 * Idem JSON.stringify mais sans planter, en cas de ref circulaire sur une propriété on renvoie quand même les autres
 * (avec le message d'erreur de JSON.stringify sur la propriété à pb)
 * @param obj
 * @param {number} [indent]  Le nb d'espaces d'indentation
 * @returns {string}
 */
function stringify (obj, indent) {
  var buffer
  try {
    // ça peut planter en cas de ref circulaire
    buffer = indent ? JSON.stringify(obj, null, indent) : JSON.stringify(obj)
  } catch (error) {
    // on a un objet avec ref circulaire
    var key
    var value
    var pile = []
    for (key in obj) {
      // on veut le même comportement que JSON.stringify qui omet les valeurs undefined et les fct
      // et met du {} pour regexp et function
      if (hasProp(obj, key) && !isUndefined(obj[key]) && !isFunction(obj[key])) {
        value = obj[key]
        buffer = '"' + key + '":'
        try {
          buffer += indent ? JSON.stringify(value, null, indent) : JSON.stringify(value)
        } catch (error) {
          buffer += '"' + error.toString() + '"'
        }
        pile.push(buffer)
      }
    }
    var sep = ','
    buffer = '{'
    if (indent) {
      var sepSup = '\n' + ' '.repeat(indent)
      sep += sepSup
      buffer += sepSup
    }
    buffer += pile.join(sep)
    if (indent) buffer += '\n'
    buffer += '}'
  }
  return buffer
}

/**
 * Retire les accents d'une chaîne
 * @param {string} string
 * @return {string} la même désaccentuée
 */
function toAscii (string) {
  function reducer (acc, r) {
    return acc.replace(r[0], r[1])
  }
  if (typeof string === 'string') {
    // si c'est déjà sans accent on retourne
    if (/^[\w]*$/.test(string)) return string
    // faut inspecter
    var toReplace = [
      [/[áàâäãå]/g, 'a'],
      [/[ÁÀÂÄÃÅ]/g, 'A'],
      ['ç', 'c'],
      ['Ç', 'C'],
      [/[éèêë]/g, 'e'],
      [/[ÉÈÊË]/g, 'E'],
      [/[íìîï]/g, 'i'],
      [/[ÍÌÎÏ]/g, 'I'],
      ['ñ', 'n'],
      ['Ñ', 'N'],
      [/[óòôöõ]/g, 'o'],
      [/[ÓÒÔÖÕ]/g, 'O'],
      [/[úùûü]/g, 'u'],
      [/[ÚÙÛÜ]/g, 'U'],
      [/[ýÿ]/g, 'y'],
      [/[ÝŸ]/g, 'Y'],
      ['æ', 'ae'],
      ['Æ', 'AE'],
      ['œ', 'oe'],
      ['Œ', 'OE']
    ]
    return toReplace.reduce(reducer, string)
  }
  console.error(new TypeError('not a string'), string)
  return ''
}

/**
 * Quasi alias de Math.round, cast en entier mais retourne NaN pour tout ce qui
 * ne ressemble pas à un nombre (chaine vide, boolean, null) là ou round retourne 0 (ou 1 pour true)
 * @param arg
 * @returns {number}
 */
function toInt (arg) {
  switch (typeof arg) {
    case 'number':
      return Math.round(arg)
    case 'string':
      if (arg === '') return NaN
      else return Math.round(arg)
    default:
      return NaN
  }
}

/**
 * Notre module pour toutes nos fonctions génériques
 * (cf aussi les extensions sesajstools/http/xhr, sesajstools/dom, etc.)
 * @module sesajstools
 */
module.exports = {
  countSlash: countSlash,
  escapeForHtml: escapeForHtml,
  formatDate: formatDate,
  getToken: getToken,
  hasProp: hasProp,
  isArray: isArray,
  isArrayEmpty: isArrayEmpty,
  isArrayNotEmpty: isArrayNotEmpty,
  isDate: isDate,
  isError: isError,
  isFunction: isFunction,
  isInArray: isInArray,
  isInt: isInt,
  isIntPos: isIntPos,
  isObject: isObject,
  isObjectPlain: isObjectPlain,
  isRegExp: isRegExp,
  isString: isString,
  isUndefined: isUndefined,
  isUrlAbsolute: isUrlAbsolute,
  pad: pad,
  parse: parse,
  splitAndTrim: splitAndTrim,
  stringify: stringify,
  toAscii: toAscii,
  toInt: toInt
}