UNPKG

dtl-js

Version:

Data Transformation Language - JSON templates and data transformation

1,263 lines (1,220 loc) 102 kB
/* ================================================= * Copyright (c) 2015-2022 Jay Kuri * * This file is part of DTL. * * DTL is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * DTL is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with DTL; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * ================================================= */ 'use strict'; const uuid = require('uuid'); const sprint = require('sprint').sprint; const strftime = require('strftime'); const BigNumber = require('bignumber.js'); const deepEqual = require('fast-deep-equal'); const dtl_random = require('./dtl-random.js'); const __internal = Symbol.for('__internal'); const __scalar = Symbol.for('__scalar'); var random_charmap = { 'a': 'abcdefghijklmnopqrstuvwxyz', 'A': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'b': 'bcdfghjklmnpqrstvwxyz', 'B': 'BCDFGHJKLMNPQRSTVWXYZ', 'e': 'aeiouy', 'E': 'AEIOUY', 'F': '0123456789abcdef', '#': '0123456789', '!': '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\', '.': 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\' }; // given a thing, try to create a regexp from it. If given a string // that isn't in the form '/.../' - return the string as is, if // we are given something that can't be made into a regexp, // return a regexp that can not match anything. function obtain_regex_from(thing) { var re; var typeof_thing = typeof thing; if (typeof_thing == 'object') { if (thing instanceof RegExp) { re = thing; } else if (Array.isArray(thing)) { re = new RegExp(thing[0], thing[1]); } else { re = new RegExp(thing.toString()); } } else if (typeof_thing == 'string') { var check = thing.match(/^\/(.*)\/([gimy]+)*$/); if (check !== null) { re = new RegExp(check[1], check[2]); } else { var check = thing.match(/^\#(.*)\#([gimy]+)*$/); if (check !== null) { re = new RegExp(check[1], check[2]); } else { re = thing; } } } else { // we can't make a regex out of whatever it is they gave us... // so we need a regex that can't ever match anything. // this may look odd, but it _is_ a regex that can never match. // Look ahead, the next letter should be a... but also b. re = new RegExp('^(?=a)b'); } return re; } function safe_object(obj) { // This function returns the original object - UNLESS the object is null // then we return an empty object in it's stead, so we don't have to keep // checking for null constantly. if (typeof obj != 'undefined' && obj === null) { return {}; } else { return obj; } } // returns a deep copy of an object disconnected from the original. // may not be the most efficient way. worth investigating further. function isolate_object(obj) { return JSON.parse(JSON.stringify(obj)); } function make_iteratee_arguments(collection, index, value) { var result = {}; let internal = {}; result[__internal] = internal result.index = index; result.all = collection; internal.index = index; internal.all = collection; if (typeof value !== 'undefined') { result.item = value; } else { result.item = collection[index]; } return result; } function transform_helper(options, input_data, transforms, transform_name) { var results; let transform_options = {}; Object.assign(transform_options, options); // at this level, we always return bignumbers. Let the top-level apply clean // them up if necessary. transform_options.return_bignumbers = true; if (typeof transforms == 'undefined' && typeof transform_name == 'undefined' && typeof input_data == 'string') { // we have only a single argument and it's a string, we will treat that // as though we are passing the current data into the named transform. transforms = input_data; input_data = transform_options.input_data; } if (typeof transform_name == 'undefined' && typeof transforms == 'string') { transform_name = transforms; transforms = transform_options.transforms; } results = transform_options.transformer(input_data, transforms, transform_name, transform_options); return results; } // if eat_undefined is set, then we don't push undefined onto the list function map(set, func, eat_undefined) { var list; //, index, item; var result = [], res, i; if (typeof set != 'object') { if (typeof set == 'undefined') { set = []; } else { set = [set]; } } if (Array.isArray(set)) { for (i = 0; i < set.length; i++) { res = func(set[i], i, set); if (!eat_undefined || res !== undefined) { result.push(res); } } } else { list = Object.keys(set).sort(); for (i = 0; i < list.length; i++) { res = func(set[list[i]], list[i], set); if (!eat_undefined || res !== undefined) { result.push(res); } } } return result; } function reduce(set, func, memo) { var list, previous_res = memo; var i; if (typeof set != 'object') { if (typeof set == 'undefined') { set = []; } else { set = [set]; } } if (Array.isArray(set)) { previous_res = set.reduce(func, memo); } else { list = Object.keys(set); previous_res = list.reduce(function(res, item, index, list) { return func(res, set[item], item, set); }, memo); } return previous_res; } function obj_keys(obj) { var all_keys, i; var keys = []; if (typeof obj == 'object') { all_keys = Object.keys(obj); if (typeof obj.prototype == 'undefined') { keys = all_keys; } else { keys = []; for (i = 0; i < all_keys.length; i++) { if (Object.prototype.hasOwnProperty.call(obj, all_keys[i])) { keys.push(all_keys[i]); } } } } else { keys = []; } return keys; } function concat() { var args = Array.prototype.slice.call(arguments); var type = 'undefined'; // find the type of the first non-empty item // so we can do the concat correctly ( otherwise 'undefined' at the start confuses things) // also allows casting by providing [] or {} at the start. var item, i = 0; while (type == 'undefined' && args.length > 0) { item = args[i]; type = typeof item; if (type == 'object' && Array.isArray(item)) { type = 'array'; } if (type == 'undefined') { args.shift(); } else { i++; } } var out; if (type == 'array') { out = []; args.forEach(function(item) { if (typeof item != 'undefined' && item !== null) { if (Array.isArray(item)) { out = out.concat(item); } else { out = out.concat([item]); } } }); } else if (type == 'object') { out = {}; args.forEach(function(item, index) { if (typeof item != 'undefined' && item !== null) { if (typeof item == 'string') { out[item] = item; } else { for (var key in item) { if (Object.prototype.hasOwnProperty.call(item, key)) { if (typeof item[key] == 'object') { out[key] = isolate_object(item[key]); } else { out[key] = item[key]; } } } } } }); } else { out = ""; args.forEach(function(item) { if (typeof item != 'undefined' && item !== null) { if (typeof item == 'string') { out += item; } else if (typeof item.toString == 'function') { out += item.toString(); } } }); } return out; } function to_base64(thing) { if (typeof Buffer != 'undefined') { if (Buffer.isBuffer(thing)) { return thing.toString('base64'); } else { return Buffer.from(thing).toString('base64'); } } else { // fallback to browser compat return btoa(thing); } } function from_base64(str) { if (typeof Buffer != 'undefined') { return Buffer.from(str, 'base64').toString('utf8'); } else { // fallback to browser compat return atob(str); } } function to_number(str) { let new_str = str.replace(/^([\.0-9]+)(.)*/, "$1"); var res = BigNumber(new_str); if (isNaN(res)) { return undefined; } else { return res; } } function numberToUint8Array(num) { const arr = new Uint8Array(4); arr[0] = (num >>> 24) & 0xFF; arr[1] = (num >>> 16) & 0xFF; arr[2] = (num >>> 8) & 0xFF; arr[3] = num & 0xFF; return arr; } /** * Converts BigNumber in an object to string or number based on boolean bignumber_as_string parameter. * @param {boolean} bignumber_as_string - If true, converts all BigNumbers to strings, else convert them to numbers. * @param {(BigNumber|Object)} obj - An object to convert its BigNumbers or an instance of BigNumber. * @returns {(Object|Array)} - Object or array with converted BigNumbers. */ function deep_bignumber_convert(bignumber_as_string, obj) { let new_obj; if (BigNumber.isBigNumber(obj)) { if (bignumber_as_string) { return obj.toString(); } else { return obj.toNumber(); } } else if (typeof obj == 'object') { if (obj === null || obj instanceof RegExp || (typeof Buffer !== 'undefined' && Buffer.isBuffer(obj))) { // if the object is null or it's a regex, we return it untouched. return obj; } else if (Array.isArray(obj)) { new_obj = []; for(let i = 0; i < obj.length; i++) { if (typeof obj[i] == 'object') { new_obj[i] = deep_bignumber_convert(bignumber_as_string, obj[i]); } else { new_obj[i] = obj[i]; } } } else { new_obj = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { if (typeof obj[key] == 'object') { new_obj[key] = deep_bignumber_convert(bignumber_as_string, obj[key]); } else { new_obj[key] = obj[key]; } } } } return new_obj; } else { return obj; } } function parse_template_string(template_string) { let segments = []; let temp_segment = ''; let expression = ''; let in_expression = false; let braceDepth = 0; let prev_char; for (let char of template_string) { if (char === '{' && prev_char != '\\') { if (braceDepth === 0) { if (temp_segment) { segments.push(temp_segment); temp_segment = ''; } in_expression = true; } braceDepth++; if (braceDepth > 1) { expression += char; } } else if (char === '}' && in_expression) { braceDepth--; if (braceDepth === 0) { segments.push({ 'expression': "(: " + expression + " :)" }); expression = ''; in_expression = false; } else { expression += char; } } else { if (in_expression) { expression += char; } else { temp_segment += char; } } prev_char = char; } if (temp_segment) { segments.push(temp_segment); } return segments; } function json_preserve_undefined(k, v) { if ( v === undefined ) { return null; } else { return v; } } function debug_helper(options, label, thing) { let args = Array.from(arguments); var debug_msg = options.debug; // || console.log.bind(console); let actual_thing = thing; if (arguments.length == 2) { actual_thing = label; label = 'debug'; } debug_msg(label, actual_thing); return actual_thing; }; function stringToUint8Array(str) { const arr = new Uint8Array(str.length); for (let i = 0; i < str.length; i++) { arr[i] = str.charCodeAt(i); } return arr; } // ALL FUNCTIONS SHOULD TAKE ARGS IN THE FOLLOWING ORDER: // THING TO OPERATE ON, PARAMETERS. var helper_list = { 'from_base64': { "meta": { "syntax": 'from_base64( $base64string )', 'returns': 'The result of base64 decoding the provided `$base64string`', "description": [ 'The `from_base64()` helper decodes the given `$base64string` and returns the', 'decoded string.' ], }, 'string': function(str) { return from_base64(str); }, }, 'to_base64': { "meta": { "syntax": 'to_base64( $string )', 'returns': 'The result of base64 encoding the provided `$string`', "description": [ 'The `to_base64()` helper encodes the given `$string` and returns the', 'encoded string.' ], }, 'string': function(thing) { return to_base64(thing); }, }, // ****************************************************************** // * below here are valid DTL helpers. Above are not yet determined * // ****************************************************************** '?': { "meta": { "syntax": '?( condition trueexpression falseexpression )', 'returns': 'Trueexpression if condition is true, falseexpression if condition is not true', "description": [ 'The `?()` helper is a simple conditional operator. Given a condition, the helper', 'will evaluate the condition and if the result is truthy it will evaluate and ', 'return the `trueexpression`. If the condition result is falsy, it will evauate and ', 'return the `falseexpression`. ', 'The `?()` is one of the primary decision mechanisms in DTL. It is important to ', 'understand that the `trueexpression` and `falseexpression` are not interpreted until', 'the condition has been evaluated. This means the `?()` can be used to execute', 'significantly complex logic only when needed. It can also be used to control which', 'of multiple transforms might be used for a given part of input data.' ], }, 'unprocessed_args': function() { var args = Array.prototype.slice.call(arguments); var options = args.shift(); var expr = options.interpret_operation(options, args.shift()); if (expr) { return options.interpret_operation(options, args[0]); } else { if (typeof args[1] !== 'undefined') { // if (args[1] == ":" && typeof args[2] !== 'undefined') { // return args[2]; // } else { // return args[1]; // } return options.interpret_operation(options, args[1]); } else { return undefined; } } } }, 'num': { "meta": { "syntax": 'num($string)', 'returns': 'The passed string converted to a number, if possible', "description": [ 'The num() helper, or its alias #() takes a string as input and converts ', 'it to a real number. If the data passed can not be parsed as a valid number, ', 'num() will return undefined. If you must have a numeric value, the fne() helper ', 'can be used in conjunction with num() in order to ensure a valid default value. ', 'For example: fne(num($data.val) 0) will provide a $data.val as a number, or if it', 'is not present or cannot be converted, will return 0.' ], }, "aliases": ['#'], "handles_bignumbers": true, "string": to_number, "undefined": function() { return undefined; }, "bignumber": function(bignumber) { return bignumber; }, "number": function(num) { return BigNumber(num); }, "*": function(thing) { if (typeof thing.toNumber == 'function') { return BigNumber(thing.toNumber()); } else { return undefined; } } }, "debug": { 'meta': { 'syntax': 'debug( [$label] $data_item )', 'returns': 'Passed value', 'description': [ 'Debug, causes the value of the item to be output to the debug console. ', 'If `$label` is provided the value will be prefixed by label in the output. ', 'Has no effect on the value and simply returns what is passed unchanged. ', 'This is useful to see the values of something while it is being processed.' ] }, "*": debug_helper, "wants_options": true }, "@": { 'meta': { 'syntax': '@()', 'returns': '', "deprecated": true, 'description': [ 'DEPRECATED. See `debug()`' ] }, "*": debug_helper, "wants_options": true }, "&": { 'meta': { 'syntax': '&( $data_item1 $data_item2 [ $data_item3 ... ] )', 'returns': 'The passed items concatenated together', 'description': [ 'Returns the passed items concatenated together. The type of item returned ', 'is determined by the first argument. If the first argument is a list, the ', 'remaining items will be added to the end of that list. If the first argument ', 'is a string, the following items will be concatenated to the end of the string, ', 'first being converted to strings if they are not already. ', 'If the first item is an object, and the additional items are also objects, ', 'the returned item will represent the objects merged at the top level.' ] }, "*": concat, }, "empty": { 'meta': { 'syntax': 'empty( $data_item )', 'returns': 'boolean indicating whether the passed item is empty', 'description': [ 'Returns true when the passed item is empty. ', 'Empty means undefined, of length 0 (in the case of an array or string) ', 'or in the case of an object, containing no keys' ] }, 'array,string': function(thing) { return (thing.length === 0); }, 'object': function(obj) { if (obj === null) { return true; } for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) return false; } return true; }, '*': function(thing) { if (thing === undefined) { return true; } else { return false; } } }, "exists": { 'meta': { 'syntax': 'exists( $data_item )', 'returns': 'boolean indicating whether the passed item is defined at all.', 'description': [ 'Returns true when the passed item is not undefined.', ] }, "undefined": function(thing) { return false; }, "*": function() { return true; } }, "group": { 'meta': { 'syntax': 'group( $items $bucket_name_transform [ $value_transform ] )', 'returns': 'An object containing groups of values, grouped by the results of $bucket_name_transform', 'description': [ 'Returns an object containing the values provided grouped into named buckets. ', 'The bucket an item goes into is based on the $bucket_name_transform provided. ', 'The $bucket_name_transform receives each item in turn and should return the ', 'name of the bucket that item belongs to. The $value_transform argument is ', 'optional and when provided will allow you to put a calculated value into the ', 'resulting group, rather than the input value. The $value_transform receives ', 'the item and should return the value to be stored.' ] }, "array,object": function(options, input, key_transform, value_transform) { var result = {}; map(input, function group_function(value, key, list) { var new_input_data = make_iteratee_arguments(list, key, value); var bucket_name = transform_helper(options, new_input_data, key_transform); if (typeof bucket_name == 'undefined') { return; } var data = value; if (typeof value_transform != 'undefined') { data = transform_helper(options, new_input_data, value_transform); } if (typeof result[bucket_name] == 'undefined') { result[bucket_name] = []; } result[bucket_name].push(data); }); return result; }, "coerce": ["array"], "wants_options": true }, "explode": { 'meta': { 'syntax': 'explode( $string )', 'returns': 'An array of single characters representing the contents of the string provided', 'description': [ 'Returns the string as an array of single characters .', ] }, "string": function(string) { var len = string.length; var arr = new Array(len); for (var i = 0; i < len; i++) { arr[i] = string.charAt(i); } return arr; } }, "first": { 'meta': { 'syntax': 'first( $array [ $transform ] )', 'returns': 'The first item in the array that matches the condition', 'description': [ 'Returns the first item in the provided array that $transform returns true for. ', 'The default for $transform is "(: !empty($item) :)" - so by default first() ', 'returns the first non-empty item in the provided array.' ] }, "array": function(options, input, transform, extra) { var results = []; var i_should_include_it; if (transform == undefined) { transform = options.transform_quoter('!empty($item)'); } for (var i = 0; i < input.length; i++) { var new_input_data = make_iteratee_arguments(input, i, input[i]); if (typeof extra !== 'undefined') { new_input_data.extra = extra; } i_should_include_it = false; i_should_include_it = transform_helper(options, new_input_data, transform); if (i_should_include_it) { return input[i]; } } // if we get here, nothing matched return undefined; }, "coerce": ["array"], "wants_options": true }, "grep": { 'meta': { 'syntax': 'grep( $array_or_object $search_transform [ $value_transform ] [ $extra ])', 'returns': 'An array containing all the items that match $search_transform', 'description': [ 'Returns an array or object containing all the items in $array_or_object ', 'that match the $search_transform. The $search_transform receives each $item ', 'in turn and should return true or false on whether the item should be included ', 'in the result. If true, by default the item is placed into the resulting array. If, ', 'however, a $value_transform is provided, the $item is provided to the $value_transform ', 'and the value returned from $value_transform is placed into the results instead. ', 'The $extra data, if provided, is also available in the transform as $extra.' ] }, "array": function(options, input, transform, value_transform, extra) { var results = []; var i_should_include_it; if (transform == undefined) { transform = options.transform_quoter('!empty($item)'); } for (var i = 0; i < input.length; i++) { var new_input_data = make_iteratee_arguments(input, i, input[i]); new_input_data.extra = extra; i_should_include_it = false; i_should_include_it = transform_helper(options, new_input_data, transform); if (i_should_include_it) { if (value_transform == undefined) { results.push(input[i]); } else { results.push(transform_helper(options, new_input_data, value_transform)); } } } return results; }, "object": function(options, input, transform, value_transform, extra) { var results = {}; if (transform == undefined) { transform = options.transform_quoter('!empty($item)'); } map(input, function(value, key, list) { var new_input_data = make_iteratee_arguments(list, key, value); new_input_data.extra = extra; var i_should_include_it = transform_helper(options, new_input_data, transform); if (i_should_include_it) { results[key] = value; if (value_transform == undefined) { results[key] = value; } else { results[key] = (transform_helper(options, new_input_data, value_transform)); } } }); return results; }, "coerce": [ "array" ], "wants_options": true }, // need to make derive more robust "derive": { 'meta': { 'syntax': 'derive( $data $action_map )', 'returns': 'Resulting data from the $action_map provided, or undefined if no matching rule was found.', 'description': [ 'Derive processes the provided data through the action_map given. ', 'An action map is an array of transformation pairs, where the first item in the pair ', 'is the test to perform on the data, and the second item is a transform which returns ', 'the data if the test is successful. During processing of the data, the first test which ', 'produces a true result will be used, and no further checks will be done. See the ', 'wiki (or the unit tests) for more details and examples.' ] }, "*": function(options, input, action_map_provided) { var action_map; var results = []; if (typeof action_map_provided == 'string') { action_map = options.transforms[action_map_provided]; } else if (Array.isArray(action_map_provided)) { action_map = action_map_provided; } var action_map_length = action_map.length; if (action_map != undefined) { var result; var match; var new_input_data = input; for (var i = 0; i < action_map_length; i++) { match = transform_helper(options, new_input_data, action_map[i][0]); if (!!match) { // action_map[i][1] is the action: result = transform_helper(options, new_input_data, action_map[i][1]); break; } } return(result); } }, "wants_options": true }, 'chain': { 'meta': { 'syntax': 'chain( $data $transform_chain)', 'returns': 'An array of items transformed using the transform chain', 'description': [ 'Chain processes the provided data through the transform_chain provided. ', 'A transform chain is simply an array of transforms. The data is provided to the first ', 'transform, and the output of that transform becomes the input to the next, and so on. ', 'This allows you to describe complex data transformations in a concise way. Even when ', 'the transformation requires multiple steps to complete.' ] }, '*': function(options, input, action_list_provided) { var action_list; var results = []; if (typeof action_list_provided == 'string') { action_list = options.transforms[action_list_provided]; } else if (Array.isArray(action_list_provided)) { action_list = action_list_provided; } var action_list_length = action_list.length; var data = input; if (action_list != undefined) { for (var i = 0; i < action_list_length; i++) { // action_map[i][0] is the test item for this pair data = transform_helper(options, data, action_list[i]); } } return data; }, "wants_options": true }, 'fne': { 'meta': { 'syntax': 'fne( $item1 $item2 $item3 ...)', 'returns': 'The first non-empty item in the provided arguments', 'description': [ 'The fne (First Non Empty) helper returns the first non-empty item in the provided arguments. ', 'This is a useful way to obtain a piece of data from one of several places. It is ', 'especially useful when you would like to use a piece of provided input data, or use ', 'an item can be defined, but still be empty, this is much safer to use than a standard ', '"if exists" type construct. The method for determining empty is exactly the same as ', 'the empty() helper. In the case where all arguments are empty, fne returns `undefined`' ] }, "*": function() { var args = Array.prototype.slice.call(arguments); for (var i = 0; i < args.length; i++) { var has_value = false; var thing_type = typeof args[i]; if (Array.isArray(args[i]) || thing_type == 'string') { has_value = (args[i].length !== 0); } else if (thing_type == 'object') { var obj = safe_object(args[i]); for (var key in obj) { if (Object.prototype.hasOwnProperty.call(args[i], key)) { has_value = true; } } } else { if (args[i] !== undefined) { has_value = true; } } if (has_value) { return args[i]; } } return undefined; } }, "now": { 'meta': { 'syntax': 'now( $seconds_only )', 'returns': 'The current time in milliseconds since epoch', 'description': [ 'now() returns the current time in milliseconds since epoch. If $seconds_only is ', 'passed and is true, the return value will be seconds since epoch and any milliseconds ', 'will be discarded.' ] }, "*": function(secondsonly) { var now = Date.now(); if (secondsonly) { return Math.floor(now / 1000); } else { return now; } }, }, "strftime": { 'meta': { 'syntax': 'strftime( $time_format $time_since_epoch [ $timezone ] )', 'returns': 'The a string formatted version of the timestamp given', 'description': [ 'strftime() returns a string representing the provided time in the format provided in ', 'the $time_format argument. The options available in $time_format are those provided for ', 'in the ISO-C (and therefore POSIX) strftime function' ] }, "*": function(string, time, timezone) { // time in strftime is assumed to be 'local' unless a timezone argument is provided. // to work with UTC times, simply provide a timezone of '+0000' var my_strftime = strftime; var t; var timestamp, date; var tz = false; if ((typeof timezone == 'string' && /^[+\-][0-9][0-9][0-9][0-9]$/.test(timezone)) || (typeof timezone == 'number')) { my_strftime = strftime.timezone(timezone); tz = true; } else if (typeof timezone != 'undefined') { return undefined; } if (typeof time == 'number') { timestamp = time; date = new Date(time); } else if (typeof time == 'object') { if (Array.isArray(time)) { t = [undefined].concat(time); if (typeof t[2] == 'number' || BigNumber.isBigNumber(t[2])) { t[2] = t[2] - 1; } if (tz) { t.shift(); date = new Date(Date.UTC.apply(undefined, t)); } else { date = new(Function.prototype.bind.apply(Date, t)); } } else { if (tz) { date = new Date(Date.UTC(time.year, (time.month - 1 || 0), (time.day || 1), (time.hour || 0), (time.minutes || 0), (time.seconds || 0), (time.milliseconds || 0))); } else { date = new Date(time.year, (time.month - 1 || 0), (time.day || 1), (time.hour || 0), (time.minutes || 0), (time.seconds || 0), (time.milliseconds || 0)); } } } else if (typeof time == 'string') { if (/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d(:\d\d)?(\.\d+)?([\+\-]\d\d:\d\d)?$/.test(time)) { date = new Date(time); my_strftime = strftime.timezone('+0000'); } else if (time == 'now') { date = new Date(); my_strftime = strftime.timezone('+0000'); } } else if (typeof time == 'undefined') { return undefined; } return my_strftime(string, date); }, }, "random_string": { 'meta': { 'syntax': 'random_string( $template [ $charmap ] )', 'returns': 'random string created using the mask and charmap given', 'description': [ 'The random_string() helper produces a random set of characters based on the template ', 'provided. The $charmap is an object where each key is a single character, and the ', 'associated value is a string containing all the charcters that can be cnosen for that ', 'key character. Each character in the $template is looked up in the charmap and a random ', 'character from the charmap is chosen. The default charmap (which is used when none is provided ', 'provides the following values: "a": lowercase alphabetical characters, "A": uppercase ', 'alphabetical characters, "b": lowercase consonants, "B": uppercase consonants, ', '"e": lowercase vowels, "E": uppercase consonants, "#": Digits 0-9, "F": hexadecimal digit (0-f), ', '"!": punctuation mark or ".": any printable character (ASCII set). Other languages / methods ', 'are supported by providing your own character map.' ] }, "*": function(mask, charmap) { var result = []; var character_map = random_charmap; var default_map = random_charmap['.']; // allow charmap to override our default map; if (typeof charmap == 'object') { character_map = charmap; default_map = charmap[Object.keys(charmap)[0]]; } else if (typeof charmap == 'string') { default_map = charmap; } var current_char_map; for (var i = 0; i < mask.length; i++) { current_char_map = undefined; if (typeof character_map == 'object' && typeof character_map[mask[i]] == 'string') { current_char_map = character_map[mask[i]]; } if (typeof current_char_map == 'undefined') { current_char_map = default_map; } result[i] = current_char_map[dtl_random.random_number_up_to(current_char_map.length - 1)]; } return result.join(''); } }, "keys": { 'meta': { 'syntax': 'keys( $object )', 'returns': 'The keys in the $object provided', 'description': [ 'The keys() helper retrieves the keys present in the given object. If given an ', 'array, the indexes present will be returned.' ] }, "object": function(obj) { // var list = []; // for (var key in obj) { // list.push(key); // } return obj_keys(obj); }, "array": function(arr) { var result = []; // Though it should not be possible to create sparse arrays natively // in DTL, they can come from the data source / source language. // This handles retrieving the indexes from sparse arrays properly. arr.forEach(function(item, index) { result.push(index); }); return result; }, "*": function(thing) { return []; } }, "length": { 'meta': { 'syntax': 'length( $item )', 'returns': 'The length of the $item provided', 'description': [ 'The length() helper returns the length of the given item. If $item is a string ', 'the length in characters will be returned. If $item is an array, the number of ', 'items in the array will be returned. If $item is an object, the number of keys ', 'in the object will be returned. For all other types, 1 will be returned, with ', 'the exception of undefined, which has a length of 0.' ] }, "string": function(str) { return str.length; }, "array": function(arr) { return arr.length; }, "undefined": function(undef) { return 0; }, "object": function(obj) { if (obj == null) { return 0; } else { return Object.keys(obj).length; } }, "*": function(thing) { return 1; } }, "url_encode": { 'meta': { 'syntax': 'url_encode( $string )', 'returns': 'The string provided encoded using Percent Encoding.', 'description': [ 'The url_encode() helper returns the string represented using Percent Encoding' ] }, "string": function(raw_string) { return encodeURIComponent(raw_string); } }, "url_decode": { 'meta': { 'syntax': 'url_decode( $encoded_string )', 'returns': 'The results of decoding $encoded_string using Percent Encoding.', 'description': [ 'The url_decode() helper returns the string decoded using Percent Encoding.', 'Undoes url_encode().' ] }, "string": function(encoded_str) { return decodeURIComponent(encoded_str); } }, "escape": { 'meta': { 'syntax': 'escape( $string [ $characters ] )', 'returns': 'The $string provided, with any occurances of special characters prefixed with a \\ ', 'description': [ 'The escape() helper adds backslashes to protect any special characters found in the ', 'provided string. This is especially useful in subscripts when a key might have odd ', 'characters in it. If the $characters argument is provided, escape() will add escaping ', 'to those characters instead of its default list: ( ) [ ] and .' ] }, "string": function(str, characters) { var chars = characters || '([\.])'; var re = new RegExp(chars, 'g'); return str.replace(re, '\\$1'); }, "*": function(arg) { return arg; } }, "regex": { 'meta': { 'syntax': 'regex( $pattern [ $flags ] )', 'returns': 'A regex created using the patter and flags provided.', 'description': [ 'The regex() helper creates a regular expression dynamically, allowing you to create ', 'a functional regular expression from input data. Regular expressions created in this ', 'way can be used in any location where a literal regular expression can be used.' ] }, "string,object": function(pattern, flags) { let re; if (typeof pattern == 'string' || pattern instanceof RegExp) { re = new RegExp(pattern, flags); } else { // this regex is impossible to match. Start of string, look ahead is a, // but next character is b. The only way to match this is a string // whose first character is both an 'a' and a 'b' at the same time. re = new RegExp('^(?=a)b'); } return re; } }, "replace": { 'meta': { 'syntax': 'replace( $string $search $replacement )', 'returns': 'The $string provided, with occurances of $search replaced with $replacement', 'description': [ 'The replace() helper searches in $string for any occurrences of $search. The $search', 'can be a string or a regex. To use a regex, use slashes around the search term. If a ', 'string is provided, only the first occurrance of the string will be replaced. If you ', 'wish to replace all occurrances of a string, use the regex form, providing the `g` flag. ', 'For example, to replace all occurrances of `a`, use `/a/g` as the search parameter. ' ] }, "string": function(str, search, replacement) { var re = obtain_regex_from(search); return str.replace(re, replacement); }, "*": function() { return ""; } }, // TODO: we need a "match" that can capture from a regexp "match": { 'meta': { 'syntax': 'match( $string $search )', 'returns': 'An array of the matches within the string', 'description': [ 'The match() helper tests the provided `$string` against the provided `$search` ', 'regular expression. It then returns an array containing the matched portions of', 'the string. The first item in the array is the matching string, and the remaining', 'elements contain the results of any captures in the regex. Returns an empty array', 'if the string did not match.' ] }, "string": function(str, search) { var result, re; re = obtain_regex_from(search); result = str.match(re); if (result === null) { result = []; } // we want a real array, not an array-like thing; return Array.prototype.slice.call(result); }, "*": function() { return []; } }, 'inject': { 'meta': { 'syntax': 'inject( $object $flattened_object)', 'returns': 'A new object with $flattened_object merged into $object', 'description': [ 'Merges $flattened_object into $object, returning a new object.', '`inject()` is designed to merge data into a deeply nested object.', 'flattened object can be the result of calling `flatten()` on an ', 'existing object, or it can be created on the spot. The keys in', 'the object are expected to key-paths indicating where the data', 'is to be placed in the new object. If the provided object does', 'not already contain the nested structure required, it will be ', 'created, replacing existing attributes if there is conflict.', 'Handling Arrays: when a numeric key is specified in the keypath', 'for a value and the corresponding object does not exist, an', 'array will be created in that position. Additionally, using a `+`', 'in a keypath where an array exists signals that the item should be', 'appended to the existing array.' ] }, "*": function(object, flattened_obj) { // Clone the original object to keep it unchanged var newObject = isolate_object(object); Object.keys(flattened_obj).forEach((keypath) => { let keys = keypath.split('.'); let value = flattened_obj[keypath]; let target = newObject; let last_key = keys[keys.length-1]; // Traverse the object to the specified keypath for (var i = 0; i < keys.length - 1; i++) { let key = keys[i]; if (Array.isArray(target) && key == '+') { key = target.length } if (typeof target[key] == 'object') { target = target[key]; } else { if (/^[0-9\+]+$/.test(keys[i+1])) { target[key] = []; } else { target[key] = {}; } target = target[key]; } } if (/^[0-9\+]+$/.test(last_key) && Array.isArray(target)) { if (last_key == '+') { if (!Array.isArray(value)) { value = [ value ]; } value.forEach((val) => { target.push(val) }) } else { target[last_key] = value; } } else { target[last_key] = value; } }) return newObject; } }, "flatten": { 'meta': { 'syntax': 'flatten( $array_or_object [ $separator ] [ $prefix ] )', 'returns': 'A structure representing the given nested structure flattened into a single-level structure', 'description': [ 'The `flatten()` helper takes either an array or an