mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-11-17 15:24:50 +00:00
bf74d13df5
* 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?
719 lines
17 KiB
JavaScript
719 lines
17 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 warning, in colour if we're on a terminal
|
|
*/
|
|
exports.warning = function(text) {
|
|
console.log($tw.node ? "\x1b[1;33m" + text + "\x1b[0m" : text);
|
|
};
|
|
|
|
/*
|
|
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;
|
|
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());
|
|
}],
|
|
[/^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();
|
|
}],
|
|
[/^[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);
|
|
}]
|
|
];
|
|
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 n = Math.floor((dt.getTime()-new Date(dt.getFullYear(),0,1) + 3600000) / 86400000);
|
|
return Math.floor(n / 7) + 1;
|
|
};
|
|
|
|
exports.getYearForWeekNo = function(date) {
|
|
var dt = new Date(date.getTime());
|
|
var d = dt.getDay();
|
|
if(d === 0) {
|
|
d = 7; // JavaScript Sun=0, ISO Sun=7
|
|
}
|
|
dt.setTime(dt.getTime() + (4 - d) * 86400000);// shift day to Thurs of same week
|
|
return dt.getFullYear();
|
|
};
|
|
|
|
exports.getHours12 = function(date) {
|
|
var h = date.getHours();
|
|
return h > 12 ? h-12 : ( h > 0 ? h : 12 );
|
|
};
|
|
|
|
/*
|
|
Convert a date delta in milliseconds into a string representation of "23 seconds ago", "27 minutes ago" etc.
|
|
delta: delta in milliseconds
|
|
Returns an object with these members:
|
|
description: string describing the delta period
|
|
updatePeriod: time in millisecond until the string will be inaccurate
|
|
*/
|
|
exports.getRelativeDate = function(delta) {
|
|
var futurep = false;
|
|
if(delta < 0) {
|
|
delta = -1 * delta;
|
|
futurep = true;
|
|
}
|
|
var units = [
|
|
{name: "Years", duration: 365 * 24 * 60 * 60 * 1000},
|
|
{name: "Months", duration: (365/12) * 24 * 60 * 60 * 1000},
|
|
{name: "Days", duration: 24 * 60 * 60 * 1000},
|
|
{name: "Hours", duration: 60 * 60 * 1000},
|
|
{name: "Minutes", duration: 60 * 1000},
|
|
{name: "Seconds", duration: 1000}
|
|
];
|
|
for(var t=0; t<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 "&", < to "<", > to ">", " to """
|
|
exports.htmlEncode = function(s) {
|
|
if(s) {
|
|
return s.toString().replace(/&/mg,"&").replace(/</mg,"<").replace(/>/mg,">").replace(/\"/mg,""");
|
|
} else {
|
|
return "";
|
|
}
|
|
};
|
|
|
|
// Converts all HTML entities to their character equivalents
|
|
exports.entityDecode = function(s) {
|
|
var 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(/[\x80-\uFFFF]/g, exports.escape); // non-ASCII characters
|
|
};
|
|
|
|
/*
|
|
Escape the RegExp special characters with a preceding backslash
|
|
*/
|
|
exports.escapeRegExp = function(s) {
|
|
return s.replace(/[\-\/\\\^\$\*\+\?\.\(\)\|\[\]\{\}]/g, '\\$&');
|
|
};
|
|
|
|
// Checks whether a link target is external, i.e. not a tiddler title
|
|
exports.isLinkExternal = function(to) {
|
|
var externalRegExp = /^(?:file|http|https|mailto|ftp|irc|news|data|skype):[^\s<>{}\[\]`|'"\\^~]+(?:\/|\b)$/i;
|
|
return externalRegExp.test(to);
|
|
};
|
|
|
|
exports.nextTick = function(fn) {
|
|
/*global window: false */
|
|
if(typeof process === "undefined") {
|
|
// Apparently it would be faster to use postMessage - http://dbaron.org/log/20100309-faster-timeouts
|
|
window.setTimeout(fn,4);
|
|
} else {
|
|
process.nextTick(fn);
|
|
}
|
|
};
|
|
|
|
/*
|
|
Convert a hyphenated CSS property name into a camel case one
|
|
*/
|
|
exports.unHyphenateCss = function(propName) {
|
|
return propName.replace(/-([a-z])/gi, function(match0,match1) {
|
|
return match1.toUpperCase();
|
|
});
|
|
};
|
|
|
|
/*
|
|
Convert a camelcase CSS property name into a dashed one ("backgroundColor" --> "background-color")
|
|
*/
|
|
exports.hyphenateCss = function(propName) {
|
|
return propName.replace(/([A-Z])/g, function(match0,match1) {
|
|
return "-" + match1.toLowerCase();
|
|
});
|
|
};
|
|
|
|
/*
|
|
Parse a text reference of one of these forms:
|
|
* title
|
|
* !!field
|
|
* title!!field
|
|
* title##index
|
|
* etc
|
|
Returns an object with the following fields, all optional:
|
|
* title: tiddler title
|
|
* field: tiddler field name
|
|
* index: JSON property index
|
|
*/
|
|
exports.parseTextReference = function(textRef) {
|
|
// Separate out the title, field name and/or JSON indices
|
|
var reTextRef = /(?:(.*?)!!(.+))|(?:(.*?)##(.+))|(.*)/mg,
|
|
match = reTextRef.exec(textRef),
|
|
result = {};
|
|
if(match && reTextRef.lastIndex === textRef.length) {
|
|
// Return the parts
|
|
if(match[1]) {
|
|
result.title = match[1];
|
|
}
|
|
if(match[2]) {
|
|
result.field = match[2];
|
|
}
|
|
if(match[3]) {
|
|
result.title = match[3];
|
|
}
|
|
if(match[4]) {
|
|
result.index = match[4];
|
|
}
|
|
if(match[5]) {
|
|
result.title = match[5];
|
|
}
|
|
} else {
|
|
// If we couldn't parse it
|
|
result.title = textRef
|
|
}
|
|
return result;
|
|
};
|
|
|
|
/*
|
|
Checks whether a string is a valid fieldname
|
|
*/
|
|
exports.isValidFieldName = function(name) {
|
|
if(!name || typeof name !== "string") {
|
|
return false;
|
|
}
|
|
name = name.toLowerCase().trim();
|
|
var fieldValidatorRegEx = /^[a-z0-9\-\._]+$/mg;
|
|
return fieldValidatorRegEx.test(name);
|
|
};
|
|
|
|
/*
|
|
Extract the version number from the meta tag or from the boot file
|
|
*/
|
|
|
|
// Browser version
|
|
exports.extractVersionInfo = function() {
|
|
if($tw.packageInfo) {
|
|
return $tw.packageInfo.version;
|
|
} else {
|
|
var metatags = document.getElementsByTagName("meta");
|
|
for(var t=0; t<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 -= str.length;
|
|
var lastIndex = str.indexOf(ending, position);
|
|
return lastIndex !== -1 && lastIndex === position;
|
|
}
|
|
};
|
|
|
|
})();
|