/*\ 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"; var base64utf8 = require("$:/core/modules/utils/base64-utf8/base64-utf8.module.js"); /* Display a message, in colour if we're on a terminal */ exports.log = function(text,colour) { console.log($tw.node ? exports.terminalColour(colour) + text + exports.terminalColour() : text); }; exports.terminalColour = function(colour) { if(!$tw.browser && $tw.node && process.stdout.isTTY) { if(colour) { var code = exports.terminalColourLookup[colour]; if(code) { return "\x1b[" + code + "m"; } } else { return "\x1b[0m"; // Cancel colour } } return ""; }; exports.terminalColourLookup = { "black": "0;30", "red": "0;31", "green": "0;32", "brown/orange": "0;33", "blue": "0;34", "purple": "0;35", "cyan": "0;36", "light gray": "0;37" }; /* Display a warning, in colour if we're on a terminal */ exports.warning = function(text) { exports.log(text,"brown/orange"); }; /* Log a table of name: value pairs */ exports.logTable = function(data) { if(console.table) { console.table(data); } else { $tw.utils.each(data,function(value,name) { console.log(name + ": " + value); }); } } /* Return the integer represented by the str (string). Return the dflt (default) parameter if str is not a base-10 number. */ exports.getInt = function(str,deflt) { var i = parseInt(str,10); return isNaN(i) ? deflt : i; } /* Repeatedly replaces a substring within a string. Like String.prototype.replace, but without any of the default special handling of $ sequences in the replace string */ exports.replaceString = function(text,search,replace) { return text.replace(search,function() { return replace; }); }; /* Repeats a string */ exports.repeat = function(str,count) { var result = ""; for(var t=0;t 1) ? "..." : ""; return (os.isFunctionDefinition) ? lines[0].replace("\\", "\\\\") + suffix: ""; }], [/^\$varType\$/i, function() { return varType; }] ]; while(t.length){ var matchString = ""; $tw.utils.each(matches, function(m) { var match = m[0].exec(t); if(match) { matchString = m[1].call(null,match); t = t.substr(match[0].length); return false; } }); if(matchString) { result += matchString; } else { result += t.charAt(0); t = t.substr(1); } } result = result.replace(/\\(.)/g,"$1"); return result.trim(); }; exports.formatTitleString = function(template,options) { var base = options.base || "", separator = options.separator || "", counter = options.counter || ""; var result = "", t = template, matches = [ [/^\$basename\$/i, function() { return base; }], [/^\$count:(\d+)\$/i, function(match) { return $tw.utils.pad(counter,match[1]); }], [/^\$separator\$/i, function() { return separator; }], [/^\$count\$/i, function() { return counter + ""; }] ]; while(t.length){ var matchString = ""; $tw.utils.each(matches, function(m) { var match = m[0].exec(t); if(match) { matchString = m[1].call(null,match); t = t.substr(match[0].length); return false; } }); if(matchString) { result += matchString; } else { result += t.charAt(0); t = t.substr(1); } } result = result.replace(/\\(.)/g,"$1"); return result; }; exports.formatDateString = function(date,template) { var result = "", t = template, matches = [ [/^TIMESTAMP/, function() { return date.getTime(); }], [/^0hh12/, function() { return $tw.utils.pad($tw.utils.getHours12(date)); }], [/^wYYYY/, function() { return $tw.utils.pad($tw.utils.getYearForWeekNo(date),4); }], [/^hh12/, function() { return $tw.utils.getHours12(date); }], [/^DDth/, function() { return date.getDate() + $tw.utils.getDaySuffix(date); }], [/^YYYY/, function() { return $tw.utils.pad(date.getFullYear(),4); }], [/^aYYYY/, function() { return $tw.utils.pad(Math.abs(date.getFullYear()),4); }], [/^\{era:([^,\|}]*)\|([^}\|]*)\|([^}]*)\}/, function(match) { var year = date.getFullYear(); return year === 0 ? match[2] : (year < 0 ? match[1] : match[3]); }], [/^0hh/, function() { return $tw.utils.pad(date.getHours()); }], [/^0mm/, function() { return $tw.utils.pad(date.getMinutes()); }], [/^0ss/, function() { return $tw.utils.pad(date.getSeconds()); }], [/^0XXX/, function() { return $tw.utils.pad(date.getMilliseconds(),3); }], [/^0DD/, function() { return $tw.utils.pad(date.getDate()); }], [/^0MM/, function() { return $tw.utils.pad(date.getMonth()+1); }], [/^0WW/, function() { return $tw.utils.pad($tw.utils.getWeek(date)); }], [/^0ddddd/, function() { return $tw.utils.pad(Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24),3); }], [/^ddddd/, function() { return Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24); }], [/^dddd/, function() { return [7,1,2,3,4,5,6][date.getDay()]; }], [/^ddd/, function() { return $tw.language.getString("Date/Short/Day/" + date.getDay()); }], [/^mmm/, function() { return $tw.language.getString("Date/Short/Month/" + (date.getMonth() + 1)); }], [/^DDD/, function() { return $tw.language.getString("Date/Long/Day/" + date.getDay()); }], [/^MMM/, function() { return $tw.language.getString("Date/Long/Month/" + (date.getMonth() + 1)); }], [/^TZD/, function() { var tz = date.getTimezoneOffset(), atz = Math.abs(tz); return (tz < 0 ? '+' : '-') + $tw.utils.pad(Math.floor(atz / 60)) + ':' + $tw.utils.pad(atz % 60); }], [/^wYY/, function() { return $tw.utils.pad($tw.utils.getYearForWeekNo(date) - 2000); }], [/^[ap]m/, function() { return $tw.utils.getAmPm(date).toLowerCase(); }], [/^hh/, function() { return date.getHours(); }], [/^mm/, function() { return date.getMinutes(); }], [/^ss/, function() { return date.getSeconds(); }], [/^XXX/, function() { return date.getMilliseconds(); }], [/^[AP]M/, function() { return $tw.utils.getAmPm(date).toUpperCase(); }], [/^DD/, function() { return date.getDate(); }], [/^MM/, function() { return date.getMonth() + 1; }], [/^WW/, function() { return $tw.utils.getWeek(date); }], [/^YY/, function() { return $tw.utils.pad(date.getFullYear() - 2000); }] ]; // If the user wants everything in UTC, shift the datestamp // Optimize for format string that essentially means // 'return raw UTC (tiddlywiki style) date string.' if(t.indexOf("[UTC]") == 0 ) { if(t == "[UTC]YYYY0MM0DD0hh0mm0ssXXX") return $tw.utils.stringifyDate(date || new Date()); var offset = date.getTimezoneOffset() ; // in minutes date = new Date(date.getTime()+offset*60*1000) ; t = t.substr(5) ; } while(t.length){ var matchString = ""; $tw.utils.each(matches, function(m) { var match = m[0].exec(t); if(match) { matchString = m[1].call(null,match); t = t.substr(match[0].length); return false; } }); if(matchString) { result += matchString; } else { result += t.charAt(0); t = t.substr(1); } } result = result.replace(/\\(.)/g,"$1"); return result; }; exports.getAmPm = function(date) { return $tw.language.getString("Date/Period/" + (date.getHours() >= 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 x = new Date(dt.getFullYear(),0,1); var n = Math.floor((dt.getTime() - x.getTime()) / 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 """ exports.htmlEncode = function(s) { if(s) { return s.toString().replace(/&/mg,"&").replace(//mg,">").replace(/\"/mg,"""); } else { return ""; } }; // Converts like htmlEncode, but forgets the double quote for brevity exports.htmlTextEncode = function(s) { if(s) { return s.toString().replace(/&/mg,"&").replace(//mg,">"); } else { return ""; } }; // Converts all HTML entities to their character equivalents exports.entityDecode = function(s) { var converter = String.fromCodePoint || String.fromCharCode, e = s.substr(1,s.length-2), // Strip the & and the ; c; if(e.charAt(0) === "#") { if(e.charAt(1) === "x" || e.charAt(1) === "X") { c = parseInt(e.substr(2),16); } else { c = parseInt(e.substr(1),10); } if(isNaN(c)) { return s; } else { return converter(c); } } else { c = $tw.config.htmlEntities[e]; if(c) { return converter(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, rawUnicode) { /* * 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. */ var regex = rawUnicode ? /[\x00-\x1f]/g : /[\x00-\x1f\x80-\uFFFF]/g; 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(regex, exports.escape); // non-ASCII characters }; // Turns a string into a legal JSON string // Derived from peg.js, thanks to David Majda exports.jsonStringify = function(s, rawUnicode) { // See http://www.json.org/ var regex = rawUnicode ? /[\x00-\x1f]/g : /[\x00-\x1f\x80-\uFFFF]/g; return (s || "") .replace(/\\/g, '\\\\') // backslash .replace(/"/g, '\\"') // double quote character .replace(/\r/g, '\\r') // carriage return .replace(/\n/g, '\\n') // line feed .replace(/\x08/g, '\\b') // backspace .replace(/\x0c/g, '\\f') // formfeed .replace(/\t/g, '\\t') // tab .replace(regex,function(s) { return '\\u' + $tw.utils.pad(s.charCodeAt(0).toString(16).toUpperCase(),4); }); // non-ASCII characters }; /* Escape the RegExp special characters with a preceding backslash */ exports.escapeRegExp = function(s) { return s.replace(/[\-\/\\\^\$\*\+\?\.\(\)\|\[\]\{\}]/g, '\\$&'); }; /* Extended version of encodeURIComponent that encodes additional characters including those that are illegal within filepaths on various platforms including Windows */ exports.encodeURIComponentExtended = function(s) { return encodeURIComponent(s).replace(/[!'()*]/g,function(c) { return "%" + c.charCodeAt(0).toString(16).toUpperCase(); }); }; // 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|obsidian|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,0); } 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; } // Since v5.2.x, there are no restrictions on characters in field names return 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 tc-tagged-\%24\%3A\%2Ftags\%2FStylesheet */ exports.tagToCssSelector = function(tagName) { return "tc-tagged-" + encodeURIComponent(tagName).replace(/[!"#$%&'()*+,\-./:;<=>?@[\\\]^`{\|}~,]/mg,function(c) { return "\\" + c; }); }; /* IE does not have sign function */ exports.sign = Math.sign || function(x) { x = +x; // convert to a number if(x === 0 || isNaN(x)) { return x; } return x > 0 ? 1 : -1; }; /* IE does not have an endsWith function */ exports.strEndsWith = function(str,ending,position) { if(str.endsWith) { return str.endsWith(ending,position); } else { if(typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > str.length) { position = str.length; } position -= ending.length; var lastIndex = str.indexOf(ending, position); return lastIndex !== -1 && lastIndex === position; } }; /* Return system information useful for debugging */ exports.getSystemInfo = function(str,ending,position) { var results = [], save = function(desc,value) { results.push(desc + ": " + value); }; if($tw.browser) { save("User Agent",navigator.userAgent); save("Online Status",window.navigator.onLine); } if($tw.node) { save("Node Version",process.version); } return results.join("\n"); }; exports.parseNumber = function(str) { return parseFloat(str) || 0; }; exports.parseInt = function(str) { return parseInt(str,10) || 0; }; exports.stringifyNumber = function(num) { return num + ""; }; exports.makeCompareFunction = function(type,options) { options = options || {}; // set isCaseSensitive to true if not defined in options var isCaseSensitive = (options.isCaseSensitive === false) ? false : true, gt = options.invert ? -1 : +1, lt = options.invert ? +1 : -1, compare = function(a,b) { if(a > b) { return gt ; } else if(a < b) { return lt; } else { return 0; } }, types = { "number": function(a,b) { return compare($tw.utils.parseNumber(a),$tw.utils.parseNumber(b)); }, "integer": function(a,b) { return compare($tw.utils.parseInt(a),$tw.utils.parseInt(b)); }, "string": function(a,b) { if(!isCaseSensitive) { a = a.toLowerCase(); b = b.toLowerCase(); } return compare("" + a,"" + b); }, "date": function(a,b) { var dateA = $tw.utils.parseDate(a), dateB = $tw.utils.parseDate(b); if(!isFinite(dateA)) { dateA = new Date(0); } if(!isFinite(dateB)) { dateB = new Date(0); } return compare(dateA,dateB); }, "version": function(a,b) { return $tw.utils.compareVersions(a,b); }, "alphanumeric": function(a,b) { if(!isCaseSensitive) { a = a.toLowerCase(); b = b.toLowerCase(); } return options.invert ? b.localeCompare(a,undefined,{numeric: true,sensitivity: "base"}) : a.localeCompare(b,undefined,{numeric: true,sensitivity: "base"}); } }; return (types[type] || types[options.defaultType] || types.number); }; })();