/*\ title: $:/core/modules/utils/utils.js type: application/javascript module-type: utils Various static utility functions. \*/ (function(){ /*jslint node: true, browser: true */ /*global $tw: false */ "use strict"; /* Display a warning, in colour if we're on a terminal */ exports.warning = function(text) { console.log($tw.node ? "\x1b[1;33m" + text + "\x1b[0m" : text); } /* Trim whitespace from the start and end of a string Thanks to Steven Levithan, http://blog.stevenlevithan.com/archives/faster-trim-javascript */ exports.trim = function(str) { if(typeof str === "string") { return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); } else { return str; } }; /* Return the number of keys in an object */ exports.count = function(object) { var s = 0; $tw.utils.each(object,function() {s++;}); return s; }; /* Check if an array is equal by value and by reference. */ exports.isArrayEqual = function(array1,array2) { if(array1 === array2) { return true; } array1 = array1 || []; array2 = array2 || []; if(array1.length !== array2.length) { return false; } return array1.every(function(value,index) { return value === array2[index]; }); }; /* Push entries onto an array, removing them first if they already exist in the array array: array to modify (assumed to be free of duplicates) value: a single value to push or an array of values to push */ exports.pushTop = function(array,value) { var t,p; if($tw.utils.isArray(value)) { // Remove any array entries that are duplicated in the new values if(value.length !== 0) { if(array.length !== 0) { if(value.length < array.length) { for(t=0; t=0; t--) { p = value.indexOf(array[t]); if(p !== -1) { array.splice(t,1); } } } } // Push the values on top of the main array array.push.apply(array,value); } } else { p = array.indexOf(value); if(p !== -1) { array.splice(p,1); } array.push(value); } return array; }; /* Remove entries from an array array: array to modify value: a single value to remove, or an array of values to remove */ exports.removeArrayEntries = function(array,value) { var t,p; if($tw.utils.isArray(value)) { for(t=0; t= 12 ? "pm" : "am")); }; exports.getDaySuffix = function(date) { return $tw.language.getString("Date/DaySuffix/" + date.getDate()); }; exports.getWeek = function(date) { var dt = new Date(date.getTime()); var d = dt.getDay(); if(d === 0) { d = 7; // JavaScript Sun=0, ISO Sun=7 } dt.setTime(dt.getTime() + (4 - d) * 86400000);// shift day to Thurs of same week to calculate weekNo var n = Math.floor((dt.getTime()-new Date(dt.getFullYear(),0,1) + 3600000) / 86400000); return Math.floor(n / 7) + 1; }; exports.getYearForWeekNo = function(date) { var dt = new Date(date.getTime()); var d = dt.getDay(); if(d === 0) { d = 7; // JavaScript Sun=0, ISO Sun=7 } dt.setTime(dt.getTime() + (4 - d) * 86400000);// shift day to Thurs of same week return dt.getFullYear(); }; exports.getHours12 = function(date) { var h = date.getHours(); return h > 12 ? h-12 : ( h > 0 ? h : 12 ); }; /* Convert a date delta in milliseconds into a string representation of "23 seconds ago", "27 minutes ago" etc. delta: delta in milliseconds Returns an object with these members: description: string describing the delta period updatePeriod: time in millisecond until the string will be inaccurate */ exports.getRelativeDate = function(delta) { var futurep = false; if(delta < 0) { delta = -1 * delta; futurep = true; } var units = [ {name: "Years", duration: 365 * 24 * 60 * 60 * 1000}, {name: "Months", duration: (365/12) * 24 * 60 * 60 * 1000}, {name: "Days", duration: 24 * 60 * 60 * 1000}, {name: "Hours", duration: 60 * 60 * 1000}, {name: "Minutes", duration: 60 * 1000}, {name: "Seconds", duration: 1000} ]; for(var t=0; t= 2) { return { delta: delta, description: $tw.language.getString( "RelativeDate/" + (futurep ? "Future" : "Past") + "/" + units[t].name, {variables: {period: result.toString()} } ), updatePeriod: units[t].duration }; } } return { delta: delta, description: $tw.language.getString( "RelativeDate/" + (futurep ? "Future" : "Past") + "/Second", {variables: {period: "1"} } ), updatePeriod: 1000 }; }; // Convert & to "&", < to "<", > to ">", " to """ ' to "'" exports.htmlEncode = function(s) { if(s) { return s.toString().replace(/&/mg,"&").replace(//mg,">").replace(/\"/mg,""").replace(/\'/mg,"'"); } else { return ""; } }; // Converts all HTML entities to their character equivalents exports.entityDecode = function(s) { var e = s.substr(1,s.length-2); // Strip the & and the ; if(e.charAt(0) === "#") { if(e.charAt(1) === "x" || e.charAt(1) === "X") { return String.fromCharCode(parseInt(e.substr(2),16)); } else { return String.fromCharCode(parseInt(e.substr(1),10)); } } else { var c = $tw.config.htmlEntities[e]; if(c) { return String.fromCharCode(c); } else { return s; // Couldn't convert it as an entity, just return it raw } } }; exports.unescapeLineBreaks = function(s) { return s.replace(/\\n/mg,"\n").replace(/\\b/mg," ").replace(/\\s/mg,"\\").replace(/\r/mg,""); }; /* * Returns an escape sequence for given character. Uses \x for characters <= * 0xFF to save space, \u for the rest. * * The code needs to be in sync with th code template in the compilation * function for "action" nodes. */ // Copied from peg.js, thanks to David Majda exports.escape = function(ch) { var charCode = ch.charCodeAt(0); if(charCode <= 0xFF) { return '\\x' + $tw.utils.pad(charCode.toString(16).toUpperCase()); } else { return '\\u' + $tw.utils.pad(charCode.toString(16).toUpperCase(),4); } }; // Turns a string into a legal JavaScript string // Copied from peg.js, thanks to David Majda exports.stringify = function(s) { /* * ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a string * literal except for the closing quote character, backslash, carriage return, * line separator, paragraph separator, and line feed. Any character may * appear in the form of an escape sequence. * * For portability, we also escape all non-ASCII characters. */ return (s || "") .replace(/\\/g, '\\\\') // backslash .replace(/"/g, '\\"') // double quote character .replace(/'/g, "\\'") // single quote character .replace(/\r/g, '\\r') // carriage return .replace(/\n/g, '\\n') // line feed .replace(/[\x80-\uFFFF]/g, exports.escape); // non-ASCII characters }; /* Escape the RegExp special characters with a preceding backslash */ exports.escapeRegExp = function(s) { return s.replace(/[\-\/\\\^\$\*\+\?\.\(\)\|\[\]\{\}]/g, '\\$&'); }; // Checks whether a link target is external, i.e. not a tiddler title exports.isLinkExternal = function(to) { var externalRegExp = /(?:file|http|https|mailto|ftp|irc|news|data|skype):[^\s<>{}\[\]`|'"\\^~]+(?:\/|\b)/i; return externalRegExp.test(to); }; exports.nextTick = function(fn) { /*global window: false */ if(typeof process === "undefined") { // Apparently it would be faster to use postMessage - http://dbaron.org/log/20100309-faster-timeouts window.setTimeout(fn,4); } else { process.nextTick(fn); } }; /* Convert a hyphenated CSS property name into a camel case one */ exports.unHyphenateCss = function(propName) { return propName.replace(/-([a-z])/gi, function(match0,match1) { return match1.toUpperCase(); }); }; /* Convert a camelcase CSS property name into a dashed one ("backgroundColor" --> "background-color") */ exports.hyphenateCss = function(propName) { return propName.replace(/([A-Z])/g, function(match0,match1) { return "-" + match1.toLowerCase(); }); }; /* Parse a text reference of one of these forms: * title * !!field * title!!field * title##index * etc Returns an object with the following fields, all optional: * title: tiddler title * field: tiddler field name * index: JSON property index */ exports.parseTextReference = function(textRef) { // Separate out the title, field name and/or JSON indices var reTextRef = /(?:(.*?)!!(.+))|(?:(.*?)##(.+))|(.*)/mg, match = reTextRef.exec(textRef), result = {}; if(match && reTextRef.lastIndex === textRef.length) { // Return the parts if(match[1]) { result.title = match[1]; } if(match[2]) { result.field = match[2]; } if(match[3]) { result.title = match[3]; } if(match[4]) { result.index = match[4]; } if(match[5]) { result.title = match[5]; } } else { // If we couldn't parse it result.title = textRef } return result; }; /* Checks whether a string is a valid fieldname */ exports.isValidFieldName = function(name) { if(!name || typeof name !== "string") { return false; } name = name.toLowerCase().trim(); var fieldValidatorRegEx = /^[a-z0-9\-\._]+$/mg; return fieldValidatorRegEx.test(name); }; /* Extract the version number from the meta tag or from the boot file */ // Browser version exports.extractVersionInfo = function() { if($tw.packageInfo) { return $tw.packageInfo.version; } else { var metatags = document.getElementsByTagName("meta"); for(var t=0; t