TiddlyWiki5/core/modules/utils/utils.js

1041 lines
26 KiB
JavaScript
Raw Normal View History

/*\
2012-07-14 14:57:36 +00:00
title: $:/core/modules/utils/utils.js
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 */
"use strict";
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) {
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"
};
/*
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");
};
/*
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<count;t++) {
result += str;
}
return result;
};
/*
Check if a string starts with another string
*/
exports.startsWith = function(str,search) {
return str.substring(0, search.length) === search;
};
/*
Check if a string ends with another string
*/
exports.endsWith = function(str,search) {
return str.substring(str.length - search.length) === search;
};
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;
}
};
exports.trimPrefix = function(str,unwanted) {
if(typeof str === "string" && typeof unwanted === "string") {
if(unwanted === "") {
return str.replace(/^\s\s*/, '');
} else {
// Safely regexp-escape the unwanted text
unwanted = unwanted.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
var regex = new RegExp('^(' + unwanted + ')+');
return str.replace(regex, '');
}
} else {
return str;
}
};
exports.trimSuffix = function(str,unwanted) {
if(typeof str === "string" && typeof unwanted === "string") {
if(unwanted === "") {
return str.replace(/\s\s*$/, '');
} else {
// Safely regexp-escape the unwanted text
unwanted = unwanted.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
var regex = new RegExp('(' + unwanted + ')+$');
return str.replace(regex, '');
}
} else {
return str;
}
};
2019-06-19 11:11: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) {
return (str || "").replace(/^\S/, function(c) {return c.toUpperCase();});
2019-06-19 11:11: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();});
}
/*
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;
};
/*
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);
}
}
return array;
2012-05-08 14:11:29 +00:00
};
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;
};
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]);
}
}
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.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;
};
2014-10-08 13:07:48 +00:00
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) {
2014-12-16 08:35:26 +00:00
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;
2014-02-10 17:34:10 +00:00
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;"
2012-12-26 19:35:12 +00:00
exports.htmlEncode = function(s) {
if(s) {
return s.toString().replace(/&/mg,"&amp;").replace(/</mg,"&lt;").replace(/>/mg,"&gt;").replace(/\"/mg,"&quot;");
} else {
return "";
}
};
// Converts like htmlEncode, but forgets the double quote for brevity
exports.htmlTextEncode = function(s) {
if(s) {
return s.toString().replace(/&/mg,"&amp;").replace(/</mg,"&lt;").replace(/>/mg,"&gt;");
} 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
2017-10-29 15:53:53 +00:00
};
// Turns a string into a legal JSON string
// Derived from peg.js, thanks to David Majda
exports.jsonStringify = function(s, rawUnicode) {
2017-10-29 15:53:53 +00:00
// See http://www.json.org/
var regex = rawUnicode ? /[\x00-\x1f]/g : /[\x00-\x1f\x80-\uFFFF]/g;
2017-10-29 15:53:53 +00:00
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) {
2017-10-29 15:53:53 +00:00
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) {
2012-11-06 17:21:56 +00:00
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) {
2022-12-24 15:56:46 +00:00
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) {
2012-10-25 13:57:48 +00:00
return propName.replace(/-([a-z])/gi, function(match0,match1) {
return match1.toUpperCase();
});
};
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
/*
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) {
// 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];
}
2012-12-13 21:31:57 +00:00
} else {
// If we couldn't parse it
result.title = textRef
2012-12-13 21:31:57 +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;
}
Add support for JSON-formatted tiddler store, and make it the default (#5708) * Add support for JSON-formatted tiddler store, and make it the default The change to `getTiddlersAsJson()` is to allow experimentation * Move JSON tiddlers into their own store area, and fix support for encrypted tiddlers Also add a dummy old-style store area for backwards compatibility The current arrangement is that JSON tiddlers will always override old-style tiddlers. * Use the deserialiser mechanism to decode the content * Refactor $:/core/modules/deserializers.js before we start extending it Cleaning up the helper function names and ordering * Drop support for the "systemArea" div It was only used in really old v5.0.x * Update deserializer to support JSON store format and add some tests * Life UI restrictions on characters in fieldnames * Add another test case * Correct mis-merge * Remove toLowerCase() methods applied to fieldnames * Insert line breaks in output of getTiddlersAsJson (#5786) Rather than have the entire store on one line, insert a line break after each tiddler. * Refactor #5786 for backwards compatibility * Only read .tiddlywiki-tiddler-store blocks from script tags Prompted by @simonbaird's comment here: https://github.com/Jermolene/TiddlyWiki5/pull/5708#discussion_r648833367 * Clean up escaping of unsafe script characters It seems that escaping `<` is sufficient * Add docs from @saqimtiaz Thanks @saqimtiaz * Docs tweaks * Remove excess whitespace Thanks @simonbaird * Fix templates for lazy loading * Remove obsolete item from release note * Clean up whitespace * Docs for the jsontiddler widget * Fix whitespace Fixes #5840 * Comments * Fix newlines in JSON store area * Remove obsolete docs change Co-authored-by: Simon Baird <simon.baird@gmail.com>
2021-07-14 08:15:30 +00:00
// Since v5.2.x, there are no restrictions on characters in field names
return name;
2014-04-28 14:54:32 +00:00
};
/*
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) || 0;
};
/*
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);
};
/*
Cryptographic hash function as used by sha256 filter operator
options.length .. number of characters returned defaults to 64
*/
exports.sha256 = function(str, options) {
options = options || {}
return sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(str)).substr(0,options.length || 64);
}
/*
Base64 utility functions that work in either browser or Node.js
*/
if(typeof window !== 'undefined') {
exports.btoa = function(binstr) { return window.btoa(binstr); }
exports.atob = function(b64) { return window.atob(b64); }
} else {
exports.btoa = function(binstr) {
return Buffer.from(binstr, 'binary').toString('base64');
}
exports.atob = function(b64) {
return Buffer.from(b64, 'base64').toString('binary');
}
}
/*
Decode a base64 string
*/
exports.base64Decode = function(string64,binary,urlsafe) {
var encoded = urlsafe ? string64.replace(/_/g,'/').replace(/-/g,'+') : string64;
if(binary) return exports.atob(encoded)
else return base64utf8.base64.decode.call(base64utf8,encoded);
};
/*
Encode a string to base64
*/
exports.base64Encode = function(string64,binary,urlsafe) {
var encoded;
if(binary) encoded = exports.btoa(string64);
else encoded = base64utf8.base64.encode.call(base64utf8,string64);
if(urlsafe) {
encoded = encoded.replace(/\+/g,'-').replace(/\//g,'_');
}
return encoded;
};
/*
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,_canonical_uri) {
type = type || "text/vnd.tiddlywiki";
var typeInfo = $tw.config.contentTypeInfo[type] || $tw.config.contentTypeInfo["text/plain"],
isBase64 = typeInfo.encoding === "base64",
parts = [];
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));
}
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;
});
};
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)) {
2015-12-23 12:19:47 +00:00
return x;
}
return x > 0 ? 1 : -1;
};
Handle binary files better when saving on Node.JS (#2420) * Save binary tiddlers with meta file The filesystemadaptor plugin was a little simplistic in its understanding of a binary file. It was using the typeInfo dictionary to choose what tiddler types were binary (and hence needed a meta file when saving). I looked as if it was trying to be smart by looking for the hasMetaFile *OR* had the encoding of base64. Unfortunately the typeInfo only defined image/jpeg and so any other base64 encoded tiddler was assumed to be of type text/vnd.tiddlywiki. The net effect was only JPG images got a meta file and everything else were saved as .tid files with base64 encoding. It all still worked but made working with binary data in a Git repo a bit daunting. There is enough information in the $tw.config.contentTypeInfo to determine if a tiddler type is encoded with base64 or not. A better list is available from boot/boot.js who registers all the types thorough the registerFileType and marks then with base64 were appropriate. This commit uses the typeInfo dictionary first for any filesystem specific overrides, then the contentTypeInfo, and finally defaults to the typeInfo["text/vnd.tiddlywiki"]. It also eliminates the now unnecessary override for image/jpeg. I think this might have been the original intent from commit 10b192e7. From my limited testing all files described in boot/boot.js (lines 1832-1856) with an encoding of base64 now save as the original binary and a meta file. Meaning that when you start the node server and then drag-n-drop a binary file (i.e. image/png) it will PUT to the server and then save it on the filesystem as-is allowing the file to be managed as a binary file and not a text file. (Binary diffs are better and GitHub supports them as well). * Prevent duplicate file extensions A side effects of using the $tw.config.contentFileInfo in the previous commit is that it will always append a file extension to the tiddler title when saving. In most cases this is the correct course of action. However, sometimes that title is already a proper filename with an extension (for example importing 'foobar.png' would save a file named 'foobar.png.png') which seemed silly. This commit simply checks to make sure the title does not already end with the file extension before appending it to the filename. A little convenience really. Since IE apparently doesn't have the String endsWith method I took the liberty to add a helper method to $tw.utils trying to follow the other polyfill patterns. I figured this was more generic and readable then attempting to use a one-off solution inline. I got the polyfill code from MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith#Polyfill Is strEndsWith the best method name?
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) {
Handle binary files better when saving on Node.JS (#2420) * Save binary tiddlers with meta file The filesystemadaptor plugin was a little simplistic in its understanding of a binary file. It was using the typeInfo dictionary to choose what tiddler types were binary (and hence needed a meta file when saving). I looked as if it was trying to be smart by looking for the hasMetaFile *OR* had the encoding of base64. Unfortunately the typeInfo only defined image/jpeg and so any other base64 encoded tiddler was assumed to be of type text/vnd.tiddlywiki. The net effect was only JPG images got a meta file and everything else were saved as .tid files with base64 encoding. It all still worked but made working with binary data in a Git repo a bit daunting. There is enough information in the $tw.config.contentTypeInfo to determine if a tiddler type is encoded with base64 or not. A better list is available from boot/boot.js who registers all the types thorough the registerFileType and marks then with base64 were appropriate. This commit uses the typeInfo dictionary first for any filesystem specific overrides, then the contentTypeInfo, and finally defaults to the typeInfo["text/vnd.tiddlywiki"]. It also eliminates the now unnecessary override for image/jpeg. I think this might have been the original intent from commit 10b192e7. From my limited testing all files described in boot/boot.js (lines 1832-1856) with an encoding of base64 now save as the original binary and a meta file. Meaning that when you start the node server and then drag-n-drop a binary file (i.e. image/png) it will PUT to the server and then save it on the filesystem as-is allowing the file to be managed as a binary file and not a text file. (Binary diffs are better and GitHub supports them as well). * Prevent duplicate file extensions A side effects of using the $tw.config.contentFileInfo in the previous commit is that it will always append a file extension to the tiddler title when saving. In most cases this is the correct course of action. However, sometimes that title is already a proper filename with an extension (for example importing 'foobar.png' would save a file named 'foobar.png.png') which seemed silly. This commit simply checks to make sure the title does not already end with the file extension before appending it to the filename. A little convenience really. Since IE apparently doesn't have the String endsWith method I took the liberty to add a helper method to $tw.utils trying to follow the other polyfill patterns. I figured this was more generic and readable then attempting to use a one-off solution inline. I got the polyfill code from MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith#Polyfill Is strEndsWith the best method name?
2016-07-11 10:18:19 +00:00
position = str.length;
}
position -= ending.length;
Handle binary files better when saving on Node.JS (#2420) * Save binary tiddlers with meta file The filesystemadaptor plugin was a little simplistic in its understanding of a binary file. It was using the typeInfo dictionary to choose what tiddler types were binary (and hence needed a meta file when saving). I looked as if it was trying to be smart by looking for the hasMetaFile *OR* had the encoding of base64. Unfortunately the typeInfo only defined image/jpeg and so any other base64 encoded tiddler was assumed to be of type text/vnd.tiddlywiki. The net effect was only JPG images got a meta file and everything else were saved as .tid files with base64 encoding. It all still worked but made working with binary data in a Git repo a bit daunting. There is enough information in the $tw.config.contentTypeInfo to determine if a tiddler type is encoded with base64 or not. A better list is available from boot/boot.js who registers all the types thorough the registerFileType and marks then with base64 were appropriate. This commit uses the typeInfo dictionary first for any filesystem specific overrides, then the contentTypeInfo, and finally defaults to the typeInfo["text/vnd.tiddlywiki"]. It also eliminates the now unnecessary override for image/jpeg. I think this might have been the original intent from commit 10b192e7. From my limited testing all files described in boot/boot.js (lines 1832-1856) with an encoding of base64 now save as the original binary and a meta file. Meaning that when you start the node server and then drag-n-drop a binary file (i.e. image/png) it will PUT to the server and then save it on the filesystem as-is allowing the file to be managed as a binary file and not a text file. (Binary diffs are better and GitHub supports them as well). * Prevent duplicate file extensions A side effects of using the $tw.config.contentFileInfo in the previous commit is that it will always append a file extension to the tiddler title when saving. In most cases this is the correct course of action. However, sometimes that title is already a proper filename with an extension (for example importing 'foobar.png' would save a file named 'foobar.png.png') which seemed silly. This commit simply checks to make sure the title does not already end with the file extension before appending it to the filename. A little convenience really. Since IE apparently doesn't have the String endsWith method I took the liberty to add a helper method to $tw.utils trying to follow the other polyfill patterns. I figured this was more generic and readable then attempting to use a one-off solution inline. I got the polyfill code from MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith#Polyfill Is strEndsWith the best method name?
2016-07-11 10:18:19 +00:00
var lastIndex = str.indexOf(ending, position);
return lastIndex !== -1 && lastIndex === position;
}
};
Fix syncer to handler errors properly (#4373) * First commit * Add throttling of saves Now we refuse to save a tiddler more often than once per second. * Wait for a timeout before trying again after an error * Modest optimisations of isDirty() method * Synchronise system tiddlers and deletions from the server Fixes two long-standing issues: * Changes to system tiddlers are not synchronised from the server to the browser * Deletions of tiddlers on the server are not propagated to browser clients * Make sure we update the dirty status even if there isn't a task to perform * Replace save-wiki button with popup sync menu * Remove the "Server" control panel tab We don't need it with the enhanced sync dropdown * Add indentation to the save-wiki button * Fix spacing in dropdown menu items * Switch between cloud icons according to dirty status * Add a menu item to copy syncer logs to the clipboard * Improve animated icon * Remove indentation from save-wiki button @pmario the annoying thing is that using `\trim whitespace` trims significant whitespace too, so it means we have to use <$text text=" "/> when we need a space that won't be trimmed. For the moment, I've removed the indentation but will keep thinking about it. * Further icon, UI and copy text tweaks Move the icons and styles from the core into the TiddlyWeb plugin * Clean up PR diff * Tweak animation durations * Break the actions from the syncer dropdown into separate tiddlers @pmario I think this makes things a bit easier to follow * Refactor syncadaptor creation and logging The goal is for the syncadaptor to be able to log to the same logger as the syncer, so that the "copy syncer logs to clipboard" data is more useful. * Don't transition the dirty indicator container colour, just the SVG's colour * Only trigger a sync for changes to tiddlers we're interested in Otherwise it is triggered by the creation of the alert tiddlers used to display errors. * Restore deleting local tiddlers removed from the server (I had commented it out for some testing and accidentally commited it). * Guard against missing adaptor info * We still need to trigger a timeout when there was no task to process * Avoid repeatedly polling for changes Instead we only trigger a timeout call at if there is a pending task (ie a tiddler that has changed but isn't yet old enough to save). * Lazy loading: include skinny versions of lazily loaded tiddlers in the index.html * Introduce _is_skinny field for indicating that a tiddler is subject to lazy loading * Remove savetrail plugin from prerelease It doesn't yet work with the new syncer * Make the savetrail plugin work again * Clear outstanding alerts when synchronisation is restored * Logger: only remove alerts from the same component Missed off 9f5c0de07 * Make the saving throttle interval configurable (#4385) After switching Bob to use the core syncer the throttle interval makes saving feel very sluggish compared to the message queue setup that I had before. The editing lock that I use to prevent conflicts with multiple users doesn't go away until the save is completed, and with the 1 second delay it means that if you edit a tiddler and save it than you have to wait one second before you can edit it again. * Tweaks to appearance of alerts * Exclude temp tiddlers from offline snapshots Otherwise alerts will persist * Tweak appearance of status line in dropdown * Update release note * Web server: Don't include full path in error messages Fixes #3724 * In change event handler check for deletions * Disable the official plugin library when the tiddlyweb plugin is loaded * Hide error details from browser for /files/ route See https://github.com/Jermolene/TiddlyWiki5/issues/3724#issuecomment-565702492 -- thanks @pmario * Revert all the changes to the relationship between the syncer and the syncadaptor Previously we had some major rearrangements to make it possible for the syncadaptor to route it's logging to the logger used by the syncer. The motivation is so that the "copy logs to clipboard" button is more useful. On reflection, changing the interface this drastically is undesirable from a backwards compatibility perspective, so I'm going to investigate other ways to achieve the logger sharing * Make the tiddlyweb adaptor use the syncer's logger So that both are availavble when copying the syncer logs to the clipboard * Update release note * Support setting port=0 to get an OS assigned port Quite useful * Update code comment * UI: Use "Get latest changes from server" instead of "Refresh" * Add getUpdatedTiddlers() method to syncadaptor API See https://github.com/Jermolene/TiddlyWiki5/pull/4373#issuecomment-573579495 * Refactor revision handling within the syncer Thanks @pmario * Fix typo in tiddlywebadaptor * Improve presentation of errors See https://github.com/Jermolene/TiddlyWiki5/pull/4373#issuecomment-573695267 * Add docs for getTiddlerRevision() * Remove unused error animation * Update comment for GET /recipes/default/tiddlers/tiddlers.json * Optimise SVG cloud image * Add optional list of allowed filters for get all tiddlers route An attempt to address @Arlen22's concern here: https://github.com/Jermolene/TiddlyWiki5/pull/4373#pullrequestreview-342146190 * Fix network error alert text translatability * Fix error code and logging for GET /recipes/default/tiddlers/tiddlers.json Thanks @Arlen22 * Flip GET /recipes/default/tiddlers/tiddlers.json allowed filter handling to be secure by default * Validate updates received from getUpdatedTiddlers() * Add syncer method to force loading of a tiddler from the server * Remove the release note update to remove the merge conflict * Fix crash when there's no config section in the tiddlywiki.info file * Use config tiddler title to check filter query (merge into fix-syncer) (#4478) * Use config tiddler title to check filter query * Create config-tiddlers-filter.tid * Add config switch to enable all filters on GET /recipes/default/tiddlers/tiddlers.json And update docs * Fix bug when deleting a tiddler with a shadow Reported by @kookma at https://github.com/Jermolene/TiddlyWiki5/pull/4373#issuecomment-604027528 Co-authored-by: jed <inmysocks@fastmail.com> Co-authored-by: Arlen22 <arlenbee@gmail.com>
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");
};
exports.parseNumber = function(str) {
return parseFloat(str) || 0;
};
exports.parseInt = function(str) {
return parseInt(str,10) || 0;
};
exports.stringifyNumber = function(num) {
return num + "";
};
2020-04-25 10:26:19 +00:00
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,
2020-04-25 10:26:19 +00:00
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);
2020-04-25 10:26:19 +00:00
},
"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"});
2020-04-25 10:26:19 +00:00
}
};
return (types[type] || types[options.defaultType] || types.number);
};
})();