/**
* 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}
/**
* 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, '<')
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
}