1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-17 23:34:50 +00:00
TiddlyWiki5/core/modules/utils/utils.js
Skeeve 6401b5c886 Now fixing bug mentioned in groups (#3188)
* fixed the "0 is not a number bug" in listops and x-listops

* Fixed one comment

* "default" is not a good name for a variable

* Following code styles.
Moving getInt to utils.

* Removing unwanted spaces introduced by me
2018-04-02 19:40:47 +01:00

804 lines
20 KiB
JavaScript

/*\
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 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");
};
/*
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<count;t++) {
result += str;
}
return result;
};
/*
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;
}
};
/*
Find the line break preceding a given position in a string
Returns position immediately after that line break, or the start of the string
*/
exports.findPrecedingLineBreak = function(text,pos) {
var result = text.lastIndexOf("\n",pos - 1);
if(result === -1) {
result = 0;
} else {
result++;
if(text.charAt(result) === "\r") {
result++;
}
}
return result;
};
/*
Find the line break following a given position in a string
*/
exports.findFollowingLineBreak = function(text,pos) {
// Cut to just past the following line break, or to the end of the text
var result = text.indexOf("\n",pos);
if(result === -1) {
result = text.length;
} else {
if(text.charAt(result) === "\r") {
result++;
}
}
return result;
};
/*
Return the number of keys in an object
*/
exports.count = function(object) {
return Object.keys(object || {}).length;
};
/*
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<value.length; t++) {
p = array.indexOf(value[t]);
if(p !== -1) {
array.splice(p,1);
}
}
} else {
for(t=array.length-1; 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<value.length; t++) {
p = array.indexOf(value[t]);
if(p !== -1) {
array.splice(p,1);
}
}
} else {
p = array.indexOf(value);
if(p !== -1) {
array.splice(p,1);
}
}
};
/*
Check whether any members of a hashmap are present in another hashmap
*/
exports.checkDependencies = function(dependencies,changes) {
var hit = false;
$tw.utils.each(changes,function(change,title) {
if($tw.utils.hop(dependencies,title)) {
hit = true;
}
});
return hit;
};
exports.extend = function(object /* [, src] */) {
$tw.utils.each(Array.prototype.slice.call(arguments, 1), function(source) {
if(source) {
for(var property in source) {
object[property] = source[property];
}
}
});
return object;
};
exports.deepCopy = function(object) {
var result,t;
if($tw.utils.isArray(object)) {
// Copy arrays
result = object.slice(0);
} else if(typeof object === "object") {
result = {};
for(t in object) {
if(object[t] !== undefined) {
result[t] = $tw.utils.deepCopy(object[t]);
}
}
} else {
result = object;
}
return result;
};
exports.extendDeepCopy = function(object,extendedProperties) {
var result = $tw.utils.deepCopy(object),t;
for(t in extendedProperties) {
if(extendedProperties[t] !== undefined) {
result[t] = $tw.utils.deepCopy(extendedProperties[t]);
}
}
return result;
};
exports.deepFreeze = function deepFreeze(object) {
var property, key;
if(object) {
Object.freeze(object);
for(key in object) {
property = object[key];
if($tw.utils.hop(object,key) && (typeof property === "object") && !Object.isFrozen(property)) {
deepFreeze(property);
}
}
}
};
exports.slowInSlowOut = function(t) {
return (1 - ((Math.cos(t * Math.PI) + 1) / 2));
};
exports.formatDateString = function(date,template) {
var result = "",
t = template,
matches = [
[/^0hh12/, function() {
return $tw.utils.pad($tw.utils.getHours12(date));
}],
[/^wYYYY/, function() {
return $tw.utils.getYearForWeekNo(date);
}],
[/^hh12/, function() {
return $tw.utils.getHours12(date);
}],
[/^DDth/, function() {
return date.getDate() + $tw.utils.getDaySuffix(date);
}],
[/^YYYY/, function() {
return date.getFullYear();
}],
[/^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());
}],
[/^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));
}],
[/^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(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();
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<units.length; t++) {
var result = Math.floor(delta / units[t].duration);
if(result >= 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 "&amp;", < to "&lt;", > to "&gt;", " to "&quot;"
exports.htmlEncode = function(s) {
if(s) {
return s.toString().replace(/&/mg,"&amp;").replace(/</mg,"&lt;").replace(/>/mg,"&gt;").replace(/\"/mg,"&quot;");
} 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 ;
if(e.charAt(0) === "#") {
if(e.charAt(1) === "x" || e.charAt(1) === "X") {
return converter(parseInt(e.substr(2),16));
} else {
return converter(parseInt(e.substr(1),10));
}
} else {
var 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) {
/*
* 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(/[\x00-\x1f\x80-\uFFFF]/g, 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) {
// See http://www.json.org/
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(/[\x00-\x1f\x80-\uFFFF]/g,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, '\\$&');
};
// 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<metatags.length; t++) {
var m = metatags[t];
if(m.name === "tiddlywiki-version") {
return m.content;
}
}
}
return null;
};
/*
Get the animation duration in ms
*/
exports.getAnimationDuration = function() {
return parseInt($tw.wiki.getTiddlerText("$:/config/AnimationDuration","400"),10);
};
/*
Hash a string to a number
Derived from http://stackoverflow.com/a/15710692
*/
exports.hashString = function(str) {
return str.split("").reduce(function(a,b) {
a = ((a << 5) - a) + b.charCodeAt(0);
return a & a;
},0);
};
/*
Decode a base64 string
*/
exports.base64Decode = function(string64) {
if($tw.browser) {
// TODO
throw "$tw.utils.base64Decode() doesn't work in the browser";
} else {
return (new Buffer(string64,"base64")).toString();
}
};
/*
Convert a hashmap into a tiddler dictionary format sequence of name:value pairs
*/
exports.makeTiddlerDictionary = function(data) {
var output = [];
for(var name in data) {
output.push(name + ": " + data[name]);
}
return output.join("\n");
};
/*
High resolution microsecond timer for profiling
*/
exports.timer = function(base) {
var m;
if($tw.node) {
var r = process.hrtime();
m = r[0] * 1e3 + (r[1] / 1e6);
} else if(window.performance) {
m = performance.now();
} else {
m = Date.now();
}
if(typeof base !== "undefined") {
m = m - base;
}
return m;
};
/*
Convert text and content type to a data URI
*/
exports.makeDataUri = function(text,type) {
type = type || "text/vnd.tiddlywiki";
var typeInfo = $tw.config.contentTypeInfo[type] || $tw.config.contentTypeInfo["text/plain"],
isBase64 = typeInfo.encoding === "base64",
parts = [];
parts.push("data:");
parts.push(type);
parts.push(isBase64 ? ";base64" : "");
parts.push(",");
parts.push(isBase64 ? text : encodeURIComponent(text));
return parts.join("");
};
/*
Useful for finding out the fully escaped CSS selector equivalent to a given tag. For example:
$tw.utils.tagToCssSelector("$:/tags/Stylesheet") --> 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;
}
};
})();