/* global btoa,JsDiff */
/* lively.vm dontTransform: ["btoa"] */
// String utility methods for printing, parsing, and converting strings.
import { inspect as inspectObject } from './object.js';
import { min, last } from './array.js';
import articles from './lib/articles.js';
const features = {
repeat: !!String.prototype.repeat,
includes: !!String.prototype.includes,
startsWith: !!String.prototype.startsWith,
endsWith: !!String.prototype.endsWith
};
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// printing and formatting strings
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function format () {
// String+ -> String
// Takes a variable number of arguments. The first argument is the format
// string. Placeholders in the format string are marked with `"%s"`.
// Example:
// lively.lang.string.format("Hello %s!", "Lively User"); // => "Hello Lively User!"
return formatFromArray(Array.prototype.slice.call(arguments));
}
function formatFromArray (objects) {
const self = objects.shift();
if (!self) { console.log('Error in Strings>>formatFromArray, first arg is undefined'); }
function appendText (object, string) { return '' + object; }
function appendInteger (value, string) { return value.toString(); }
function appendFloat (value, string, precision) {
if (precision > -1) return `${Number.parseFloat(value.toFixed(precision))}`;
else return value.toString();
}
function appendObject (value, string) { return inspectObject(value); }
const appenderMap = { s: appendText, d: appendInteger, i: appendInteger, f: appendFloat, o: appendObject };
const reg = /((^%|[^\\]%)(\d+)?(\.)([a-zA-Z]))|((^%|[^\\]%)([a-zA-Z]))/;
function parseFormat (fmt) {
const oldFmt = fmt;
const parts = [];
for (let m = reg.exec(fmt); m; m = reg.exec(fmt)) {
const type = m[8] || m[5];
const appender = type in appenderMap ? appenderMap[type] : appendObject;
const precision = m[3] ? parseInt(m[3]) : (m[4] == '.' ? -1 : 0);
parts.push(fmt.substr(0, m[0][0] == '%' ? m.index : m.index + 1));
parts.push({ appender: appender, precision: precision });
fmt = fmt.substr(m.index + m[0].length);
}
if (fmt) { parts.push(fmt.toString()); }
return parts;
}
const parts = parseFormat(self);
let str = '';
let objIndex = 0;
for (let i = 0; i < parts.length; ++i) {
const part = parts[i];
if (part && typeof (part) === 'object') {
const object = objects[objIndex++];
str += (part.appender || appendText)(object, str, part.precision);
} else {
str += appendText(part, str);
}
}
return str;
}
function indent (str, indentString, depth) {
// String -> String -> String? -> String
// Example:
// string.indent("Hello", " ", 2) // => " Hello"
if (!depth || depth <= 0) return str;
let indent = ''; while (depth > 0) { depth--; indent += indentString; }
return lines(str).map(function (line) { return indent + line; }).join('\n');
}
function minIndent (str, indentString) {
// Find out what the minum indentation of the text in str is
// Example:
// minIndent(" Hello", " ") // => 2
if (!indentString) indentString = ' ';
const indentRe = new RegExp('^(' + indentString + ')*', 'gm');
return min(str.match(indentRe).map(ea => Math.floor(ea.length / indentString.length)));
}
function changeIndent (str, indentString, depth) {
// Add or remove indent from lines in str to match depth
// Example:
// string.changeIndent(" Hello", " ", 1) // => " Hello"
if (!indentString) indentString = ' ';
if (!depth) depth = 0;
const existingIndent = minIndent(str, indentString);
if (existingIndent === depth) return str;
if (existingIndent < depth) return indent(str, indentString, depth - existingIndent);
const prefixToRemove = indentString.repeat(existingIndent - depth);
return lines(str)
.map(function (line) { return line.slice(prefixToRemove.length); })
.join('\n');
}
function quote (str) {
// Example:
// string.print("fo\"o") // => "\"fo\\\"o\""
return '"' + str.replace(/"/g, '\\"') + '"';
}
function articlize (str) {
return articles.articlize(str);
}
function findArticle (str) {
return articles.find(str);
}
function print (obj) {
// Prints Arrays and escapes quotations. See `obj.inspect` for how to
// completely print / inspect JavaScript data strcutures
// Example:
// string.print([[1,2,3], "string", {foo: 23}])
// // => [[1,2,3],"string",[object Object]]
if (obj && Array.isArray(obj)) return '[' + obj.map(print) + ']';
if (typeof obj !== 'string') return String(obj);
let result = String(obj);
result = result.replace(/\n/g, '\\n\\\n');
result = result.replace(/(")/g, '\\$1');
result = '\"' + result + '\"';
return result;
}
function printNested (list, depth) {
// Example:
// string.printNested([1,2,[3,4,5]]) // => "1\n2\n 3\n 4\n 5\n"
depth = depth || 0;
return list.reduce(function (s, ea) {
return s += Array.isArray(ea)
? printNested(ea, depth + 1)
: indent(ea + '\n', ' ', depth);
}, '');
}
function pad (string, n, left) {
// Examples:
// pad("Foo", 2) // => "Foo "
// pad("Foo", 2, true) // => " Foo"
return left ? ' '.repeat(n) + string : string + ' '.repeat(n);
}
function printTable (tableArray, options) {
// Array -> Object? -> String
// Takes a 2D Array and prints a table string. Kind of the reverse
// operation to `tableize`
// Example:
// string.printTable([["aaa", "b", "c"], ["d", "e","f"]])
// // =>
// // aaa b c
// // d e f
const columnWidths = [];
const separator = (options && options.separator) || ' ';
const alignLeftAll = !options || !options.align || options.align === 'left';
const alignRightAll = options && options.align === 'right';
function alignRight (columnIndex) {
if (alignLeftAll) return false;
if (alignRightAll) return true;
return options &&
Array.isArray(options.align) &&
options.align[columnIndex] === 'right';
}
tableArray.forEach(function (row) {
row.forEach(function (cellVal, i) {
if (columnWidths[i] === undefined) columnWidths[i] = 0;
columnWidths[i] = Math.max(columnWidths[i], String(cellVal).length);
});
});
return tableArray.map(function (row) {
return row.map(function (cellVal, i) {
const cellString = String(cellVal);
return pad(cellString,
columnWidths[i] - cellString.length,
alignRight(i));
}).join(separator);
}).join('\n');
}
function printTree (rootNode, nodePrinter, childGetter, indent) {
// Object -> Function -> Function -> Number? -> String
// A generic function to print a tree representation from a nested data structure.
// Receives three arguments:
// - `rootNode` an object representing the root node of the tree
// - `nodePrinter` is a function that gets a tree node and should return stringified version of it
// - `childGetter` is a function that gets a tree node and should return a list of child nodes
// Example:
// var root = {name: "a", subs: [{name: "b", subs: [{name: "c"}]}, {name: "d"}]};
// string.printTree(root, function(n) { return n.name; }, function(n) { return n.subs; });
// // =>
// // a
// // |-b
// // | \-c
// // \-d
const nodeList = [];
indent = indent || ' ';
iterator(0, 0, rootNode);
return nodeList.join('\n');
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function iterator (depth, index, node) {
// ignore-in-doc
// 1. Create stringified representation of node
nodeList[index] = (indent.repeat(depth)) + nodePrinter(node, depth);
const children = childGetter(node, depth);
let childIndex = index + 1;
if (!children || !children.length) return childIndex;
// 2. If there are children then assemble those linear inside nodeList
// The childIndex is the pointer of the current items of childList into
// nodeList.
let lastIndex = childIndex;
const lastI = children.length - 1;
children.forEach(function (ea, i) {
childIndex = iterator(depth + 1, childIndex, ea);
// 3. When we have printed the recursive version then augment the
// printed version of the direct children with horizontal slashes
// directly in front of the represented representation
const isLast = lastI === i;
const cs = nodeList[lastIndex].split('');
const fromSlash = (depth * indent.length) + 1;
const toSlash = (depth * indent.length) + indent.length;
for (var i = fromSlash; i < toSlash; i++) cs[i] = '-';
if (isLast) cs[depth * indent.length] = '\\';
nodeList[lastIndex] = cs.join('');
// 4. For all children (direct and indirect) except for the
// last one (itself and all its children) add vertical bars in
// front of each at position of the current nodes depth. This
// makes is much easier to see which child node belongs to which
// parent
if (!isLast) {
nodeList.slice(lastIndex, childIndex).forEach(function (ea, i) {
const cs2 = ea.split('');
cs2[depth * indent.length] = '|';
nodeList[lastIndex + i] = cs2.join('');
});
}
lastIndex = childIndex;
});
return childIndex;
}
}
function toArray (s) {
// Example:
// string.toArray("fooo") // => ["f","o","o","o"]
return s.split('');
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// parsing strings into other entities
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function lines (str) {
// Example: string.lines("foo\nbar\n\rbaz") // => ["foo","bar","baz"]
return str.split(/\n\r?/);
}
function paragraphs (string, options) {
// Examples:
// var text = "Hello, this is a pretty long sentence\nthat even includes new lines."
// + "\n\n\nThis is a sentence in a new paragraph.";
// string.paragraphs(text) // => [
// // "Hello, this is a pretty long sentence\nthat even includes new lines.",
// // "This is a sentence in a new paragraph."]
// string.paragraphs(text, {keepEmptyLines: true}) // => [
// // "Hello, this is a pretty long sentence\n that even includes new lines.",
// // "\n ",
// // "This is a sentence in a new paragraph."]
const sep = options ? options.sep : '\n\n';
if (!options || !options.keepEmptyLines) return string.split(new RegExp(sep + '+'));
function isWhiteSpace (s) { return (/^\s*$/).test(s); }
return string.split('\n').concat('').reduce(function (parasAndLast, line) {
const paras = parasAndLast[0]; let last = parasAndLast[1];
if (isWhiteSpace(last) === isWhiteSpace(line)) {
last += '\n' + line;
} else {
last.length && paras.push(last); last = line;
}
return [paras, last];
}, [[], ''])[0];
}
function nonEmptyLines (str) {
// Example: string.nonEmptyLines("foo\n\nbar\n") // => ["foo","bar"]
return lines(str).compact();
}
function tokens (str, regex) {
// Example:
// string.tokens(' a b c') => ['a', 'b', 'c']
return str.split(regex || /\s+/).filter(function (tok) {
return !(/^\s*$/).test(tok);
});
}
function tableize (s, options) {
// String -> Object? -> Array
// Takes a String representing a "table" and parses it into a 2D-Array (as
// accepted by the `collection.Grid` methods or `string.printTable`)
// ```js
// options = {
// convertTypes: BOOLEAN, // automatically convert to Numbers, Dates, ...?
// cellSplitter: REGEXP // how to recognize "cells", by default just spaces
// }
// ```
// Examples:
// string.tableize('a b c\nd e f')
// // => [["a","b","c"],["d","e","f"]]
// // can also parse csv like
// var csv = '"Symbol","Name","LastSale",\n'
// + '"FLWS","1-800 FLOWERS.COM, Inc.","5.65",\n'
// + '"FCTY","1st Century Bancshares, Inc","5.65",'
// string.tableize(csv, {cellSplitter: /^\s*"|","|",?\s*$/g})
// // => [["Symbol","Name","LastSale"],
// // ["FLWS","1-800 FLOWERS.COM, Inc.",5.65],
// // ["FCTY","1st Century Bancshares, Inc",5.65]]
options = options || {};
const splitter = options.cellSplitter || /\s+/;
const emptyStringRe = /^\s*$/;
const convertTypes = options.hasOwnProperty('convertTypes') ? !!options.convertTypes : true;
const _lines = lines(s); const table = [];
for (let i = 0; i < _lines.length; i++) {
let _tokens = tokens(_lines[i], splitter);
if (convertTypes) {
_tokens = _tokens.map(function (tok) {
if (tok.match(emptyStringRe)) return tok;
const num = Number(tok);
if (!isNaN(num)) return num;
const date = new Date(tok);
if (!isNaN(+date)) return date;
return tok.trim();
});
}
if (_tokens.length > 0) table.push(_tokens);
}
return table;
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// (un)escape / encoding / decoding
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function unescapeCharacterEntities (s) {
// Converts [character entities](http://dev.w3.org/html5/html-author/charref)
// into utf-8 strings
// Example:
// string.unescapeCharacterEntities("foo && bar") // => "foo && bar"
if (typeof document === 'undefined') throw new Error('Cannot unescapeCharacterEntities');
const div = document.createElement('div');
div.innerHTML = s;
return div.textContent;
}
function toQueryParams (s, separator) {
// Example:
// string.toQueryParams("http://example.com?foo=23&bar=test")
// // => {bar: "test", foo: "23"}
const match = s.trim().match(/([^?#]*)(#.*)?$/);
if (!match) return {};
const hash = match[1].split(separator || '&').inject({}, function (hash, pair) {
if ((pair = pair.split('='))[0]) {
const key = decodeURIComponent(pair.shift());
let value = pair.length > 1 ? pair.join('=') : pair[0];
if (value != undefined) value = decodeURIComponent(value);
if (key in hash) {
if (!Array.isArray(hash[key])) hash[key] = [hash[key]];
hash[key].push(value);
} else hash[key] = value;
}
return hash;
});
return hash;
}
function base64EncodeUnicode (str) {
// [https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem]()
// Base64 encode a unicode string
// First we use encodeURIComponent to get percent-encoded UTF-8,
// then we convert the percent encodings into raw bytes which
// can be fed into btoa.
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
function toSolidBytes (match, p1) {
return String.fromCharCode('0x' + p1);
}
));
}
function base64DecodeUnicode (str) {
// [https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem]()
// Decode a base64 encoded string to a unicode string
// Going backwards: from bytestream, to percent-encoding, to original string.
return decodeURIComponent(atob(str).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}
// -=-=-=-=-=-=-=-=-=-=-=-=-
// file system path support
// -=-=-=-=-=-=-=-=-=-=-=-=-
const pathDotRe = /\/\.\//g;
const pathDoubleDotRe = /\/[^\/]+\/\.\./;
const pathDoubleSlashRe = /(^|[^:])[\/]+/g;
const urlStartRe = /^[a-z0-9-_\.]+:\/\//;
const slashEndRe = /\/+$/;
function normalizePath (pathString) {
const urlStartMatch = pathString.match(urlStartRe);
const urlStart = urlStartMatch ? urlStartMatch[0] : null;
let result = urlStart ? pathString.slice(urlStart.length) : pathString;
// /foo/../bar --> /bar
do {
pathString = result;
result = pathString.replace(pathDoubleDotRe, '');
} while (result != pathString);
// foo//bar --> foo/bar
result = result.replace(pathDoubleSlashRe, '$1/');
// foo/./bar --> foo/bar
result = result.replace(pathDotRe, '/');
if (urlStart) result = urlStart + result;
return result;
}
/**
* Joins the strings passed as paramters together so that ea string is
* connected via a single "/".
* @example
* string.joinPath("foo", "bar") // => "foo/bar";
* @params { string[] } paths - The set of paths to be joined.
* @returns { string } The joined path.
*/
function joinPath (/* paths */) {
return normalizePath(
Array.prototype.slice.call(arguments).reduce((path, ea) =>
typeof ea === 'string'
? path.replace(/\/*$/, '') + '/' + ea.replace(/^\/*/, '')
: path));
}
/**
* Given a path such as "path/to/file" returns a folderized version
* such as: "path/to/file/".
* @param {string} pathString - The path to transform.
* @returns { string } The transformed path;
*/
function ensureFolder (pathString) {
return pathString.replace(slashEndRe, '') + '/';
}
// -=-=-=-=-=-=-=-=-
// ids and hashing
// -=-=-=-=-=-=-=-=-
const newUUIDTemplate = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
const newUUIDRe = /[xy]/g;
const newUUIDReplacer = c => {
const r = Math.random() * 16 | 0;
const v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
};
function newUUID () {
// Example:
// newUUID() // => "3B3E74D0-85EA-45F2-901C-23ECF3EAB9FB"
return newUUIDTemplate.replace(newUUIDRe, newUUIDReplacer).toUpperCase();
}
function isUUID (str) {
if (str) str = str.toLowerCase();
return /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.test(str);
}
function createDataURI (content, mimeType) {
// String -> String -> String
// Takes some string representing content and a mime type.
// For a list of mime types see: [http://www.iana.org/assignments/media-types/media-types.xhtml]()
// More about data URIs: [https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs]()
// Example:
// window.open(string.createDataURI('<h1>test</h1>', 'text/html'));
mimeType = mimeType || 'text/plain';
return 'data:' + mimeType + ';base64,' + base64EncodeUnicode(content);
}
function hashCode (s) {
// [http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/]()
// Example: string.hashCode("foo") // => 101574
let hash = 0; const len = s.length;
if (len == 0) return hash;
for (let i = 0; i < len; i++) {
const c = s.charCodeAt(i);
hash = ((hash << 5) - hash) + c;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
function md5 (string) {
// © Joseph Myers [http://www.myersdaily.org/joseph/javascript/md5-text.html]()
// Example:
// string.md5("foo") // => "acbd18db4cc2f85cedef654fccc4a4d8"
/* ignore-in-doc
this function is much faster,
so if possible we use it. Some IEs
are the only ones I know of that
need the idiotic second function,
generated by an if clause. */
// var add32 = hex(md51("hello")) === "5d41402abc4b2a76b9719d911017c592" ?
// function add32(a, b) { return (a + b) & 0xFFFFFFFF; } :
const add32 = function add32 (x, y) {
const lsw = (x & 0xFFFF) + (y & 0xFFFF);
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
};
function cmn (q, a, b, x, s, t) {
a = add32(add32(a, q), add32(x, t));
return add32((a << s) | (a >>> (32 - s)), b);
}
function ff (a, b, c, d, x, s, t) {
return cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function gg (a, b, c, d, x, s, t) {
return cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function hh (a, b, c, d, x, s, t) {
return cmn(b ^ c ^ d, a, b, x, s, t);
}
function ii (a, b, c, d, x, s, t) {
return cmn(c ^ (b | (~d)), a, b, x, s, t);
}
function md5cycle (x, k) {
let a = x[0]; let b = x[1]; let c = x[2]; let d = x[3];
a = ff(a, b, c, d, k[0], 7, -680876936);
d = ff(d, a, b, c, k[1], 12, -389564586);
c = ff(c, d, a, b, k[2], 17, 606105819);
b = ff(b, c, d, a, k[3], 22, -1044525330);
a = ff(a, b, c, d, k[4], 7, -176418897);
d = ff(d, a, b, c, k[5], 12, 1200080426);
c = ff(c, d, a, b, k[6], 17, -1473231341);
b = ff(b, c, d, a, k[7], 22, -45705983);
a = ff(a, b, c, d, k[8], 7, 1770035416);
d = ff(d, a, b, c, k[9], 12, -1958414417);
c = ff(c, d, a, b, k[10], 17, -42063);
b = ff(b, c, d, a, k[11], 22, -1990404162);
a = ff(a, b, c, d, k[12], 7, 1804603682);
d = ff(d, a, b, c, k[13], 12, -40341101);
c = ff(c, d, a, b, k[14], 17, -1502002290);
b = ff(b, c, d, a, k[15], 22, 1236535329);
a = gg(a, b, c, d, k[1], 5, -165796510);
d = gg(d, a, b, c, k[6], 9, -1069501632);
c = gg(c, d, a, b, k[11], 14, 643717713);
b = gg(b, c, d, a, k[0], 20, -373897302);
a = gg(a, b, c, d, k[5], 5, -701558691);
d = gg(d, a, b, c, k[10], 9, 38016083);
c = gg(c, d, a, b, k[15], 14, -660478335);
b = gg(b, c, d, a, k[4], 20, -405537848);
a = gg(a, b, c, d, k[9], 5, 568446438);
d = gg(d, a, b, c, k[14], 9, -1019803690);
c = gg(c, d, a, b, k[3], 14, -187363961);
b = gg(b, c, d, a, k[8], 20, 1163531501);
a = gg(a, b, c, d, k[13], 5, -1444681467);
d = gg(d, a, b, c, k[2], 9, -51403784);
c = gg(c, d, a, b, k[7], 14, 1735328473);
b = gg(b, c, d, a, k[12], 20, -1926607734);
a = hh(a, b, c, d, k[5], 4, -378558);
d = hh(d, a, b, c, k[8], 11, -2022574463);
c = hh(c, d, a, b, k[11], 16, 1839030562);
b = hh(b, c, d, a, k[14], 23, -35309556);
a = hh(a, b, c, d, k[1], 4, -1530992060);
d = hh(d, a, b, c, k[4], 11, 1272893353);
c = hh(c, d, a, b, k[7], 16, -155497632);
b = hh(b, c, d, a, k[10], 23, -1094730640);
a = hh(a, b, c, d, k[13], 4, 681279174);
d = hh(d, a, b, c, k[0], 11, -358537222);
c = hh(c, d, a, b, k[3], 16, -722521979);
b = hh(b, c, d, a, k[6], 23, 76029189);
a = hh(a, b, c, d, k[9], 4, -640364487);
d = hh(d, a, b, c, k[12], 11, -421815835);
c = hh(c, d, a, b, k[15], 16, 530742520);
b = hh(b, c, d, a, k[2], 23, -995338651);
a = ii(a, b, c, d, k[0], 6, -198630844);
d = ii(d, a, b, c, k[7], 10, 1126891415);
c = ii(c, d, a, b, k[14], 15, -1416354905);
b = ii(b, c, d, a, k[5], 21, -57434055);
a = ii(a, b, c, d, k[12], 6, 1700485571);
d = ii(d, a, b, c, k[3], 10, -1894986606);
c = ii(c, d, a, b, k[10], 15, -1051523);
b = ii(b, c, d, a, k[1], 21, -2054922799);
a = ii(a, b, c, d, k[8], 6, 1873313359);
d = ii(d, a, b, c, k[15], 10, -30611744);
c = ii(c, d, a, b, k[6], 15, -1560198380);
b = ii(b, c, d, a, k[13], 21, 1309151649);
a = ii(a, b, c, d, k[4], 6, -145523070);
d = ii(d, a, b, c, k[11], 10, -1120210379);
c = ii(c, d, a, b, k[2], 15, 718787259);
b = ii(b, c, d, a, k[9], 21, -343485551);
x[0] = add32(a, x[0]);
x[1] = add32(b, x[1]);
x[2] = add32(c, x[2]);
x[3] = add32(d, x[3]);
}
function md51 (s) {
const n = s.length;
const state = [1732584193, -271733879, -1732584194, 271733878]; let i;
for (i = 64; i <= n; i += 64) {
md5cycle(state, md5blk(s.substring(i - 64, i)));
}
s = s.substring(i - 64);
const tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; const sl = s.length;
for (i = 0; i < sl; i++) tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3);
tail[i >> 2] |= 0x80 << ((i % 4) << 3);
if (i > 55) {
md5cycle(state, tail);
i = 16;
while (i--) { tail[i] = 0; }
// for (i=0; i<16; i++) tail[i] = 0;
}
tail[14] = n * 8;
md5cycle(state, tail);
return state;
}
/* ignore-in-doc
* there needs to be support for Unicode here,
* unless we pretend that we can redefine the MD-5
* algorithm for multi-byte characters (perhaps
* by adding every four 16-bit characters and
* shortening the sum to 32 bits). Otherwise
* I suggest performing MD-5 as if every character
* was two bytes--e.g., 0040 0025 = @%--but then
* how will an ordinary MD-5 sum be matched?
* There is no way to standardize text to something
* like UTF-8 before transformation; speed cost is
* utterly prohibitive. The JavaScript standard
* itself needs to look at this: it should start
* providing access to strings as preformed UTF-8
* 8-bit unsigned value arrays.
*/
function md5blk (s) {
// ignore-in-doc
/* I figured global was faster. */
const md5blks = []; let i; /* Andy King said do it this way. */
for (i = 0; i < 64; i += 4) {
md5blks[i >> 2] = s.charCodeAt(i) +
(s.charCodeAt(i + 1) << 8) +
(s.charCodeAt(i + 2) << 16) +
(s.charCodeAt(i + 3) << 24);
}
return md5blks;
}
const hex_chr = '0123456789abcdef'.split('');
function rhex (n) {
let s = ''; let j = 0;
for (; j < 4; j++) s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F];
return s;
}
function hex (x) {
const l = x.length;
for (let i = 0; i < l; i++) x[i] = rhex(x[i]);
return x.join('');
}
return hex(md51(string));
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-
// matching strings / regexps
// -=-=-=-=-=-=-=-=-=-=-=-=-=-
function reMatches (string, re) {
// Different to the native `match` function this method returns an object
// with `start`, `end`, and `match` fields
// Example:
// string.reMatches("Hello World", /o/g)
// // => [{start: 4, end: 5, match: "o"},{start: 7, end: 8, match: "o"}]
const matches = [];
string.replace(re, function (match, idx) {
matches.push({ match: match, start: idx, end: idx + match.length });
});
return matches;
}
function stringMatch (s, patternString, options) {
// returns `{matched: true}` if success otherwise
// `{matched: false, error: EXPLANATION, pattern: STRING|RE, pos: NUMBER}`
// Example:
// string.stringMatch("foo 123 bar", "foo __/[0-9]+/__ bar") // => {matched: true}
// string.stringMatch("foo aaa bar", "foo __/[0-9]+/__ bar")
// // => {
// // error: "foo <--UNMATCHED-->aaa bar",
// // matched: false,
// // pattern: /[0-9]+/,
// // pos: 4
// // }
options = options || {};
if (options.normalizeWhiteSpace) s = s.replace(/\s+/g, ' ');
if (options.ignoreIndent) {
s = s.replace(/^\s+/gm, '');
patternString = patternString.replace(/^\s+/gm, '');
}
return s == patternString
? { matched: true }
: embeddedReMatch(s, patternString);
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function splitInThree (string, start, end, startGap, endGap) {
// ignore-in-doc
// split string at start and end
// return (0, start), (start, end), (end, ...)
startGap = startGap || 0; endGap = endGap || 0;
return [string.slice(0, start),
string.slice(start + startGap, end - endGap),
string.slice(end)];
}
function matchStringForward (s, pattern) {
// ignore-in-doc
// try to match pattern at beginning of string. if matched, return
// result object with {
// match: STRING,
// REST: STRING -- remaining string after pattern was consumed
// }
if (pattern.constructor !== RegExp) {
const idx = s.indexOf(pattern);
if (idx === 0) return { match: pattern, rest: s.slice(pattern.length) };
// no match
for (let i = 0; i < pattern.length; i++) // figure out where we failed
{
if (pattern[i] != s[i]) { return { match: null, pos: i }; }
}
return { match: null };
}
const matches = reMatches(s, pattern);
// show(matches)
// show(string.slice(matches[0].end));
return (!matches || !matches.length || matches[0].start !== 0)
? { match: null }
: { match: matches[0].match, rest: s.slice(matches[0].end) };
}
function matchStringForwardWithAllPatterns (s, patterns) {
// ignore-in-doc
// like matchStringForward, just apply list of patterns
let pos = 0;
for (let i = 0; i < patterns.length; i++) {
const p = patterns[i];
const result = matchStringForward(s, p);
if (!result.match) return { matched: false, pos: pos + (result.pos || 0), pattern: p };
pos += result.match.length;
s = result.rest;
}
return s.length ? { matched: false, pos: pos } : { matched: true };
}
function splitIntoPatterns (matcher) {
const starts = reMatches(matcher, /__\//g);
const ends = reMatches(matcher, /\/__/g);
if (starts.length !== ends.length) {
throw new Error('pattern invalid: ' +
matcher +
' cannot be split into __/.../__ embedded RegExps' +
'\nstarts: ' + JSON.stringify(starts) +
'\nvs ends:\n' + JSON.stringify(ends));
}
let consumed = 0;
return starts.reduce(function (patterns, start, i) {
const end = ends[i];
const matcher = patterns.pop();
const splitted = splitInThree(
matcher,
start.start - consumed,
end.end - consumed,
3, 3);
if (splitted[0].length) {
patterns.push(splitted[0]);
consumed += splitted[0].length;
}
try {
if (splitted[1].length) {
patterns.push(new RegExp(splitted[1]));
consumed += splitted[1].length + 3 + 3;
}
} catch (e) {
throw new Error('Cannot create pattern re from: ' + inspectObject(splitted));
}
if (splitted[2].length) { patterns.push(splitted[2]); }
return patterns;
}, [matcher]);
}
function embeddedReMatch (s, patternString) {
// ignore-in-doc
// the main match func
const patterns = splitIntoPatterns(patternString);
const result = matchStringForwardWithAllPatterns(s, patterns);
if (result.matched) return result;
result.error = s.slice(0, result.pos) + '<--UNMATCHED-->' + s.slice(result.pos);
return result;
}
}
function peekRight (s, start, needle) {
// Finds the next occurence of `needle` (String or RegExp). Returns delta
// index.
// Example:
// peekRight("Hello World", 0, /o/g) // => 4
// peekRight("Hello World", 5, /o/) // => 2
s = s.slice(start);
if (typeof needle === 'string') {
const idx = s.indexOf(needle);
return idx === -1 ? null : idx + start;
} else if (needle.constructor === RegExp) {
const matches = reMatches(s, needle);
return matches[0] ? matches[0].start : null;
}
return null;
}
function peekLeft (s, start, needle) {
// Similar to `peekRight`
s = s.slice(0, start);
if (typeof needle === 'string') {
const idx = s.lastIndexOf(needle);
return idx === -1 ? null : idx;
} else if (needle.constructor === RegExp) {
const matches = reMatches(s, needle);
return last(matches) ? last(matches).start : null;
}
return null;
}
function lineIndexComputer (s) {
// String -> Function
// For converting character positions to line numbers.
// Returns a function accepting char positions. If the char pos is outside
// of the line ranges -1 is returned.
// Example:
// var idxComp = lineIndexComputer("Hello\nWorld\n\nfoo");
// idxComp(3) // => 0 (index 3 is "l")
// idxComp(6) // => 1 (index 6 is "W")
// idxComp(12) // => 2 (index 12 is "\n")
// ignore-in-doc
// line ranges: list of numbers, each line has two entries:
// i -> start of line, i+1 -> end of line
const _lineRanges = lineRanges(s);
// ignore-in-doc
// FIXME, this is O(n). Make cumputation more efficient, binary lookup?
return function (pos) {
for (let line = 0; line < _lineRanges.length; line++) {
const lineRange = _lineRanges[line];
if (pos >= lineRange[0] && pos < lineRange[1]) { return line; }
}
return -1;
};
}
function lineNumberToIndexesComputer (s) {
// String -> Function
// For converting line numbers to [startIndex, endIndex]
// Example:
// var idxComp = lineNumberToIndexesComputer("Hello\nWorld\n\nfoo");
// idxComp(1) // => [6,12]
return function (lineNo) { return lineRanges(s)[lineNo]; };
}
function lineRanges (s) {
let from = 0; let to = 0; const linesOfS = lines(s); const result = [];
for (let i = 0; i < linesOfS.length; i++) {
const line = linesOfS[i];
to = from + line.length + 1;
result.push([from, to]);
from = to;
}
return result;
}
function findLineWithIndexInLineRanges (lineRanges, idx) {
// given a list of `lineRanges` (produced by
// `livley.lang.string.lineRanges(string)`) like lineRanges = [[0, 12], [12, 33]]
// and an string index `idx` into `string`, find the line no (the index into
// `lineRanges`) that includes idx. The index intervals include start and exclude end:
// Example:
// findLineWithIndex2(lineRanges, 2); // => 0
// findLineWithIndex2(lineRanges, 12); // => 1
// findLineWithIndex2(lineRanges, 33); // => 1
// findLineWithIndex2(lineRanges, 34); // => -1
// findLineWithIndex2(lineRanges, -4); // => -1
const nRows = lineRanges.length;
if (nRows === 0) return -1;
// let currentRow = Math.floor(nRows/2), lastRow = nRows;
let startRow = 0; let endRow = nRows;
while (true) {
const middle = startRow + Math.floor((endRow - startRow) / 2);
const [from, to] = lineRanges[middle];
if (idx < from) {
if (middle === 0) return -1;
endRow = middle;
continue;
}
if (idx > to) { startRow = middle; continue; }
return middle;
}
return -1;
}
function regexIndexOf (string, regex, startpos = 0) {
const indexOf = this.substring(startpos || 0).search(regex);
return (indexOf >= 0) ? (indexOf + (startpos || 0)) : indexOf;
}
function regexLastIndexOf (string, regex, startpos = string.length) {
regex = (regex.global)
? regex
: new RegExp(regex.source, 'g' +
(regex.ignoreCase ? 'i' : '') +
(regex.multiLine ? 'm' : ''));
const stringToWorkWith = this.substring(0, startpos + 1);
let lastIndexOf = -1; let nextStop = 0; let result;
while ((result = regex.exec(stringToWorkWith)) != null) {
lastIndexOf = result.index;
regex.lastIndex = ++nextStop;
}
return lastIndexOf;
}
// -=-=-=-=-
// diffing
// -=-=-=-=-
function diff (s1, s2) {
if (typeof JsDiff === 'undefined') return 'diff not supported';
return JsDiff.convertChangesToXML(JsDiff.diffWordsWithSpace(s1, s2));
}
// -=-=-=-=-
// testing
// -=-=-=-=-
function empty (s) {
// show-in-doc
return s == '';
}
const includes = features.includes
? function (s, pattern) { return s.includes(pattern); }
: function (s, pattern) {
// Example:
// include("fooo!", "oo") // => true
return s.indexOf(pattern) > -1;
};
const include = includes;
const startsWith = features.startsWith
? function (s, pattern) { return s.startsWith(pattern); }
: function (s, pattern) {
// Example:
// startsWith("fooo!", "foo") // => true
return s.indexOf(pattern) === 0;
};
function startsWithVowel (s) {
// show-in-doc
const c = s[0];
return c === 'A' || c === 'E' || c === 'I' || c === 'O' || c === 'U' ||
c === 'a' || c === 'e' || c === 'i' || c === 'o' || c === 'u' || false;
}
const endsWith = features.endsWith
? function (s, pattern) { return s.endsWith(pattern); }
: function (s, pattern) {
// Example:
// endsWith("fooo!", "o!") // => true
const d = s.length - pattern.length;
return d >= 0 && s.lastIndexOf(pattern) === d;
};
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// string conversion and manipulation
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
function withDecimalPrecision (str, precision) {
// String -> Number -> String
// Example: withDecimalPrecision("1.12345678", 3) // => "1.123"
const floatValue = parseFloat(str);
return isNaN(floatValue) ? str : floatValue.toFixed(precision);
}
function capitalize (s) {
// Example:
// capitalize("foo bar") // => "Foo bar"
return s.length ? s.charAt(0).toUpperCase() + s.slice(1) : s;
}
function camelCaseString (s) {
// Spaces to camels, including first char
// Example: camelCaseString("foo bar baz") // => "FooBarBaz"
return s.split(' ').map(capitalize).join('');
}
function camelize (s) {
// Dashes to camels, excluding first char
// Example: camelize("foo-bar-baz") // => "fooBarBaz"
const parts = s.split('-');
const len = parts.length;
if (len == 1) return parts[0];
let camelized = s.charAt(0) == '-'
? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
: parts[0];
for (let i = 1; i < len; i++) { camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); }
return camelized;
}
function decamelize (s) {
// Camels to spaces
const len = s.length;
let lastSplitAt = 0;
const parts = [];
for (let i = 0; i < len; i++) {
if (s[i] != ' ' &&
isNaN(Number.parseInt(s[i])) &&
s[i] === s[i].toUpperCase() &&
i !== 0) {
parts.push(s.slice(lastSplitAt, i).toLowerCase());
lastSplitAt = i;
}
}
parts.push(s.slice(lastSplitAt).toLowerCase());
return parts.join(' ');
}
function truncate (s, length, truncation) {
// Enforces that s is not more then `length` characters long.
// Example:
// truncate("123456789", 5) // => "12..."
length = length || 30;
truncation = truncation === undefined ? '...' : truncation;
return s.length > length
? s.slice(0, length - truncation.length) + truncation
: String(s);
}
function truncateLeft (s, length, truncation) {
// Enforces that s is not more then `length` characters long.
// Example:
// truncate("123456789", 5) // => "12..."
length = length || 30;
truncation = truncation === undefined ? '...' : truncation;
return s.length > length
? truncation + s.slice(-length)
: String(s);
}
function regExpEscape (s) {
// For creating RegExps from strings and not worrying about proper escaping
// of RegExp special characters to literally match those.
// Example:
// var re = new RegExp(regExpEscape("fooo{20}"));
// re.test("fooo") // => false
// re.test("fooo{20}") // => true
return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1')
.replace(/\x08/g, '\\x08');
}
function succ (s) {
// Uses char code.
// Example:
// succ("a") // => "b"
// succ("Z") // => "["
return s.slice(0, s.length - 1) + String.fromCharCode(s.charCodeAt(s.length - 1) + 1);
}
function digitValue (s) {
// ignore-in-doc
return s.charCodeAt(0) - '0'.charCodeAt(0);
}
const times = features.repeat
? function (s, count) { return s.repeat(count); }
: function (s, count) {
// Example:
// string.times("test", 3) // => "testtesttest"
return count < 1 ? '' : new Array(count + 1).join(s);
};
function longestCommonSubstring (a, b) {
// Example:
// longestCommonSubstring("bar foo barrr", "hello fooo world");
// => {indexA: 3, indexB: 5, length: 4, string: " foo"}
const lcs = [];
for (let i = 0; i < a.length; i++) {
lcs[i] = [];
for (let j = 0; j < b.length; j++) { lcs[i][j] = 0; }
}
// if B is null then LCS of A, B =0
for (let i = 0; i < a.length; i++) lcs[i][0] = 0;
// fill the rest of the matrix
for (let i = 1; i < a.length; i++) {
for (let j = 1; j < b.length; j++) {
lcs[i][j] = a[i - 1] == b[j - 1]
? lcs[i - 1][j - 1] + 1
: 0;
}
}
let maxLength = -1; let indexA = -1; let indexB = -1;
for (let i = 0; i < a.length; i++) {
for (let j = 0; j < b.length; j++) {
const length = lcs[i][j];
if (maxLength < length) {
maxLength = length;
indexA = i - length;
indexB = j - length;
}
}
}
return {
length: maxLength,
indexA,
indexB,
string: maxLength > 0 ? a.slice(indexA, indexA + maxLength) : ''
};
}
/**
* Applys a change to a string.
* @param { string } string - The string to change.
* @param { Object } change - The change to apply
* @param { Number } change.start - The start positing to apply the change from.
* @param { Number } [change.end] - If applicable the position in the string up to which the change applies.
* @param { string[] } [change.lines] - If applicable the lines to insert or replace into the range.
* @param { "insert"|"remove"|"replace" } change.action - The type of change to be applied.
* @returns { string } The transformed string.
*/
function applyChange (string, change) {
switch (change.action) {
case 'insert':
return string.slice(0, change.start) +
change.lines.join('\n') +
string.slice(change.start);
case 'remove':
return string.slice(0, change.start) +
string.slice(change.end);
case 'replace':
return string.slice(0, change.start) +
change.lines.join('\n') +
string.slice(change.end);
}
return string;
}
/**
* Apply a set of changes to a given string.
* @param { string } s - The string to be changed.
* @param { Object[] } changes - The set of changes to be applied to the string.
* @returns { string } The transformed string.
*/
function applyChanges (s, changes) {
return changes.reduce(function (result, change) {
return applyChange(result, change);
}, s);
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// diffing / comparing
function levenshtein (a, b) {
// How many edit operations separate string a from b?
// MIT licensed, https://gist.github.com/andrei-
// Copyright (c) 2011 Andrei Mackenzie and https://github.com/kigiri
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
let tmp, i, j, prev, val, row;
// swap to save some memory O(min(a,b)) instead of O(a)
if (a.length > b.length) { tmp = a; a = b; b = tmp; }
row = Array(a.length + 1);
// init the row
for (i = 0; i <= a.length; i++) row[i] = i;
// fill in the rest
for (i = 1; i <= b.length; i++) {
prev = i;
for (j = 1; j <= a.length; j++) {
if (b[i - 1] === a[j - 1]) {
val = row[j - 1]; // match
} else {
val = Math.min(row[j - 1] + 1, // substitution
Math.min(prev + 1, // insertion
row[j] + 1)); // deletion
}
row[j - 1] = prev;
prev = val;
}
row[a.length] = prev;
}
return row[a.length];
}
function incName (name) {
return name.replace(/(?:_([0-9]*))?$/, (match, n) => match ? `_${Number(n) + 1}` : '_1');
}
// poly-filling...
// if (!features.repeat) String.prototype.repeat = function(n) { string.repeat(this, n); };
// if (!features.includes) String.prototype.includes = function(x) { string.include(this, x); }
// if (!features.startsWith) String.prototype.startsWith = function(n) { string.startsWith(this, n); }
// if (!features.endsWith) String.prototype.endsWith = function(n) { string.endsWith(this, n); }
export {
format,
formatFromArray,
indent,
minIndent,
changeIndent,
quote,
articlize,
findArticle,
print,
printNested,
pad,
printTable,
printTree,
toArray,
lines,
paragraphs,
nonEmptyLines,
tokens,
tableize,
unescapeCharacterEntities,
base64EncodeUnicode,
base64DecodeUnicode,
toQueryParams,
normalizePath,
joinPath,
newUUID,
isUUID,
createDataURI,
hashCode,
md5,
reMatches,
stringMatch,
peekRight,
peekLeft,
lineIndexComputer,
lineNumberToIndexesComputer,
findLineWithIndexInLineRanges,
regexIndexOf,
regexLastIndexOf,
lineRanges,
diff,
empty,
includes,
include,
startsWith,
startsWithVowel,
endsWith,
withDecimalPrecision,
capitalize,
camelCaseString,
camelize,
truncate,
truncateLeft,
regExpEscape,
succ,
digitValue,
times,
longestCommonSubstring,
applyChange,
applyChanges,
levenshtein,
decamelize,
ensureFolder,
incName
};