2012-04-30 11:23:03 +00:00
|
|
|
/*\
|
2012-07-14 14:57:36 +00:00
|
|
|
title: $:/core/modules/utils/utils.js
|
2012-04-30 11:23:03 +00:00
|
|
|
type: application/javascript
|
|
|
|
module-type: utils
|
|
|
|
|
|
|
|
Various static utility functions.
|
|
|
|
|
|
|
|
\*/
|
|
|
|
(function(){
|
|
|
|
|
2012-05-04 17:49:04 +00:00
|
|
|
/*jslint node: true, browser: true */
|
|
|
|
/*global $tw: false */
|
2012-04-30 11:23:03 +00:00
|
|
|
"use strict";
|
|
|
|
|
2019-07-03 16:39:32 +00:00
|
|
|
var base64utf8 = require("$:/core/modules/utils/base64-utf8/base64-utf8.module.js");
|
|
|
|
|
2017-09-04 13:55:12 +00:00
|
|
|
/*
|
|
|
|
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) {
|
2017-09-22 14:18:11 +00:00
|
|
|
if(!$tw.browser && $tw.node && process.stdout.isTTY) {
|
2017-09-04 13:55:12 +00:00
|
|
|
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"
|
|
|
|
};
|
|
|
|
|
2014-10-21 18:30:27 +00:00
|
|
|
/*
|
|
|
|
Display a warning, in colour if we're on a terminal
|
|
|
|
*/
|
|
|
|
exports.warning = function(text) {
|
2017-09-04 13:55:12 +00:00
|
|
|
exports.log(text,"brown/orange");
|
2016-05-01 17:17:47 +00:00
|
|
|
};
|
|
|
|
|
2018-04-02 18:40:47 +00:00
|
|
|
/*
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2016-08-04 14:54:33 +00:00
|
|
|
/*
|
|
|
|
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;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-05-01 17:17:47 +00:00
|
|
|
/*
|
|
|
|
Repeats a string
|
|
|
|
*/
|
|
|
|
exports.repeat = function(str,count) {
|
|
|
|
var result = "";
|
|
|
|
for(var t=0;t<count;t++) {
|
|
|
|
result += str;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
2014-10-21 18:30:27 +00:00
|
|
|
|
2012-12-26 19:35:12 +00:00
|
|
|
/*
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-06-19 11:11:02 +00:00
|
|
|
/*
|
2019-06-21 07:24:02 +00:00
|
|
|
Convert a string to sentence case (ie capitalise first letter)
|
2019-06-19 11:11:02 +00:00
|
|
|
*/
|
|
|
|
exports.toSentenceCase = function(str) {
|
2019-06-21 07:24:02 +00:00
|
|
|
return (str || "").replace(/^\S/, function(c) {return c.toUpperCase();});
|
2019-06-19 11:11:02 +00:00
|
|
|
}
|
|
|
|
|
2019-06-21 07:24:02 +00:00
|
|
|
/*
|
|
|
|
Convert a string to title case (ie capitalise each initial letter)
|
|
|
|
*/
|
|
|
|
exports.toTitleCase = function(str) {
|
|
|
|
return (str || "").replace(/(^|\s)\S/g, function(c) {return c.toUpperCase();});
|
|
|
|
}
|
|
|
|
|
2016-04-22 07:36:29 +00:00
|
|
|
/*
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2012-11-19 12:55:04 +00:00
|
|
|
/*
|
|
|
|
Return the number of keys in an object
|
|
|
|
*/
|
|
|
|
exports.count = function(object) {
|
2015-11-03 22:47:47 +00:00
|
|
|
return Object.keys(object || {}).length;
|
2012-11-19 12:55:04 +00:00
|
|
|
};
|
|
|
|
|
2018-11-06 13:34:51 +00:00
|
|
|
/*
|
|
|
|
Determine whether an array-item is an object-property
|
|
|
|
*/
|
|
|
|
exports.hopArray = function(object,array) {
|
|
|
|
for(var i=0; i<array.length; i++) {
|
|
|
|
if($tw.utils.hop(object,array[i])) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
2012-05-08 14:11:29 +00:00
|
|
|
/*
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2012-12-13 21:31:57 +00:00
|
|
|
/*
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2012-12-14 13:30:10 +00:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2012-05-03 20:47:16 +00:00
|
|
|
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;
|
2012-05-04 17:49:04 +00:00
|
|
|
for(t in extendedProperties) {
|
2012-05-05 10:21:15 +00:00
|
|
|
if(extendedProperties[t] !== undefined) {
|
|
|
|
result[t] = $tw.utils.deepCopy(extendedProperties[t]);
|
2012-05-03 20:47:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
2015-07-10 15:43:50 +00:00
|
|
|
exports.deepFreeze = function deepFreeze(object) {
|
|
|
|
var property, key;
|
2017-03-17 14:20:04 +00:00
|
|
|
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);
|
|
|
|
}
|
2015-07-10 15:43:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2012-11-08 18:34:04 +00:00
|
|
|
exports.slowInSlowOut = function(t) {
|
|
|
|
return (1 - ((Math.cos(t * Math.PI) + 1) / 2));
|
|
|
|
};
|
|
|
|
|
2014-10-08 13:07:48 +00:00
|
|
|
exports.formatDateString = function(date,template) {
|
2014-12-15 17:50:24 +00:00
|
|
|
var result = "",
|
2014-12-13 12:30:39 +00:00
|
|
|
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());
|
|
|
|
}],
|
2017-07-01 17:09:16 +00:00
|
|
|
[/^0XXX/, function() {
|
2019-11-18 13:24:47 +00:00
|
|
|
return $tw.utils.pad(date.getMilliseconds(),3);
|
2017-07-01 17:09:16 +00:00
|
|
|
}],
|
2014-12-13 12:30:39 +00:00
|
|
|
[/^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() {
|
2014-12-15 17:50:24 +00:00
|
|
|
return $tw.utils.pad($tw.utils.getYearForWeekNo(date) - 2000);
|
2014-12-13 12:30:39 +00:00
|
|
|
}],
|
|
|
|
[/^[ap]m/, function() {
|
|
|
|
return $tw.utils.getAmPm(date).toLowerCase();
|
|
|
|
}],
|
|
|
|
[/^hh/, function() {
|
|
|
|
return date.getHours();
|
|
|
|
}],
|
|
|
|
[/^mm/, function() {
|
|
|
|
return date.getMinutes();
|
|
|
|
}],
|
|
|
|
[/^ss/, function() {
|
|
|
|
return date.getSeconds();
|
|
|
|
}],
|
2017-07-01 17:09:16 +00:00
|
|
|
[/^XXX/, function() {
|
|
|
|
return date.getMilliseconds();
|
|
|
|
}],
|
2014-12-13 12:30:39 +00:00
|
|
|
[/^[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() {
|
2014-12-15 17:50:24 +00:00
|
|
|
return $tw.utils.pad(date.getFullYear() - 2000);
|
2014-12-13 12:30:39 +00:00
|
|
|
}]
|
|
|
|
];
|
2017-07-01 17:09:16 +00:00
|
|
|
// If the user wants everything in UTC, shift the datestamp
|
2018-04-02 18:40:47 +00:00
|
|
|
// Optimize for format string that essentially means
|
2017-07-01 17:09:16 +00:00
|
|
|
// 'return raw UTC (tiddlywiki style) date string.'
|
|
|
|
if(t.indexOf("[UTC]") == 0 ) {
|
2018-04-02 18:40:47 +00:00
|
|
|
if(t == "[UTC]YYYY0MM0DD0hh0mm0ssXXX")
|
2017-07-01 17:09:16 +00:00
|
|
|
return $tw.utils.stringifyDate(new Date());
|
|
|
|
var offset = date.getTimezoneOffset() ; // in minutes
|
|
|
|
date = new Date(date.getTime()+offset*60*1000) ;
|
|
|
|
t = t.substr(5) ;
|
2018-04-02 18:40:47 +00:00
|
|
|
}
|
2014-12-13 12:30:39 +00:00
|
|
|
while(t.length){
|
2014-12-15 17:50:24 +00:00
|
|
|
var matchString = "";
|
2014-12-13 12:30:39 +00:00
|
|
|
$tw.utils.each(matches, function(m) {
|
2014-12-16 08:35:26 +00:00
|
|
|
var match = m[0].exec(t);
|
2014-12-15 17:50:24 +00:00
|
|
|
if(match) {
|
|
|
|
matchString = m[1].call();
|
|
|
|
t = t.substr(match[0].length);
|
2014-12-13 12:30:39 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
});
|
2014-12-15 17:50:24 +00:00
|
|
|
if(matchString) {
|
|
|
|
result += matchString;
|
2014-12-13 12:30:39 +00:00
|
|
|
} else {
|
|
|
|
result += t.charAt(0);
|
|
|
|
t = t.substr(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
result = result.replace(/\\(.)/g,"$1");
|
|
|
|
return result;
|
2012-04-30 11:23:03 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
exports.getAmPm = function(date) {
|
2014-10-09 09:33:08 +00:00
|
|
|
return $tw.language.getString("Date/Period/" + (date.getHours() >= 12 ? "pm" : "am"));
|
2012-04-30 11:23:03 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
exports.getDaySuffix = function(date) {
|
2014-10-09 09:33:08 +00:00
|
|
|
return $tw.language.getString("Date/DaySuffix/" + date.getDate());
|
2012-04-30 11:23:03 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
exports.getWeek = function(date) {
|
|
|
|
var dt = new Date(date.getTime());
|
|
|
|
var d = dt.getDay();
|
2014-10-09 09:33:08 +00:00
|
|
|
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
|
2016-11-13 23:56:18 +00:00
|
|
|
var x = new Date(dt.getFullYear(),0,1);
|
|
|
|
var n = Math.floor((dt.getTime() - x.getTime()) / 86400000);
|
2014-10-09 09:33:08 +00:00
|
|
|
return Math.floor(n / 7) + 1;
|
2012-04-30 11:23:03 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
exports.getYearForWeekNo = function(date) {
|
|
|
|
var dt = new Date(date.getTime());
|
|
|
|
var d = dt.getDay();
|
2014-10-09 09:33:08 +00:00
|
|
|
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
|
2012-04-30 11:23:03 +00:00
|
|
|
return dt.getFullYear();
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.getHours12 = function(date) {
|
|
|
|
var h = date.getHours();
|
|
|
|
return h > 12 ? h-12 : ( h > 0 ? h : 12 );
|
|
|
|
};
|
|
|
|
|
2012-10-20 13:23:57 +00:00
|
|
|
/*
|
|
|
|
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) {
|
2014-02-05 17:44:01 +00:00
|
|
|
var futurep = false;
|
2014-02-10 17:34:10 +00:00
|
|
|
if(delta < 0) {
|
|
|
|
delta = -1 * delta;
|
2014-02-05 17:44:01 +00:00
|
|
|
futurep = true;
|
|
|
|
}
|
2012-10-20 13:23:57 +00:00
|
|
|
var units = [
|
2014-03-20 20:55:59 +00:00
|
|
|
{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}
|
2012-10-20 13:23:57 +00:00
|
|
|
];
|
|
|
|
for(var t=0; t<units.length; t++) {
|
|
|
|
var result = Math.floor(delta / units[t].duration);
|
|
|
|
if(result >= 2) {
|
|
|
|
return {
|
2012-10-28 18:41:48 +00:00
|
|
|
delta: delta,
|
2014-03-20 20:55:59 +00:00
|
|
|
description: $tw.language.getString(
|
|
|
|
"RelativeDate/" + (futurep ? "Future" : "Past") + "/" + units[t].name,
|
|
|
|
{variables:
|
|
|
|
{period: result.toString()}
|
|
|
|
}
|
|
|
|
),
|
2012-10-20 13:23:57 +00:00
|
|
|
updatePeriod: units[t].duration
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
2012-10-28 18:41:48 +00:00
|
|
|
delta: delta,
|
2014-03-20 20:55:59 +00:00
|
|
|
description: $tw.language.getString(
|
2014-03-31 11:41:54 +00:00
|
|
|
"RelativeDate/" + (futurep ? "Future" : "Past") + "/Second",
|
2014-03-20 20:55:59 +00:00
|
|
|
{variables:
|
|
|
|
{period: "1"}
|
|
|
|
}
|
|
|
|
),
|
2012-10-20 13:23:57 +00:00
|
|
|
updatePeriod: 1000
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2015-03-21 14:17:42 +00:00
|
|
|
// Convert & to "&", < to "<", > to ">", " to """
|
2012-12-26 19:35:12 +00:00
|
|
|
exports.htmlEncode = function(s) {
|
2012-04-30 11:23:03 +00:00
|
|
|
if(s) {
|
2015-03-21 14:17:42 +00:00
|
|
|
return s.toString().replace(/&/mg,"&").replace(/</mg,"<").replace(/>/mg,">").replace(/\"/mg,""");
|
2012-04-30 11:23:03 +00:00
|
|
|
} else {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Converts all HTML entities to their character equivalents
|
|
|
|
exports.entityDecode = function(s) {
|
2016-02-20 21:44:50 +00:00
|
|
|
var converter = String.fromCodePoint || String.fromCharCode,
|
2018-07-28 15:22:38 +00:00
|
|
|
e = s.substr(1,s.length-2), // Strip the & and the ;
|
|
|
|
c;
|
2012-04-30 11:23:03 +00:00
|
|
|
if(e.charAt(0) === "#") {
|
|
|
|
if(e.charAt(1) === "x" || e.charAt(1) === "X") {
|
2018-07-28 15:22:38 +00:00
|
|
|
c = parseInt(e.substr(2),16);
|
2012-04-30 11:23:03 +00:00
|
|
|
} else {
|
2018-07-28 15:22:38 +00:00
|
|
|
c = parseInt(e.substr(1),10);
|
|
|
|
}
|
|
|
|
if(isNaN(c)) {
|
|
|
|
return s;
|
|
|
|
} else {
|
|
|
|
return converter(c);
|
2012-04-30 11:23:03 +00:00
|
|
|
}
|
|
|
|
} else {
|
2018-07-28 15:22:38 +00:00
|
|
|
c = $tw.config.htmlEntities[e];
|
2012-04-30 11:23:03 +00:00
|
|
|
if(c) {
|
2016-02-18 13:49:09 +00:00
|
|
|
return converter(c);
|
2012-04-30 11:23:03 +00:00
|
|
|
} 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);
|
2014-01-03 10:50:00 +00:00
|
|
|
if(charCode <= 0xFF) {
|
2012-04-30 11:23:03 +00:00
|
|
|
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.
|
|
|
|
*
|
2015-02-08 18:46:22 +00:00
|
|
|
* For portability, we also escape all non-ASCII characters.
|
2012-04-30 11:23:03 +00:00
|
|
|
*/
|
2015-02-08 18:46:22 +00:00
|
|
|
return (s || "")
|
2012-04-30 11:23:03 +00:00
|
|
|
.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
|
2017-10-29 15:53:53 +00:00
|
|
|
.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
|
2012-04-30 11:23:03 +00:00
|
|
|
};
|
|
|
|
|
2012-10-17 18:10:48 +00:00
|
|
|
/*
|
|
|
|
Escape the RegExp special characters with a preceding backslash
|
|
|
|
*/
|
|
|
|
exports.escapeRegExp = function(s) {
|
2012-11-06 17:21:56 +00:00
|
|
|
return s.replace(/[\-\/\\\^\$\*\+\?\.\(\)\|\[\]\{\}]/g, '\\$&');
|
2012-10-17 18:10:48 +00:00
|
|
|
};
|
|
|
|
|
2015-01-06 01:39:24 +00:00
|
|
|
// Checks whether a link target is external, i.e. not a tiddler title
|
|
|
|
exports.isLinkExternal = function(to) {
|
2016-07-20 10:37:44 +00:00
|
|
|
var externalRegExp = /^(?:file|http|https|mailto|ftp|irc|news|data|skype):[^\s<>{}\[\]`|"\\^]+(?:\/|\b)/i;
|
2015-01-06 01:39:24 +00:00
|
|
|
return externalRegExp.test(to);
|
|
|
|
};
|
|
|
|
|
2012-04-30 11:23:03 +00:00
|
|
|
exports.nextTick = function(fn) {
|
|
|
|
/*global window: false */
|
2014-12-23 08:20:22 +00:00
|
|
|
if(typeof process === "undefined") {
|
2012-04-30 11:23:03 +00:00
|
|
|
// Apparently it would be faster to use postMessage - http://dbaron.org/log/20100309-faster-timeouts
|
|
|
|
window.setTimeout(fn,4);
|
|
|
|
} else {
|
|
|
|
process.nextTick(fn);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2012-06-11 09:41:13 +00:00
|
|
|
/*
|
|
|
|
Convert a hyphenated CSS property name into a camel case one
|
|
|
|
*/
|
|
|
|
exports.unHyphenateCss = function(propName) {
|
2012-10-25 13:57:48 +00:00
|
|
|
return propName.replace(/-([a-z])/gi, function(match0,match1) {
|
|
|
|
return match1.toUpperCase();
|
|
|
|
});
|
2012-06-11 09:41:13 +00:00
|
|
|
};
|
|
|
|
|
2012-10-25 21:20:27 +00:00
|
|
|
/*
|
|
|
|
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();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2012-12-13 21:31:57 +00:00
|
|
|
/*
|
2013-01-15 17:50:47 +00:00
|
|
|
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
|
2012-12-13 21:31:57 +00:00
|
|
|
*/
|
|
|
|
exports.parseTextReference = function(textRef) {
|
2013-01-15 17:50:47 +00:00
|
|
|
// Separate out the title, field name and/or JSON indices
|
2014-11-21 18:16:22 +00:00
|
|
|
var reTextRef = /(?:(.*?)!!(.+))|(?:(.*?)##(.+))|(.*)/mg,
|
2014-11-21 17:07:03 +00:00
|
|
|
match = reTextRef.exec(textRef),
|
|
|
|
result = {};
|
2013-01-15 17:50:47 +00:00
|
|
|
if(match && reTextRef.lastIndex === textRef.length) {
|
|
|
|
// Return the parts
|
2014-11-21 17:07:03 +00:00
|
|
|
if(match[1]) {
|
|
|
|
result.title = match[1];
|
|
|
|
}
|
|
|
|
if(match[2]) {
|
|
|
|
result.field = match[2];
|
|
|
|
}
|
|
|
|
if(match[3]) {
|
2014-11-21 18:16:22 +00:00
|
|
|
result.title = match[3];
|
|
|
|
}
|
|
|
|
if(match[4]) {
|
|
|
|
result.index = match[4];
|
|
|
|
}
|
|
|
|
if(match[5]) {
|
|
|
|
result.title = match[5];
|
2014-11-21 17:07:03 +00:00
|
|
|
}
|
2012-12-13 21:31:57 +00:00
|
|
|
} else {
|
2014-11-21 17:07:03 +00:00
|
|
|
// If we couldn't parse it
|
|
|
|
result.title = textRef
|
2012-12-13 21:31:57 +00:00
|
|
|
}
|
2014-11-21 17:07:03 +00:00
|
|
|
return result;
|
2012-12-13 21:31:57 +00:00
|
|
|
};
|
|
|
|
|
2014-04-28 14:54:32 +00:00
|
|
|
/*
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
2012-05-02 10:02:47 +00:00
|
|
|
/*
|
|
|
|
Extract the version number from the meta tag or from the boot file
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Browser version
|
|
|
|
exports.extractVersionInfo = function() {
|
2014-12-18 19:52:15 +00:00
|
|
|
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;
|
|
|
|
}
|
2012-05-02 10:02:47 +00:00
|
|
|
}
|
|
|
|
}
|
2012-07-13 16:38:23 +00:00
|
|
|
return null;
|
2012-05-02 10:02:47 +00:00
|
|
|
};
|
|
|
|
|
2013-08-28 14:15:56 +00:00
|
|
|
/*
|
|
|
|
Get the animation duration in ms
|
|
|
|
*/
|
|
|
|
exports.getAnimationDuration = function() {
|
2019-02-12 12:14:55 +00:00
|
|
|
return parseInt($tw.wiki.getTiddlerText("$:/config/AnimationDuration","400"),10) || 0;
|
2013-08-28 14:15:56 +00:00
|
|
|
};
|
|
|
|
|
2013-11-28 10:53:37 +00:00
|
|
|
/*
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
2013-11-28 14:03:08 +00:00
|
|
|
/*
|
|
|
|
Decode a base64 string
|
|
|
|
*/
|
|
|
|
exports.base64Decode = function(string64) {
|
2019-07-03 16:39:32 +00:00
|
|
|
return base64utf8.base64.decode.call(base64utf8,string64);
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Encode a string to base64
|
|
|
|
*/
|
|
|
|
exports.base64Encode = function(string64) {
|
|
|
|
return base64utf8.base64.encode.call(base64utf8,string64);
|
2013-11-28 14:03:08 +00:00
|
|
|
};
|
|
|
|
|
2014-02-23 12:33:00 +00:00
|
|
|
/*
|
|
|
|
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");
|
|
|
|
};
|
|
|
|
|
2014-03-31 11:41:54 +00:00
|
|
|
/*
|
|
|
|
High resolution microsecond timer for profiling
|
|
|
|
*/
|
|
|
|
exports.timer = function(base) {
|
|
|
|
var m;
|
|
|
|
if($tw.node) {
|
2018-04-02 18:40:47 +00:00
|
|
|
var r = process.hrtime();
|
2014-03-31 11:41:54 +00:00
|
|
|
m = r[0] * 1e3 + (r[1] / 1e6);
|
2014-03-31 17:42:30 +00:00
|
|
|
} else if(window.performance) {
|
2014-03-31 11:41:54 +00:00
|
|
|
m = performance.now();
|
2014-03-31 17:42:30 +00:00
|
|
|
} else {
|
2014-04-14 08:02:52 +00:00
|
|
|
m = Date.now();
|
2014-03-31 11:41:54 +00:00
|
|
|
}
|
|
|
|
if(typeof base !== "undefined") {
|
|
|
|
m = m - base;
|
|
|
|
}
|
|
|
|
return m;
|
2014-04-28 14:16:31 +00:00
|
|
|
};
|
2014-03-31 11:41:54 +00:00
|
|
|
|
2015-03-18 11:40:09 +00:00
|
|
|
/*
|
|
|
|
Convert text and content type to a data URI
|
|
|
|
*/
|
2020-03-30 09:55:37 +00:00
|
|
|
exports.makeDataUri = function(text,type,_canonical_uri) {
|
2015-03-18 11:40:09 +00:00
|
|
|
type = type || "text/vnd.tiddlywiki";
|
|
|
|
var typeInfo = $tw.config.contentTypeInfo[type] || $tw.config.contentTypeInfo["text/plain"],
|
|
|
|
isBase64 = typeInfo.encoding === "base64",
|
|
|
|
parts = [];
|
2020-03-30 09:55:37 +00:00
|
|
|
if(_canonical_uri) {
|
|
|
|
parts.push(_canonical_uri);
|
|
|
|
} else {
|
|
|
|
parts.push("data:");
|
|
|
|
parts.push(type);
|
|
|
|
parts.push(isBase64 ? ";base64" : "");
|
|
|
|
parts.push(",");
|
|
|
|
parts.push(isBase64 ? text : encodeURIComponent(text));
|
|
|
|
}
|
2015-03-18 11:40:09 +00:00
|
|
|
return parts.join("");
|
|
|
|
};
|
|
|
|
|
2015-09-28 19:27:43 +00:00
|
|
|
/*
|
|
|
|
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;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-12-23 12:19:47 +00:00
|
|
|
/*
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2016-07-11 10:18:19 +00:00
|
|
|
/*
|
|
|
|
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;
|
|
|
|
}
|
2016-10-08 13:06:30 +00:00
|
|
|
position -= ending.length;
|
2016-07-11 10:18:19 +00:00
|
|
|
var lastIndex = str.indexOf(ending, position);
|
|
|
|
return lastIndex !== -1 && lastIndex === position;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-03-30 14:24:05 +00:00
|
|
|
/*
|
|
|
|
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");
|
|
|
|
};
|
|
|
|
|
2020-04-13 09:03:01 +00:00
|
|
|
exports.parseNumber = function(str) {
|
|
|
|
return parseFloat(str) || 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.parseInt = function(str) {
|
2020-04-15 14:28:03 +00:00
|
|
|
return parseInt(str,10) || 0;
|
2020-04-13 09:03:01 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
exports.stringifyNumber = function(num) {
|
|
|
|
return num + "";
|
|
|
|
};
|
|
|
|
|
2020-04-25 10:26:19 +00:00
|
|
|
exports.makeCompareFunction = function(type,options) {
|
|
|
|
options = options || {};
|
|
|
|
var 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) {
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
return (types[type] || types[options.defaultType] || types.number);
|
|
|
|
};
|
|
|
|
|
2015-11-03 22:47:47 +00:00
|
|
|
})();
|