/**
@preserve
Copyright 2021 Sleepless Software Inc. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/
/**
Returns true if we appear to be running in a browser (as opposed to Node, etc.)
*/
function isBrowser() {
return typeof globalThis.navigator === "object";
}
/**
An alias for console.log.
Behaves identically.
*/
function log( ...args ) {
console.log.apply( this, args );
//if( typeof m === "object" ) {
// return console.dir( m );
//}
//return console.log( m );
}
/**
Throws an Error if a condition is true.
@arg {any} - that which is tested for truthiness
@arg {string} - Message displayd by the Error object if condition is true
*/
function throwIf( c, s = "FAILED ASSERTION" ) {
if( c ) {
throw new Error( s );
}
return c;
}
/**
Convert an object to JSON text without throwing.
@arg {any} - The object to converted.
@returns {string} JSON string, or null if JSON.stringify() threw an Error.
@see {@link j2o}
*/
function o2j(v, r = null, s = 2) { try { return JSON.stringify(v, r, s) } catch(e) { return null } }
/**
Convert JSON string to object or primitive.
CAUTION: there is no way to distinguish between this function encountering an error
in parsing and a successful conversion of the string "null" to null.
@arg {sting} - The string to be converted
@returns {varies} - The output of JSON.parse() or null if it threw an Error
@see {@link o2j}
*/
function j2o(j) { try { return JSON.parse(j) } catch(e) { return null } }
/**
Convert whatever to float or 0 if not at all number-like.
CAUTION: This is not very smart; It just coerces to string, then strips out extraneous chars
@arg {any} - What you want to convert
@returns {number}
@example
toFlt( "123.9" ) // 123.9
toFlt( null ) // 0.0
toFlt( undefined ) // 0.0
toFlt( NaN ) // 0.0
toFlt( 123.9 ) // 123.9
*/
function toFlt(v) {
return parseFloat((""+v).replace(/[^-.0-9]/g, "")) || 0.0;
}
/**
Convert whatever to integer or 0 if not at all number-like.
CAUTION: This is not very smart; It just coerces to string, then strips out extraneous chars
CAUTION: Uses Math.round()
@arg {any} - What you want to convert
@returns {number} - an integer
@example
toFlt( "123.9" ) // 123
toFlt( null ) // 0
toFlt( undefined ) // 0
toFlt( NaN ) // 0
toFlt( 123.9 ) // 124
*/
function toInt(v) {
return Math.round( toFlt( v ) );
};
/**
Convert from pennies to dollars.
Note: This is different from simply dividing by 100, in that
the arg is passed through toFlt(), so you can pass in any object
and it will try to do a sensible thing with it.
@arg {number} - A number of pennies
@returns {number} - a number fixed to 2 decimals
@example
centsToBucks( 123 ) // 1.23
centsToBucks( "123" ) // 1.23
centsToBucks( [123] ) // 0.0
centsToBucks( NaN ) // 0.0
@see {@link bucksToCents}
*/
function centsToBucks(cents) {
return M.toFlt( M.toInt(cents) / 100 );
}
/**
An alias for centsToBucks().
@see {@link centsToBucks}
*/
function c2b( cents ) {
return centsToBucks( cents );
}
/**
Convert from dollars to pennies.
Note: This is different from simply multiplying by 100, in that
the arg is passed through toFlt(), so you can pass in any object
and it will try to do a sensible thing with it.
@arg {number} - A float - dollars and cents
@returns {number} - a whole number of cents
@example
bucksToCents( 1.23 ) // 123
bucksToCents( "1.23" ) // 123
bucksToCents( [1.23] ) // 0
bucksToCents( NaN ) // 0
@see {@link centsToBucks}
*/
function bucksToCents(bucks) {
return Math.round( (M.toFlt(bucks) * 1000) / 10 );
}
/**
An alias for bucksToCents().
@see {@link bucksToCents}
*/
function b2c( bucks ) {
return bucksToCents( bucks );
}
/**
Format a number into a string with any # of decimal places,
and optional alternative decimal & thousand-separation chars
@arg {number} - the number to format
@arg {number} - decimal places
@arg {string} - decimal point separator
@arg {string} - thousands separator
@returns {string}
@example
numFmt( 1234.56 ) // "1,235"
numFmt( 1234.56, 1 ) // "1,234.6"
numFmt( 1234.56, 1, "," ) // "1,234,6"
numFmt( 1234.56, 1, "_" ) // "1,234_6"
numFmt( 1234.56, 1, ",", "." ) // "1.234,6"
numFmt( 1234.56, 1, ".", "" ) // "1234.6"
*/
function numFmt(n, plcs, dot, sep) {
n = M.toFlt(n);
sep = typeof sep === "string" ? sep : ","; // thousands separator char
dot = typeof dot === "string" ? dot : "."; // decimal point char
plcs = M.toInt(plcs);
let p = Math.pow(10, plcs);
n = Math.round( n * p ) / p;
let sign = n < 0 ? '-' : '';
n = Math.abs(+n || 0);
let intPart = parseInt(n.toFixed(plcs), 10) + '';
let j = intPart.length > 3 ? intPart.length % 3 : 0;
return sign +
(j ? intPart.substr(0, j) + sep : '') +
intPart.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + sep) +
(plcs ? dot + Math.abs(n - intPart).toFixed(plcs).slice(-plcs) : '');
}
/**
Convert fraction to percent.
convert something like 0.12 to a string that looks like "12" with
optional alternate decimal and thousands-seperator chars
NOTE: there is no "%" added, you have to do that yourself if you want it.
@arg {number} - the number to format
@arg {number} - decimal places
@arg {string} - decimal point separator
@arg {string} - thousands separator
@returns {string}
@example
toPct( 0.4 ) + "%" // "40%"
toPct( 123.4,",", "." ) // "12,340"
*/
function toPct(n, plcs, dot, sep) {
return M.numFmt(n * 100, plcs, dot, sep);
}
/**
Convert whatever to a string that looks like "1,234.56"
Add the $ symbol yourself.
@arg {number} - the number to format
@arg {string} - decimal point separator
@arg {string} - thousands separator
@returns {string}
@example
toMoney( 1234.56 ) // "1,234.56"
toMoney( 1234.56, 1, ".", "" ) // "1.234,56"
*/
function toMoney(n, dot, sep) {
return M.numFmt(n, 2, dot, sep);
}
/**
Returns a human readable string that describes 'n' as a number of bytes,
"1 KB", "21.5 MB", etc.
@arg {number}
@returns {string}
*/
function byteSize(sz) {
if(typeof sz != "number")
return sz;
if(sz < 1024)
return M.numFmt(sz, 0) + " B"
sz = sz / 1024
if(sz < 1024)
return M.numFmt(sz, 1) + " KB"
sz = sz / 1024
if(sz < 1024)
return M.numFmt(sz, 1) + " MB"
sz = sz / 1024
if(sz < 1024)
return M.numFmt(sz, 1) + " GB"
sz = sz / 1024
return M.numFmt(sz, 1) + " TB"
}
/**
Return a Unix timestamp for current time, or for a Date object if provided
*/
function time( dt ) {
if( ! dt ) dt = new Date();
return M.toInt( dt.getTime() / 1000 );
}
/**
Convert "YYYY-MM-YY" or "YYYY-MM-YY HH:MM:SS" to Unix timestamp
*/
function my2ts(m) {
if( m.length == 10 && /\d\d\d\d-\d\d-\d\d/.test(m) ) {
m += " 00:00:00";
}
if(m === "0000-00-00 00:00:00") {
return 0;
}
var a = m.split( /[^\d]+/ );
if(a.length != 6) {
return 0;
}
var year = M.toInt(a[0]);
var month = M.toInt(a[1]);
var day = M.toInt(a[2]);
var hour = M.toInt(a[3]);
var minute = M.toInt(a[4]);
var second = M.toInt(a[5]);
var d = new Date(year, month - 1, day, hour, minute, second, 0);
return M.toInt(d.getTime() / 1000);
}
/** Convert Unix timestamp to "YYYY-MM-DD HH:MM:SS" */
function ts2my(ts) {
var d = M.ts2dt(ts);
if(!d) {
return "";
}
return ""+
d.getFullYear()+
"-"+
("0"+(d.getMonth() + 1)).substr(-2)+
"-"+
("0"+d.getDate()).substr(-2)+
" "+
("0"+d.getHours()).substr(-2)+
":"+
("0"+d.getMinutes()).substr(-2)+
":"+
("0"+d.getSeconds()).substr(-2)+
"";
}
/** Convert Unix timestamp to Date object
Returns null (NOT a date object for "now" as you might expect) if ts is falsey.
*/
function ts2dt(ts) {
ts = M.toInt(ts);
return ts ? new Date(ts * 1000) : null;
};
/** Convert Date object to Unix timestamp
*/
function dt2ts(dt) {
if(! (dt instanceof Date) )
return 0;
return M.toInt(dt.getTime() / 1000);
};
/**
Convert "MM/DD/YYYY HH:MM:SS" to Date object or null if string can't be parsed
If year is 2 digits, it will try guess the century (not recommended).
Time part (HH:MM:SS) can be omitted and seconds is optional
if utc argument is truthy, then return a UTC version
*/
function us2dt(us, utc) {
if(!us) {
return null;
}
var m = (""+us).split( /[^\d]+/ );
if(m.length < 3) {
return null;
}
while(m.length < 7) {
m.push("0");
}
// try to convert 2 digit year to 4 digits (best guess)
var year = M.toInt(m[2]);
var nowyear = new Date().getFullYear();
if(year <= ((nowyear + 10) - 2000))
year = 2000 + year;
if(year < 100)
year = 1900 + year;
var mon = M.toInt(m[0]) - 1;
var date = M.toInt(m[1]);
var hour = M.toInt(m[3]);
var min = M.toInt(m[4]);
var sec = M.toInt(m[5]);
var ms = M.toInt(m[6]);
if(utc) {
return new Date(Date.UTC(year, mon, date, hour, min, sec, ms));
}
return new Date(year, mon, date, hour, min, sec, ms);
}
/**
Convert "MM/DD/YYYY HH:MM:SS" to Unix timestamp.
If utc argument is truthy, then return a UTC version.
*/
function us2ts(us, utc) {
return M.dt2ts(M.us2dt(us, utc));
}
/**
Convert Unix timestamp to "MM/DD/YYYY HH:MM:SS" or "" if ts is 0
*/
function ts2us(ts) {
var d = M.ts2dt(ts);
if(!d) {
return "";
}
return ""+
("0"+(d.getMonth() + 1)).substr(-2)+
"/"+
("0"+d.getDate()).substr(-2)+
"/"+
d.getFullYear()+
" "+
("0"+d.getHours()).substr(-2)+
":"+
("0"+d.getMinutes()).substr(-2)+
":"+
("0"+d.getSeconds()).substr(-2)+
"";
}
/**
Convert Unix timestamp to "MM/DD" or "" if ts is 0
*/
function ts2us_md(ts) {
return M.ts2us(ts).substr(0, 5);
}
/**
Convert Unix timestamp to "MM/DD/YYYY" or "" if ts is 0
*/
function ts2us_mdy(ts) {
return M.ts2us(ts).substr(0, 10);
}
/**
Convert Unix timestamp to "MM/DD/YY" or "" if ts is 0
*/
function ts2us_mdy2(ts) {
let us = M.ts2us_mdy(ts);
if( us != "" ) {
var a = us.split("/");
a[2] = a[2].substr(2);
us = a.join("/");
}
return us;
}
/**
Convert Unix timestamp to "HH:MM" or "" if ts is 0
*/
function ts2us_hm(ts) {
return M.ts2us(ts).substr(11, 5);
}
/**
Convert Unix timestamp to "MM/DD/YYYY HH:MM" or "" if ts is 0
*/
function ts2us_mdyhm(ts) {
let s = M.ts2us_mdy(ts) + " " + M.ts2us_hm(ts);
return s != " " ? s : "" ;
}
/**
Convert Unix timestamp to "MM/DD/YY HH:MM" or "" if ts is 0
*/
function ts2us_mdy2hm(ts) {
let s = M.ts2us_mdy2(ts) + " " + M.ts2us_hm(ts);
return s != " " ? s : "" ;
}
/**
Convert Unix timestamp to something like "01-Jan-2016" or "" if ts is 0
*/
function ts2us_dMy(ts) {
var d = M.ts2dt(ts);
if(!d) {
return "";
}
var month_names = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ");
return ""+
("0"+d.getDate()).substr(-2)+
"-"+
month_names[d.getMonth()]+
"-"+
d.getFullYear();
}
/**
return an array containing only distinct values.
E.g. [ 1,2,2 ].distinct() // [1,2]
*/
Array.prototype.distinct = function( cb ) {
let hash = {};
for( let el of this ) {
hash[ cb ? cb( el ) : (""+el) ] = true;
}
return Object.keys( hash );
}
/**
Make all lowercase
E.g. "Foo".lcase() // "foo"
*/
String.prototype.lcase = function() { return this.toLowerCase() }
/**
Make all uppercase
E.g. "Foo".ucase() // "FOO"
*/
String.prototype.ucase = function() { return this.toUpperCase() }
/**
Capitalize first word
E.g. "foo bar".ucfirst() // "Foo bar"
*/
String.prototype.ucfirst = function() {
return this.substring(0,1).toUpperCase() + this.substring(1)
}
/**
Capitalize all words
E.g. "foo bar".ucwords() // "Foo Bar"
*/
String.prototype.ucwords = function( sep ) {
sep = sep || /[\s]+/;
var a = this.split( sep );
for( var i = 0; i < a.length; i++ ) {
a[ i ] = a[ i ].ucfirst();
}
return a.join( " " );
}
/**
Returns true if the string begins with the prefix string
E.g. "Foobar".startsWith( "Foo" ) // true
E.g. "foobar".startsWith( "Foo" ) // false
TODO: support regexp arg
*/
if( String.prototype.startsWith === undefined ) {
String.prototype.startsWith = function(prefix) {
return this.substr(0, prefix.length) == prefix;
}
}
/**
Returns true if the string ends with the suffix string
E.g. "Foobar".endsWith( "bar" ) // true
E.g. "foobar".endsWith( "Bar" ) // false
TODO: support regexp arg
*/
if( String.prototype.endsWith === undefined ) {
String.prototype.endsWith = function(suffix) {
return this.substr(-suffix.length) == suffix;
}
}
/**
Abbreviate to 'l' chars with ellipses
E.g. "Foo bar baz".abbr(6) // "Fo ..."
*/
String.prototype.abbr = function(l) {
l = M.toInt(l) || 5;
if(this.length <= l) {
return "" + this; // Cuz ... some times this === a String object, not a literal
}
return this.substr(0, l - 4) + " ...";
}
/**
Convert a string from something like "prof_fees" to "Prof Fees"
*/
String.prototype.toLabel = function() {
var s = this.replace( /[_]+/g, " " );
s = s.ucwords();
return s;
}
/**
Convert a string from something like "Prof. Fees" to "prof_fees"
*/
String.prototype.toId = function() {
var s = this.toLowerCase();
s = s.replace( /[^a-z0-9]+/g, " " );
s = s.trim();
s = s.replace( /\s+/g, "_" );
return s;
}
/**
Returns true if string contains all of the arguments irrespective of case
"I,\nhave a lovely bunch of coconuts".looksLike("i have", "coconuts") == true
*/
String.prototype.looks_like = function() {
var a = Array.prototype.slice.call(arguments); // convert arguments to true array
var s = "_" + this.toId() + "_"; //.split("_"); //toLowerCase();
for(var i = 0; i < a.length; i++) {
var t = "_" + (a[i].toId()) + "_";
if(s.indexOf(t) == -1)
return false;
}
return true;
}
String.prototype.looksLike = String.prototype.looks_like; // Deprecate
/**
Replaces instances of "__key__" in string s,
with the values from corresponding key in data.
*/
String.prototype.substitute = function( data ) {
let s = this;
for( let key in data ) {
let re = new RegExp( "__" + key + "__", "g" );
s = s.replace( re, "" + data[ key ] );
}
return s;
}
/**
Returns true if the string looks like a valid email address
*/
String.prototype.is_email = function() {
return /^[A-Za-z0-9_\+-]+(\.[A-Za-z0-9_\+-]+)*@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.([A-Za-z]{2,})$/.test(this);
}
// XXX
// Create this:
//String.prototype.is_url = function() {
//}
/**
Returns a human readable relative time description for a Unix timestmap versus "now"
E.M. agoStr( time() - 60 ) // "60 seconds ago"
E.M. agoStr( time() - 63 ) // "1 minute ago"
Pass a truthy value for argument 'no_suffix' to suppress the " ago" at the end
*/
function ago_str(ts, no_suffix) {
if(ts == 0)
return "";
var t = M.time() - ts;
if(t < 1)
return "Just now";
var v = ""
var round = Math.round
if(t>31536000) v = round(t/31536000,0)+' year';
else if(t>2419200) v = round(t/2419200,0)+' month';
else if(t>604800) v = round(t/604800,0)+' week';
else if(t>86400) v = round(t/86400,0)+' day';
else if(t>3600) v = round(t/3600,0)+' hour';
else if(t>60) v = round(t/60,0)+' minute';
else v = t+' second';
if(M.toInt(v) > 1)
v += 's';
return v + (no_suffix ? "" : " ago");
}
/**
Run some functions in parallel / simultaneously
runp() (no args) is DEPRECATED
*/
function runp( a_this, ...args ) {
const legacy_runp = function() {
var o = {};
var q = [];
var add = function add() {
let args = Array.prototype.slice.call(arguments);
if( typeof args[ 0 ] === "function" ) {
q.push( args );
return o;
}
let arr = args.shift();
let fun = args.shift();
arr.forEach( x => {
q.push( [ fun, x ].concat( args ) );
});
return o;
};
var run = function(cb) {
var errors = [];
var results = [];
var num_done = 0;
if( q.length == 0 ) {
if(cb) {
cb(errors, results);
}
return;
}
q.forEach(function(args, i) {
let fun = args.shift();
args.unshift( function(e, r) {
errors[i] = e || null;
results[i] = r || null;
num_done += 1;
if(num_done == q.length) {
if(cb) {
cb(errors, results);
}
}
} );
fun.apply( null, args );
});
};
o.add = add;
o.run = run;
return o;
};
if( a_this === undefined && args.length == 0 ) {
return legacy_runp(); // revert to old behavior
}
const calls = [];
// Add a call
const add = function( fun, ...args ) {
calls.push( { fun, args } );
return me;
}
const run = function( done ) {
const results = [];
let num_done = 0;
// advance the num_done count, then if we're finished, call done()
const one_done = function() {
num_done += 1;
if( num_done == calls.length )
done( results );
};
// step through all the calls and fire them off
calls.forEach( ( next, i ) => {
const { fun, args } = next; // dereference function and args
// append okay, fail funcs to args
args.push( function( data ) {
results[ i ] = { data }; // store data in results and advance done count
one_done();
} );
args.push( function( error ) {
results[ i ] = { error }; // store error in results and advance done count
one_done();
} );
fun.apply( a_this, args ); // call the function with the remaining array elements as args
} );
return me;
}
const me = { add, run };
return me;
};
/**
Run some functions sequentially / synchronously ( see test.js )
runq() (no args) is DEPRECATED
*/
function runq( a_this, ...args ) {
// This is the old original version
const legacy_runq = function() {
var o = {};
var q = []
var add = function(f) {
q.push(f);
return o;
};
var run = function(cb, arg) {
if(q.length == 0) {
cb(null, arg);
return;
}
var f = q.shift();
f(function(e, arg) {
if(e) {
q = [];
cb(e, arg);
}
else {
run(cb, arg);
}
}, arg);
};
o.add = add
o.run = run
return o
};
if( a_this === undefined && args.length == 0 ) {
return legacy_runq(); // revert to old behavior
}
const queue = []; // holds the queued calls
const results = []; // collects the results from each call
// Add a call to the queue
const add = function( fun, ...args ) {
queue.push( { fun, args } );
return me;
}
// starts the queue running
const run = function( _okay, _fail ) {
const call_one = function() {
const next = queue.shift(); // get next call from queue, which is an array
if( ! next ) {
// queue empty; all done
_okay( results );
return;
}
const { fun, args } = next; // dereference function and args
// append okay and fail args
args.push( function( data ) {
results.push( data ); // store the returned results
setTimeout( call_one, 1 ); // move on to the next call
} );
args.push( function( error ) {
_fail( error ); // call the _fail function; nothing else happens
} );
fun.apply( a_this, args ); // call the function with the remaining array elements as args
};
call_one();
return me;
}
const me = { add, run };
return me;
};
/**
Sort of like Markdown, but not really.
@arg {string}
@returns {string}
@see {@link t2h}
*/
function text2html( t ) {
// nuke CRs
t = t.replace(/\r/gi, "\n")
// remove leading/trailing whitespace on all lines
// t = t.split( /\n/ ).map( l => l.trim() ).join( "\n" );
// append/prepend a couple newlines so that regexps below will match at beginning and end
t = "\n\n" + t + "\n\n"; // note: will cause a <p> to always appear at start of output
// DEPRECATE - old style
// hyper link/anchor
// (link url)
// (link url alt_display_text)
t = t.replace(/\(\s*link\s+([^\s\)]+)\s*\)/gi, "<a href=\"$1\">$1</a>");
t = t.replace(/\(\s*link\s+([^\s\)]+)\s*([^\)]+)\)/gi, "<a href=\"$1\">$2</a>");
// DEPRECATE - old style
// hyper link/anchor that opens in new window/tab
// (xlink url)
// (xlink url alt_display_text)
t = t.replace(/\(\s*xlink\s+([^\s\)]+)\s*\)/gi, "<a target=_blank href=\"$1\">$1</a>");
t = t.replace(/\(\s*xlink\s+([^\s\)]+)\s*([^\)]+)\)/gi, "<a target=_blank href=\"$1\">$2</a>");
// DEPRECATE - old style
// image
// (image src title)
t = t.replace(/\(\s*image\s+([^\s\)]+)\s*\)/gi, "<img src=\"$1\">");
t = t.replace(/\(\s*image\s+([^\s\)]+)\s*([^\)]+)\)/gi, "<img src=\"$1\" title=\"$2\">");
// hyper link/anchor
t = t.replace( /\^\^\s*([^\s]*)\s*\^\^/gi, "^$1 $1^" );
t = t.replace( /\^\^\s*([^\s]*)\s+([^\^]+)\^\^/gi, "<a target=_blank href=\"$1\">$2</a>" );
t = t.replace( /\^\s*([^\s]*)\s*\^/gi, "^$1 $1^" );
t = t.replace( /\^\s*([^\s]*)\s+([^\^]+)\^/gi, "<a href=\"$1\">$2</a>" );
// image
t = t.replace(/\|\s*([^\s\)]+)\s*\|/gi, "(image $1 $1)");
t = t.replace(/\|\s*([^\s\)]+)\s*([^\)]+)\|/gi, "<img src=\"$1\" title=\"$2\">");
// figure
// (figure src caption)
t = t.replace(/\(\s*figure\s+([^\s\)]+)\s*\)/gi, "(figure $1 $1)");
t = t.replace(/\(\s*figure\s+([^\s\)]+)\s*([^\)]+)\)/gi, "<figure><img src=\"$1\" title=\"$2\"><figcaption>$2</figcaption></figure>");
// symbols
// (tm)
// (r)
// (c)
// (cy) "(C) 2021"
// (cm Foocorp) "(C) 2021 Foocorp All Rights Reserved"
t = t.replace(/\(tm\)/gi, "™");
t = t.replace(/\(r\)/gi, "®");
t = t.replace(/\(c\)/gi, "©");
//t = t.replace(/\(cy\)/gi, "© "+(new Date().getFullYear()));
//t = t.replace(/\(cm\s([^)]+)\)/gi, "© "+(new Date().getFullYear())+" $1 – All Rights Reserved" )
// one or more blank lines mark a paragraph
t = t.replace(/\n\n+/gi, "\n\n<p>\n");
// headings h1 and h2
// Heading 1
// =========
// Heading 2
// ---------
// Heading 3
// - - - - -
// Heading 4
// - - - - -
// Heading 5
// - - - - -
t = t.replace(/\n([^\s\n][^\n]+)\n(-\s\s\s){4,}-\n/gi, "\n<h5>$1</h5>\n" );
t = t.replace(/\n([^\s\n][^\n]+)\n(-\s\s){4,}-\n/gi, "\n<h4>$1</h4>\n" );
t = t.replace(/\n([^\s\n][^\n]+)\n(-\s){4,}-\n/gi, "\n<h3>$1</h3>\n" );
t = t.replace(/\n([^\s\n][^\n]+)\n-{5,}\n/gi, "\n<h2>$1</h2>\n" );
t = t.replace(/\n([^\s\n][^\n]+)\n={5,}\n/gi, "\n<h1>$1</h1>\n" );
// styles
// //italic//
// **bold**
// __underline__
t = t.replace(/([^:])\/\/(.*)\/\//gi, "$1<i>$2</i>");
t = t.replace(/\*\*(.*)\*\*/gi, "<b>$1</b>");
t = t.replace(/__(.*)__/gi, "<u>$1</u>");
// "
// block quote text
// "
t = t.replace(/\n\s*"\s*\n([^"]+)"\s*\n/gi, "\n<blockquote>$1</blockquote>\n"); // blockquote
// >
// centered text
// >
t = t.replace(/\n\s*>\s*\n([^>]+)>\s*\n/gi, "\n<div style='text-align:center;'>$1</div>\n");
// >>
// right justified text
// >>
t = t.replace(/\n\s*>>\s*\n([^>]+)>>\s*\n/gi, "\n<div style='text-align:right'>$1</div>\n");
// {{
// code block
// }}
// foo {inline code} bar
t = t.split( "{{\n" ).join( "<pre><code>" ); // code block
t = t.split( "}}\n" ).join( "</code></pre>" );
t = t.replace(/{([^}]+)}/gi, "<code>$1</code>"); // inline code
// Unordered list
// - item
// - item
t = t.replace(/\n((\s*-\s+[^\n]+\n)+)/gi, "\n<ul>\n$1\n</ul>");
t = t.replace(/\n\s*-\s+/gi, "\n<li>");
// Ordered list
// 1. item 1
// # item 2
// 1. item 3
t = t.replace(/\n((\s*(\d+|#)\.?\s+[^\n]+\n)+)/gi, "\n<ol>\n$1\n</ol>");
t = t.replace(/\n\s*(\d+|#)\.?\s+([^\n]+)/gi, "\n<li>$2</li>");
// Horiz. rule
// ---- (4 or more dashes)
t = t.replace(/\n\s*-{4,}\s*\n/gi, "\n<hr>\n"); // horizontal rule
// Dashes
// -- (n-dash)
// --- (m-dash)
t = t.replace(/-{3}/gi, "—"); // mdash
t = t.replace(/-{2}/gi, "–"); // ndash
if( typeof navigator !== "undefined" ) {
// Only supported if running in browser
// (lastModified) // last modified data of document.
t = t.replace(/\(\s*lastModified\s*\)/gi, document.lastModified);
}
return t;
};
/**
Alias for text2html()
@see {@link text2html}
*/
function t2h( text ) {
return text2html( text );
}
// - - - - - - - - - - -
// The inimitable Log5 ...
// - - - - - - - - - - -
(function() {
var util = null;
var style = null;
if( ! isBrowser ) {
util = require( "util" );
// style = require( "./ansi-styles.js" );
}
const n0 = function(n) {
if(n >= 0 && n < 10)
return "0"+n
return n
}
const ts = function() {
var d = new Date()
return d.getFullYear() + "-" +
n0(d.getMonth()+1) + "-" +
n0(d.getDate()) + "_" +
n0(d.getHours()) + ":" +
n0(d.getMinutes()) + ":" +
n0(d.getSeconds())
}
const mkLog = function(prefix) {
prefix = " " + (prefix || "")
var o = {}
o.logLevel = 0
var f = function logFunc( l ) {
var n = 0, ll = l
if( typeof l === "number" ) { // if first arg is a number ...
if(arguments.length == 1) { // and it's the only arg ...
o.logLevel = l // set logLevel to l
return logFunc // and return
}
// there are more args after the number
n = 1 // remove the number from arguments array
}
else {
ll = 0 // first arg is not number, log level for this call is 0
}
if( o.logLevel < ll ) // if log level is below the one given in this call ...
return logFunc; // just do nothing and return
let s = ts() + prefix;
for( var i = n; i < arguments.length; i++ ) { // step through args
let x = arguments[ i ];
if( x === undefined ) {
x = "undefined";
}
if( typeof x === "object" ) { // if arg is an object ...
if( ! isBrowser ) { // and we're in node ...
x = util.inspect( x, { depth: 10 } ); // convert obj to formatted JSON
} else { // otherwise ...
x = M.o2j( x ); // just convert obj to JSON
}
}
s += x; // append to the growing string
}
if( (! isBrowser) && style ) {
if( process.stdout.isTTY ) {
switch( ll ) {
case 1:
s = `${style.red.open}${s}${style.red.close}`;
break;
case 2:
s = `${style.yellow.open}${s}${style.yellow.close}`;
break;
case 3:
break;
case 4:
s = `${style.cyan.open}${s}${style.cyan.close}`;
break;
case 5:
s = `${style.magenta.open}${s}${style.magenta.close}`;
break;
}
}
process.stdout.write( s + "\n" ); // write string to stdout
} else {
switch( ll ) {
case 1: console.error( s ); break;
case 2: console.warn( s ); break;
default: console.log( s ); break;
}
}
return logFunc
}
f.E = function( s ) { f( 1, "******* " + s ); } // error
f.W = function( s ) { f( 2, "- - - - " + s ); } // warning
f.I = function( s ) { f( 3, s ); } // info
f.V = function( s ) { f( 4, s ); } // verbose
f.D = function( s ) { f( 5, s ); } // debug
return f;
}
const defLog = mkLog("")(3);
defLog.mkLog = mkLog;
M.log5 = defLog;
M.L = defLog;
})();
/**
Make all the sleepless functions/objects into globals
(if you feel you must, and I often do)
*/
M.globalize = function() {
for( const k in M ) {
globalThis[ k ] = M[ k ];
}
return M;
};
/**
Fetch the contents of a file.
In the browser this is done by with and HTTP GET request to the server.
In Node, fs.readFile()/readfileSync() is used.
@arg {string}
@arg {string}
@arg {Function}
@returns {string} - only if using Node, and only if no callback is provided
*/
function get_file( path, enc, cb ) {
if( isBrowser ) {
let x = new XMLHttpRequest();
x.onload = function() { cb(x.responseText, x); };
x.open("GET", url);
x.send();
} else {
const fs = require("fs");
if(!cb) {
return fs.readFileSync( path, enc );
}
fs.readFile( path, enc, cb );
}
}
/**
Return ASCII sha1 string for a string
@arg {string}
@returns {string}
@see {@link sha256}
*/
function sha1(s) {
throwIf( isBrowser, "sha1() is not yet supported in browser." );
if( isBrowser ) {
// ...
} else {
const crypto = require("crypto");
var h = crypto.createHash("sha1");
h.update(s);
return h.digest("hex");
}
};
/**
Return ASCII sha256 string for a string
@arg {string}
@returns {string}
@see {@link sha1}
*/
function sha256(s) {
throwIf( isBrowser, "sha256() is not yet supported in browser." );
if( isBrowser ) {
// ...
} else {
const crypto = require("crypto");
var h = crypto.createHash("sha256");
h.update(s);
return h.digest("hex");
}
}
/**
Get fs stat object.
If cb provided, do it asyncrhonously and call cb
NOTE: This is non-browser only function
@returns {object} - fs.Stats object or null if error (ENOENT)
*/
function file_info( path, cb ) {
throwIf( isBrowser, "file_info() is not yet supported in browser." );
let st = null;
if( ! cb ) {
// do synchronously
st = fs.statSync( path, { throwIfNoEntry: false } );
return st;
}
// do async
fs.stat( path, ( error, st ) => {
if( error )
cb( null );
else
cb( st );
} );
}
/**
Returns true if path is a file.
NOTE: This is non-browser only function
@returns {boolean}
*/
function is_file( path, cb ) {
if( ! cb ) {
let st = M.file_info( path );
return st ? st.isFile() : false;
}
M.file_info( path, st => {
cb( st ? st.isFile() : false );
} );
};
/**
Returns true if path is a directory.
NOTE: This is non-browser only function
@returns {boolean}
*/
function is_dir( path, cb ) {
if( ! cb ) {
let st = M.file_info( path );
return st ? st.isDirectory() : false;
}
M.file_info( path, st => {
cb( st ? st.isDirectory() : false );
} );
};
/**
Returns a pretty randomish sha1 hash.
NOTE: This is non-browser only function in that it calls sha1()
@returns {string}
@see {@link sha1}
*/
function rand_hash() {
throwIf( isBrowser, "rand_hash() is not yet supported in browser." );
return M.sha1( "" + ( Date.now() + Math.random() ) );
};
/**
More convenient version of localStorage.
*/
LS = {
// XXX Add ttl feature
get: function( k ) {
throwIf( ! isBrowser, "LS.get() is not yet supported in non-browser contexts." );
try {
return M.j2o( localStorage.getItem( k ) );
} catch( e ) { }
return null;
},
set: function( k, v ) {
throwIf( ! isBrowser, "LS.set() is not yet supported in non-browser contexts." );
try {
return localStorage.setItem( k, M.o2j( v ) );
} catch( e ) { }
return null;
},
clear: function() {
throwIf( ! isBrowser, "LS.clear() is not yet supported in non-browser contexts." );
return localStorage.clear();
}
};
if( isBrowser ) {
/**
Returns an object constructed from the current page's query args,
or, if an argument is provided, just a single value for key.
@arg {string} - key to return
@returns {string}
@example
query_data() // { page: "home", foo: "bar" }
query_data( "page" ) // "home"
*/
function query_data( key ) {
var o = {};
var s = document.location.search;
if(s) {
var kv = s.substr(1).split("&")
for(var i = 0; i < kv.length; i++) {
var aa = kv[i].split("=");
o[aa[0]] = decodeURIComponent(aa[1]);
}
}
if( key !== undefined && typeof key === "string" )
return o[ key ];
return o
};
/**
Convert HTMLCollection to normal array
*/
HTMLCollection.prototype.toArray = function toArray() {
let arr = [];
for(let i = 0; i < this.length; i++) {
arr.push( this[ i ]);
}
return arr;
};
/**
Convert NodeList to normal array
*/
NodeList.prototype.toArray = HTMLCollection.prototype.toArray;
/**
Add a class to an element
*/
HTMLElement.prototype.addClass = function(c) {
let cl = this.classList;
if( ! cl.contains( c ) )
cl.add( c );
return this;
};
/**
*/
HTMLElement.prototype.hasClass = function(c) {
return this.classList.contains( c );
};
/**
Remove a class from an element
*/
HTMLElement.prototype.remClass = function(c) {
let cl = this.classList;
if( cl.contains( c ) )
cl.remove( c );
return this;
};
/**
Remember original display, then set display to 'none'
*/
HTMLElement.prototype.hide = function() {
if( this.style._orig_display === undefined )
this.style._orig_display = this.style.display;
this.style.display = "none";
return this;
};
/**
If original display was remembered, set display to that
Note unhide() is not exactly the same as show()
*/
HTMLElement.prototype.unhide = function() {
if( this.style._orig_display !== undefined )
this.style.display = this.style._orig_display;
return this;
};
/**
Remember original display, then set display to 'inherit'
*/
HTMLElement.prototype.show = function() {
if( this.style._orig_display === undefined )
this.style._orig_display = this.style.display;
this.style.display = "inherit";
return this;
}
/**
If original display was remembered, set display to that
Note unshow() is not exactly the same as hide()
*/
HTMLElement.prototype.unshow = HTMLElement.prototype.unhide;
/**
Find all child elements matching query selector
*/
HTMLElement.prototype.findAll = function( qs ) {
return this.querySelectorAll( qs ).toArray();
}
/**
Find first child element matching query selector
*/
HTMLElement.prototype.find = function( qs ) {
return this.findAll( qs )[ 0 ];
}
/**
*/
HTMLElement.prototype.findAllNamed = function( name ) {
return this.find( "[name="+name+"]" );
}
/**
*/
HTMLElement.prototype.findNamed = function( name ) {
return this.findAllNamed( name )[ 0 ];
}
/**
Get (or set if v is provided) an attribute's value
*/
HTMLElement.prototype.attr = function(a, v) {
if(v !== undefined) {
this.setAttribute(a, v);
return this;
}
else {
return this.getAttribute(a);
}
};
/**
Get (or set if v is provided) an element's value
*/
HTMLElement.prototype.val = function(v, chg = false) {
if(v !== undefined) {
this.value = v;
if( chg ) {
// fire a change event
const evt = new Event( "change" );
this.dispatchEvent( evt );
}
return this;
}
else {
return (this.value || "").trim();
}
};
/**
Fire a fake 'change' event on an element
*/
HTMLElement.prototype.change = function() {
const evt = new Event( "change" );
this.dispatchEvent( evt );
return this;
};
/**
Get (or set if h is provided) an element's innerHTML
@arg {string}
*/
HTMLElement.prototype.html = function(h) {
if(h !== undefined) {
this.innerHTML = h;
return this;
}
else {
return this.innerHTML;
}
};
/**
Injects data values into a single DOM element
*/
HTMLElement.prototype.inject = function( data ) {
let e = this;
// Inject into the body of the element
e.innerHTML = e.innerHTML.substitute( data );
// Inject into the attributes of the actual tag of the element.
let attrs = e.attributes;
for( let i = 0 ; i < attrs.length ; i++ ) {
let attr = attrs[ i ];
let val = attr.value;
if( val ) {
if( typeof val === "string" ) {
if( val.match( /__/ ) ) {
attr.value = val.substitute( data );
}
}
}
}
return e;
}
/**
handy thing to grab the data out of a form
*/
HTMLFormElement.prototype.getData = function() {
const types = "input select textarea".toUpperCase().split( " " );
let data = {};
for( let i = 0 ; i < this.elements.length ; i++ ) {
const e = this.elements[ i ];
if( types.includes( e.tagName ) ) {
data[ e.name ] = e.value;
}
}
return data;
};
/**
Takes an object, copies values into matching named form fields,
then sets onchange handlers that update the object values.
*/
HTMLFormElement.prototype.setData = function( d, change_cb ) {
for( let e of this.elements ) {
let k = e.name;
if( d[ k ] !== undefined ) {
let v = d[ k ];
if( e.type == "checkbox" )
e.checked = !! v;
else
e.value = v;
e.onchange = evt => {
let v = e.value;
if( e.type == "checkbox" )
v = !! e.checked;
d[ k ] = v;
if( change_cb )
change_cb( evt );
};
}
}
};
// ---------------------------------------
// The world renowned rplc8()!
// ---------------------------------------
(function() {
// Replaces instances of "__key__" in string s,
// with the values from corresponding key in data.
let substitute = function( s, data ) {
return s.substitute( data );
}
// Injects data values into a single DOM element
let inject = function( e, data ) {
return e.inject( data );
}
// The main function
M.rplc8 = function( elem, data, cb ) {
// If elem isn't a DOM element, then it has to be query selector string
if( ! ( elem instanceof HTMLElement ) ) {
if( typeof elem !== "string" ) {
throw new Error( "rplc8: invalid selector string" );
}
let coll = document.querySelectorAll( elem );
if( coll.length !== 1 ) {
throw new Error( "rplc8: selector \""+elem+"\" matches "+coll.length+" elements" );
}
elem = coll[ 0 ];
}
let sib = elem.nextSibling; // Might be null.
let mom = elem.parentNode; // Almost certainly not null.
let clones = [];
mom.removeChild( elem ); // Take template out of the DOM.
let validate_data = function( data ) {
// Ensure that data is an array or object
if( ! ( data instanceof Array ) ) {
// If it's a single object, put it into an array.
if( typeof data === "object" ) {
data = [ data ];
}
else {
data = [];
//throw new Error( "rplc8: Replication is neither array nor object." );
}
}
// Ensure that the first element in the array is an object.
if( data.length > 0 && typeof data[ 0 ] !== "object" ) {
throw new Error( "rplc8: Replication data array does not contain objects." );
}
return data;
}
let obj = { };
let splice = function( index, remove_count, new_data, cb ) {
if( index < 0 ) {
index = clones.length + index;
}
if( index > clones.length) {
index = clones.length;
}
let sib = clones[ index ] || null;
if( index < clones.length ) {
// remove the old clones
let n = 0;
while( n < remove_count && index < clones.length ) {
let clone = clones.splice( index, 1 )[ 0 ];
sib = clone.nextSibling;
mom.removeChild( clone );
n += 1;
}
}
// insert new clones if data provided
if( new_data ) {
data = validate_data( new_data );
let n = 0
while( n < data.length ) {
let d = data[ n ]; // Get data object from array.
let clone = elem.cloneNode( true ); // Clone template element and
inject( clone, d ); // inject the data.
mom.insertBefore( clone, sib ); // Insert it into the DOM
let i = index + n;
clones.splice( i, 0, clone ); // insert clone into array
if( cb ) { // If call back function provided,
// then call it with a refreshing function
cb( clone, d, i, function( new_data, cb ) {
splice( i, 1, new_data, cb );
});
}
n += 1;
}
}
return obj;
}
let append = function( data, cb ) {
return splice( clones.length, 0, data, cb );
}
let prepend = function( data, cb ) {
return splice( 0, 0, data, cb );
}
let update = function( data, cb ) {
return splice( 0, clones.length, data, cb );
}
let clear = function( index, count ) {
return splice( index || 0, count || clones.length );
}
update( data, cb );
obj.splice = splice;
obj.append = append;
obj.prepend = prepend;
obj.update = update;
obj.clear = clear;
return obj;
};
M.rplc8.substitute = substitute;
M.rplc8.inject = inject;
})();
// Lets you navigate through pseudo-pages within an actual page
// without any actual document fetching from the server.
// XXX deprecate in favor of navigate
M.Nav = function(data, new_show) {
if(typeof data === "function") {
new_show = data;
data = null;
}
if(!data) {
// no data object passed in use current query data
data = {};
var a = document.location.search.split(/[?&]/);
a.shift();
a.forEach(function(kv) {
var p = kv.split("=");
data[p[0]] = (p.length > 1) ? decodeURIComponent(p[1]) : "";
})
}
var state = { pageYOffset: 0, data: data };
// build URL + query-string from current path and contents of 'data'
var qs = "";
for(var k in data) {
qs += (qs ? "&" : "?") + k + "=" + encodeURIComponent(data[k]);
}
var url = document.location.pathname + qs;
// if browser doesn't support pushstate, just redirect to the url
if(history.pushState === undefined) {
document.location = url;
return;
}
if(!Nav.current_show) {
// 1st time Nav() has been called
// set current show func to a simple default
Nav.current_show = function(data) {
if(data["page"] !== undefined) {
// hide all elements with class "page" by setting css display to "none"
var pages = document.getElementsByClassName('page')
for(var i = 0; i < pages.length; i++ ) {
pages[ i ].style.display = "none";
}
// jump to top of document
document.body.scrollIntoView();
// show the new page
var p = document.getElementById( "page_"+data.page ).style.display = "inherit";
}
}
if(history.replaceState !== undefined) {
// set state for the current/initial location
history.replaceState(state, "", url);
// wire in the pop handler
window.onpopstate = function(evt) {
if(evt.state) {
var data = evt.state;
Nav.current_show(evt.state.data);
}
}
}
}
else {
// this is 2nd or later call to Nav()
state.pageYOffset = window.pageYOffset;
history.pushState(state, "", url);
}
// if new show func supplied, start using that one
if(new_show) {
Nav.current_show = new_show;
}
Nav.current_show(data);
};
// Lets you navigate through pseudo-pages within an actual page
// without any actual document fetching from the server.
// navigate( data_object )
// navigate( data_object, show_function )
// navigate( show_function )
M.navigate = function( data, new_show ) {
if( typeof data === "function" ) {
new_show = data;
data = null;
}
if( ! data ) {
// no data object passed in; use current query data
//data = {};
//const a = document.location.search.split( /[?&]/ );
//a.shift();
//a.forEach( function( kv ) {
// var p = kv.split( "=" );
// data[ p[ 0 ] ] = ( p.length > 1 ) ? decodeURIComponent( p[ 1 ] ) : "";
//})
data = query_data();
}
var state = { pageYOffset: 0, data };
// build URL + query-string from current path and contents of 'data'
let qs = "";
for( let k in data ) {
qs += (qs ? "&" : "?") + k + "=" + encodeURIComponent(data[k]);
}
const url = document.location.pathname + qs;
// if browser doesn't support pushstate, just redirect to the url
if( history.pushState === undefined ) {
document.location = url;
return;
}
M.Nav2.default_show = function( data ) {
if( data[ "page" ] !== undefined ) {
// hide all <page> elements by setting css display to "none"
const pages = M.QS( "page" );
for( let p of pages ) {
if( p._nav === undefined ) {
p._nav = {};
p._nav.orig_display = p.style.display;
}
p.style.display = "none";
}
// jump to top of document
document.body.scrollIntoView();
// show the new page
const pg = data.page;
const el = QS1( "page[name=" + pg + "]" );
if( el ) {
el.style.display = el._nav.orig_display;
} else {
throw new Error( "Nav2: No page with name " + pg );
}
}
};
if( ! Nav2.current_show ) {
// 1st time Nav2() has been called
// Set up the built-in default show function
Nav2.current_show = Nav2.default_show;
if( history.replaceState !== undefined ) {
// set state for the current/initial location
history.replaceState( state, "", url );
// wire in the pop handler
window.onpopstate = function( evt ) {
if( evt.state ) {
const data = evt.state;
Nav2.current_show( evt.state.data );
}
}
}
} else {
// this is 2nd or later call to Nav2()
state.pageYOffset = window.pageYOffset;
history.pushState( state, "", url );
}
// if new show func supplied, start using that one
if( new_show ) {
Nav2.current_show = new_show;
}
Nav2.current_show( data );
};
M.Nav2 = M.navigate // XXX Deprecate in favor of navigate()
// Ties a Javascript object to some user interface elements in the browser DOM.
// If anything changes in the data object then mod_cb is called.
// XXX Not great. Don't recommend using this
// XXX Deprecate
M.MXU = function( base, data, mod_cb ) {
const form_types = "input select textarea".toUpperCase().split( " " );
let named_element = function( name ) {
return base.querySelector( "[name="+name+"]" );
}
let proxy = new Proxy( data, {
get: function( tgt, prop ) {
let e = named_element( prop );
if( e ) {
let v;
if( form_types.includes( e.tagName ) ) {
if( e.type == "checkbox" ) {
v = e.checked;
}
else {
v = e.value;
}
}
else {
v = e.innerHTML;
}
tgt[ prop ] = v;
}
return tgt[ prop ];
},
set: function( tgt, prop, v ) {
tgt[ prop ] = v;
let e = named_element( prop );
if( e ) {
if( form_types.includes( e.tagName ) ) {
if( e.type == "checkbox" ) {
e.checked = !! v;
}
else {
e.value = v;
}
}
else {
e.innerHTML = v;
}
}
if( mod_cb ) {
mod_cb( prop, v );
}
},
});
for(let key in data ) {
proxy[ key ] = data[ key ];
let e = named_element( key );
if( e && form_types.includes( e.tagName ) ) {
e.onchange = evt => {
proxy[ key ] = e.value;
}
}
}
return proxy;
};
// FDrop
/*
var dt = elem("droptarget");
FDrop.attach(dt, function(files, evt) {
files = files;
var f = files[0];
FDrop.mk_data_url(f, function(u) {
dt.innerHTML = "<img height=100 src='"+u+"'><br>name="+f.name+"<br>type="+f.type+"<br>size="+f.size+"<p>data url:<p>"+u;
});
});
*/
(function() {
let attach = function(element, cb) {
let style = element.style
let old_opacity = style.opacity
element.ondragenter = function(evt) {
style.opacity = "0.5";
}
element.ondragleave = function(evt) {
if(evt.target === element)
style.opacity = old_opacity;
}
// for drag/drop to work, element MUST have ondragover AND ondrop defined
element.ondragover = function(evt) {
evt.preventDefault(); // required: ondragover MUST call this.
}
element.ondrop = function(evt) {
evt.preventDefault(); // required
style.opacity = old_opacity; // because ondragleave not called on drop (chrome at least)
let files = evt.dataTransfer.files
cb(files, evt);
}
};
let mk_data_url = function(f, cb) {
let reader = new FileReader();
reader.onload = function() {
let data = reader.result;
cb(data);
};
reader.readAsDataURL(f);
};
let put_file = function( file, url, okay, fail ) {
let xhr = new XMLHttpRequest();
xhr.onload = function() {
okay( xhr );
}
xhr.upload.addEventListener("error", fail );
xhr.open( "PUT", url, true );
xhr.setRequestHeader( "Content-Type", file.type );
xhr.send( file );
};
M.FDrop = {
attach,
mk_data_url,
put_file,
};
})();
// Load an image asyncrhonously, scale it to a specific width/height, convert
// the image to a "data:" url, and return it via callback.
M.scale_data_image = function( image_data_url, new_width, new_height, cb ) {
let img = new Image();
img.onload = function() {
let cnv = document.createElement( "canvas" );
cnv.width = new_width;
cnv.height = new_height;
var ctx = cnv.getContext("2d");
ctx.drawImage(img, 0, 0, new_width, new_height);
let new_data_url = cnv.toDataURL( "image/jpeg", 0.5 );
cb( new_data_url );
}
img.src = image_data_url;
};
}