From ea609d4376431aba3c96328bddab20c8818d66a4 Mon Sep 17 00:00:00 2001 From: Jakub Mifek Date: Wed, 29 Aug 2018 14:51:27 +0200 Subject: [PATCH 1/5] New functionality for st.js ## Ease of partial template use ```js let data = { "items": [ [ 1, 2, 3 ], [ 4, 5, 6 ], 7 ] }; let template = { "items": { "{{#each items}}": "{{#template partial_template}}" } }; let partialTemplate = { "sub-item": "{{this}}" }; ST.setTemplates({ "partial_template": partialTemplate }).transform(template, data) ``` Results in: ```json { "items": [ { "sub-item": [ 1, 2, 3 ] }, { "sub-item": [ 4, 5, 6 ] }, { "sub-item": 7 } ] } ``` ## Let statement memory clean-up After the let statement ended, the memory wasn't cleaned which could result in conflict especially when using same partial templates at multiple locations. If lasting of the variable in the memory was intended I would recommend declaring new function #var for that purpose. ## flatten function I introduced new function to the TRANSFORMER functionality. Just as there are function each, merge or concat, I defined function #flatten. The flatten function is used for 'flattening' of given array. ```js let data = { "items": [ [ 1, 2, 3 ], [ 4, 5, 6 ], 7 ] }; let template = { "items1": { "{{#flatten}}": "{{items}}" }, "items2": { "{{#flatten}}": { "{{#each items}}": "{{this}}" } }, "items3": { "{{#flatten}}": [1, 2, [3, 4], [5, 6, [7, 8]]] } }; ST.transform(template, data) ``` Results in: ```json { "items1": [ 1, 2, 3, 4, 5, 6, 7 ], "items2": [ 1, 2, 3, 4, 5, 6, 7 ], "items3": [ 1, 2, 3, 4, 5, 6, [ 7, 8 ] ] } ``` --- st.js | 200 ++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 145 insertions(+), 55 deletions(-) diff --git a/st.js b/st.js index 2568411..4583cc6 100644 --- a/st.js +++ b/st.js @@ -1,12 +1,12 @@ -(function() { +(function () { var $context = this; var root; // root context var Helper = { - is_template: function(str) { + is_template: function (str) { var re = /\{\{(.+)\}\}/g; return re.test(str); }, - is_array: function(item) { + is_array: function (item) { return ( Array.isArray(item) || (!!item && @@ -15,7 +15,7 @@ ) ); }, - resolve: function(o, path, new_val) { + resolve: function (o, path, new_val) { // 1. Takes any object // 2. Finds subtree based on path // 3. Sets the value to new_val @@ -30,7 +30,7 @@ }, }; var Conditional = { - run: function(template, data) { + run: function (template, data) { // expecting template as an array of objects, // each of which contains '#if', '#elseif', 'else' as key @@ -74,7 +74,7 @@ // so return null return null; }, - is: function(template) { + is: function (template) { // TRUE ONLY IF it's in a correct format. // Otherwise return the original template // Condition 0. Must be an array @@ -140,7 +140,7 @@ // Condition 4. // in case there's more than two items, everything between the first and the last item should be #elseif var they_are_all_elseifs = true; - for (var template_index = 1; template_index < template.length-1; template_index++) { + for (var template_index = 1; template_index < template.length - 1; template_index++) { var template_item = template[template_index]; for (var template_key in template_item) { func = TRANSFORM.tokenize(template_key); @@ -160,7 +160,7 @@ // Now we need to check the validity of the last item // Condition 5. // in case there's more than one item, it should end with #else or #elseif - var last = template[template.length-1]; + var last = template[template.length - 1]; for (var last_key in last) { func = TRANSFORM.tokenize(last_key); if (['#else', '#elseif'].indexOf(func.name.toLowerCase()) === -1) { @@ -173,10 +173,11 @@ }; var TRANSFORM = { memory: {}, - transform: function(template, data, injection, serialized) { + templates: {}, // Modified by Jakub Mifek + transform: function (template, data, injection, serialized) { var selector = null; if (/#include/.test(JSON.stringify(template))) { - selector = function(key, value) { return /#include/.test(key) || /#include/.test(value); }; + selector = function (key, value) { return /#include/.test(key) || /#include/.test(value); }; } var res; if (injection) { @@ -203,7 +204,7 @@ return res; } }, - tokenize: function(str) { + tokenize: function (str) { // INPUT : string // OUTPUT : {name: FUNCTION_NAME:STRING, args: ARGUMENT:ARRAY} var re = /\{\{(.+)\}\}/g; @@ -228,15 +229,17 @@ } return null; }, - run: function(template, data) { + run: function (template, data) { var result; var fun; if (typeof template === 'string') { // Leaf node, so call TRANSFORM.fillout() if (Helper.is_template(template)) { + var include_string_re = /\{\{([ ]*#include)[ ]*([^ ]*)\}\}/g; + fun = TRANSFORM.tokenize(template); + if (include_string_re.test(template)) { - fun = TRANSFORM.tokenize(template); if (fun.expression) { // if #include has arguments, evaluate it before attaching result = TRANSFORM.fillout(data, '{{' + fun.expression + '}}', true); @@ -245,6 +248,23 @@ // {'wrapper': '{{#include}}'} result = template; } + + /////////////////////////////// + /// Modified by Jakub Mifek /// + /////////////////////////////// + + } else if (fun && fun.name === "#template") { + + // insert one of stored templates + + let partial_template = TRANSFORM.templates[fun.expression]; + + result = TRANSFORM.run(partial_template, data); + + /////////////////////////// + /// End of modification /// + /////////////////////////// + } else { // non-#include result = TRANSFORM.fillout(data, template); @@ -275,9 +295,9 @@ // This needs to precede everything else since it's meant to be overwritten // in case of collision var include_object_re = /\{\{([ ]*#include)[ ]*(.*)\}\}/; - var include_keys = Object.keys(template).filter(function(key) { return include_object_re.test(key); }); + var include_keys = Object.keys(template).filter(function (key) { return include_object_re.test(key); }); if (include_keys.length > 0) { - // find the first key with #include + // find the first key with #include fun = TRANSFORM.tokenize(include_keys[0]); if (fun.expression) { // if #include has arguments, evaluate it before attaching @@ -296,6 +316,31 @@ if (fun) { if (fun.name === '#include') { // this was handled above (before the for loop) so just ignore + + /////////////////////////////// + /// Modified by Jakub Mifek /// + /////////////////////////////// + } else if (fun.name === '#flatten') { + let arr = TRANSFORM.run(template[key], data); + result = []; + + if(Helper.is_array(arr)) { + + // For each item in the array + for(let i = 0; i < arr.length; i++) { + + // If array then flatten + if(Helper.is_array(arr[i])) { + + for(let j = 0; j < arr[i].length; j++) + result.push(arr[i][j]); + + // Just push if anything else + } else { + result.push(arr[i]); + } + } + } } else if (fun.name === '#let') { if (Helper.is_array(template[key]) && template[key].length == 2) { var defs = template[key][0]; @@ -305,18 +350,39 @@ var parsed_keys = TRANSFORM.run(defs, data); // 2. modify the data - for(var parsed_key in parsed_keys) { + let originals = {}; + let moriginals = {}; + for (var parsed_key in parsed_keys) { + moriginals[parsed_key] = TRANSFORM.memory[parsed_key]; TRANSFORM.memory[parsed_key] = parsed_keys[parsed_key]; + originals[parsed_key] = data[parsed_key]; data[parsed_key] = parsed_keys[parsed_key]; } - // 2. Pass it into TRANSFORM.run + // 3. Pass it into TRANSFORM.run result = TRANSFORM.run(real_template, data); + + // 4. Remove the data from memory + for (var parsed_key in parsed_keys) { + if (moriginals[parsed_key]) + TRANSFORM.memory[parsed_key] = moriginals[parsed_key]; + else + delete TRANSFORM.memory[parsed_key]; + + if (originals[parsed_key]) + data[parsed_key] = originals[parsed_key]; + else + delete data[parsed_key]; + } + + /////////////////////////// + /// End of modification /// + /////////////////////////// } } else if (fun.name === '#concat') { if (Helper.is_array(template[key])) { result = []; - template[key].forEach(function(concat_item) { + template[key].forEach(function (concat_item) { var res = TRANSFORM.run(concat_item, data); result = result.concat(res); }); @@ -332,7 +398,7 @@ } else if (fun.name === '#merge') { if (Helper.is_array(template[key])) { result = {}; - template[key].forEach(function(merge_item) { + template[key].forEach(function (merge_item) { var res = TRANSFORM.run(merge_item, data); for (var key in res) { result[key] = res[key]; @@ -342,7 +408,7 @@ // necessary because #merge merges multiple objects into one, // and one of them may be 'this', in which case the $index attribute // will have snuck into the final result - if(typeof data === 'object') { + if (typeof data === 'object') { delete result["$index"]; // #let handling @@ -375,7 +441,7 @@ result = []; for (var index = 0; index < newData.length; index++) { // temporarily set $index - if(typeof newData[index] === 'object') { + if (typeof newData[index] === 'object') { newData[index]["$index"] = index; // #let handling for (var declared_vars in TRANSFORM.memory) { @@ -401,7 +467,7 @@ var loop_item = TRANSFORM.run(template[key], newData[index]); // clean up $index - if(typeof newData[index] === 'object') { + if (typeof newData[index] === 'object') { delete newData[index]["$index"]; // #let handling for (var declared_vars in TRANSFORM.memory) { @@ -487,12 +553,14 @@ } return result; }, - fillout: function(data, template, raw) { + fillout: function (data, template, raw) { // 1. fill out if possible // 2. otherwise return the original var replaced = template; // Run fillout() only if it's a template. Otherwise just return the original string + if (Helper.is_template(template)) { + var re = /\{\{(.*?)\}\}/g; // variables are all instances of {{ }} in the current expression @@ -528,7 +596,7 @@ } return replaced; }, - _fillout: function(options) { + _fillout: function (options) { // Given a template and fill it out with passed slot and its corresponding data var re = /\{\{(.*?)\}\}/g; var full_re = /^\{\{((?!\}\}).)*\}\}$/; @@ -627,14 +695,14 @@ $selected: [], $injected: [], $progress: null, - exec: function(current, path, filter) { + exec: function (current, path, filter) { // if current matches the pattern, put it in the selected array if (typeof current === 'string') { // leaf node should be ignored // we're lookin for keys only } else if (Helper.is_array(current)) { - for (var i=0; i 0) { - SELECT.$selected.sort(function(a, b) { + SELECT.$selected.sort(function (a, b) { // sort by path length, so that deeper level items will be replaced first // TODO: may need to look into edge cases return b.path.length - a.path.length; - }).forEach(function(selection) { - //SELECT.$selected.forEach(function(selection) { + }).forEach(function (selection) { + //SELECT.$selected.forEach(function(selection) { // parse selected var parsed_object = TRANSFORM.run(template, selection.object); @@ -764,7 +832,7 @@ // update selected object with the parsed result selection.object = parsed_object; }); - SELECT.$selected.sort(function(a, b) { + SELECT.$selected.sort(function (a, b) { return a.index - b.index; }); } else { @@ -779,7 +847,7 @@ delete Boolean.prototype.$root; return SELECT; }, - transform: function(obj, serialized) { + transform: function (obj, serialized) { SELECT.$parsed = []; SELECT.$progress = null; /* @@ -808,11 +876,11 @@ root = data; if (SELECT.$selected && SELECT.$selected.length > 0) { - SELECT.$selected.sort(function(a, b) { + SELECT.$selected.sort(function (a, b) { // sort by path length, so that deeper level items will be replaced first // TODO: may need to look into edge cases return b.path.length - a.path.length; - }).forEach(function(selection) { + }).forEach(function (selection) { // parse selected var parsed_object = TRANSFORM.run(selection.object, data); // apply the result to root @@ -822,7 +890,7 @@ // update selected object with the parsed result selection.object = parsed_object; }); - SELECT.$selected.sort(function(a, b) { + SELECT.$selected.sort(function (a, b) { return a.index - b.index; }); } else { @@ -840,53 +908,53 @@ }, // Terminal methods - objects: function() { + objects: function () { SELECT.$progress = null; if (SELECT.$selected) { - return SELECT.$selected.map(function(item) { return item.object; }); + return SELECT.$selected.map(function (item) { return item.object; }); } else { return [SELECT.$selected_root]; } }, - keys: function() { + keys: function () { SELECT.$progress = null; if (SELECT.$selected) { - return SELECT.$selected.map(function(item) { return item.key; }); + return SELECT.$selected.map(function (item) { return item.key; }); } else { if (Array.isArray(SELECT.$selected_root)) { - return Object.keys(SELECT.$selected_root).map(function(key) { return parseInt(key); }); + return Object.keys(SELECT.$selected_root).map(function (key) { return parseInt(key); }); } else { return Object.keys(SELECT.$selected_root); } } }, - paths: function() { + paths: function () { SELECT.$progress = null; if (SELECT.$selected) { - return SELECT.$selected.map(function(item) { return item.path; }); + return SELECT.$selected.map(function (item) { return item.path; }); } else { if (Array.isArray(SELECT.$selected_root)) { - return Object.keys(SELECT.$selected_root).map(function(item) { + return Object.keys(SELECT.$selected_root).map(function (item) { // key is integer return '[' + item + ']'; }); } else { - return Object.keys(SELECT.$selected_root).map(function(item) { + return Object.keys(SELECT.$selected_root).map(function (item) { // key is string return '["' + item + '"]'; }); } } }, - values: function() { + values: function () { SELECT.$progress = null; if (SELECT.$selected) { - return SELECT.$selected.map(function(item) { return item.value; }); + return SELECT.$selected.map(function (item) { return item.value; }); } else { return Object.values(SELECT.$selected_root); } }, - root: function() { + root: function () { SELECT.$progress = null; return SELECT.$selected_root; }, @@ -894,13 +962,13 @@ // Native JSON object override var _stringify = JSON.stringify; - JSON.stringify = function(val, replacer, spaces) { + JSON.stringify = function (val, replacer, spaces) { var t = typeof val; if (['number', 'string', 'boolean'].indexOf(t) !== -1) { return _stringify(val, replacer, spaces); } if (!replacer) { - return _stringify(val, function(key, val) { + return _stringify(val, function (key, val) { if (SELECT.$injected && SELECT.$injected.length > 0 && SELECT.$injected.indexOf(key) !== -1) { return undefined; } if (key === '$root' || key === '$index') { return undefined; @@ -919,6 +987,26 @@ } }; + /////////////////////////////// + /// Modified by Jakub Mifek /// + /////////////////////////////// + + var setTemplates = function(templates) { + for(let template in templates) + TRANSFORM.templates[template] = templates[template]; + + return { + select: SELECT.select, + inject: SELECT.inject, + transform: TRANSFORM.transform, + setTemplates: setTemplates, + }; + } + + /////////////////////////// + /// End of modification /// + /////////////////////////// + // Export if (typeof exports !== 'undefined') { var x = { @@ -930,6 +1018,7 @@ inject: SELECT.inject, select: SELECT.select, transform: TRANSFORM.transform, + setTemplates: setTemplates, // Modified by Jakub Mifek }; if (typeof module !== 'undefined' && module.exports) { exports = module.exports = x; } exports = x; @@ -938,6 +1027,7 @@ select: SELECT.select, inject: SELECT.inject, transform: TRANSFORM.transform, + setTemplates: setTemplates, // Modified by Jakub Mifek }; } }()); From 69a0386b676072f04b82021dbabe9a79b4f68a4b Mon Sep 17 00:00:00 2001 From: Jakub Mifek Date: Thu, 30 Aug 2018 10:50:50 +0200 Subject: [PATCH 2/5] Update st.js - Optional function ## Optional Optional is a function that can be called on a key as follows: ```js let data = { items: [1, 2, [3, 4], [5, 6, [7, 8] ] ] } let template = { "{{#optional items}}": [{ "{{#if items.length > 5}}": "{{items}}" }] } ST.transform(template, data) ``` This results in following JSON: ```json { } ``` This is because, the value attached to the key 'items' is empty. The functionality is meant to be same as #? operator just for more complicated situations. The key will not be used if the result is: - undefined - null - false - empty object - empty array --- st.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/st.js b/st.js index 4583cc6..47a7215 100644 --- a/st.js +++ b/st.js @@ -319,9 +319,17 @@ /////////////////////////////// /// Modified by Jakub Mifek /// - /////////////////////////////// + /////////////////////////////// + } else if (fun.name === '#optional') { + let ret = TRANSFORM.run(template[key], data); + + if(!ret || ret == null || ret == undefined || typeof ret === 'object' && Object.keys(ret).length === 0 || Helper.is_array(ret) && ret.length === 0) { + // We want to ignore these cases + } else { + result[fun.expression] = ret; + } } else if (fun.name === '#flatten') { - let arr = TRANSFORM.run(template[key], data); + let arr = TRANSFORM.run(template[key], data); result = []; if(Helper.is_array(arr)) { From 9dbc63851e37b9d79a9bec7fc304ad07a7d19f13 Mon Sep 17 00:00:00 2001 From: Jakub Mifek Date: Sat, 1 Sep 2018 10:04:10 +0200 Subject: [PATCH 3/5] Update st.js - Let in forcycle fixer upper There was a but in memory cleanup in #each when #let was used. This fixes it. --- st.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/st.js b/st.js index 47a7215..f4933e4 100644 --- a/st.js +++ b/st.js @@ -447,12 +447,15 @@ // Ideally newData should be an array since it was prefixed by #each if (newData && Helper.is_array(newData)) { result = []; + let originals = {}; // Modified by Jakub Mifek for (var index = 0; index < newData.length; index++) { // temporarily set $index if (typeof newData[index] === 'object') { newData[index]["$index"] = index; // #let handling + for (var declared_vars in TRANSFORM.memory) { + originals[declared_vars] = newData[index][declared_vars]; // Modified by Jakub Mifek newData[index][declared_vars] = TRANSFORM.memory[declared_vars]; } } else { @@ -478,8 +481,11 @@ if (typeof newData[index] === 'object') { delete newData[index]["$index"]; // #let handling - for (var declared_vars in TRANSFORM.memory) { - delete newData[index][declared_vars]; + for (var declared_vars in TRANSFORM.memory) { // Modified by Jakub Mifek + if(originals[declared_vars]) + newData[index][declared_vars] = originals[declared_vars]; + else + delete newData[index][declared_vars]; } } else { delete String.prototype.$index; From db36c6d6502a4cd4197930f9d3a9a0a28d791f61 Mon Sep 17 00:00:00 2001 From: Jakub Mifek Date: Mon, 11 Mar 2019 08:22:42 +0100 Subject: [PATCH 4/5] Update - $this variable in loop $this variable references current context within a loop cycle. --- st.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/st.js b/st.js index f4933e4..7d95939 100644 --- a/st.js +++ b/st.js @@ -449,9 +449,10 @@ result = []; let originals = {}; // Modified by Jakub Mifek for (var index = 0; index < newData.length; index++) { - // temporarily set $index + // temporarily set $index and $this if (typeof newData[index] === 'object') { newData[index]["$index"] = index; + newData[index]["$this"] = newData[index]; // #let handling for (var declared_vars in TRANSFORM.memory) { @@ -477,9 +478,10 @@ // run var loop_item = TRANSFORM.run(template[key], newData[index]); - // clean up $index + // clean up $index and $this if (typeof newData[index] === 'object') { delete newData[index]["$index"]; + delete newData[index]["$this"]; // #let handling for (var declared_vars in TRANSFORM.memory) { // Modified by Jakub Mifek if(originals[declared_vars]) From 148a806a34787a108af82f83775000171ba3b674 Mon Sep 17 00:00:00 2001 From: Aneesh Dalvi Date: Mon, 18 Mar 2019 23:33:19 -0400 Subject: [PATCH 5/5] =?UTF-8?q?Add=20{{#let*}},=20which=20mimics=20Scheme?= =?UTF-8?q?=E2=80=99s=20let*=20form.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This saves the hassle of having nested let’s. Also merge let/let* results with whatever was there before, instead of over-writing. --- st.js | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/st.js b/st.js index 4583cc6..7541b43 100644 --- a/st.js +++ b/st.js @@ -360,7 +360,10 @@ } // 3. Pass it into TRANSFORM.run - result = TRANSFORM.run(real_template, data); + var exp = TRANSFORM.run(real_template, data); + for (var e in exp) { + result[e] = exp[e]; + } // 4. Remove the data from memory for (var parsed_key in parsed_keys) { @@ -379,6 +382,51 @@ /// End of modification /// /////////////////////////// } + } else if (fun.name === '#let*') { + if (Helper.is_array(template[key]) && template[key].length == 2) { + var defs = template[key][0]; + var real_template = template[key][1]; + + // 1. Parse the first item to assign variables + var parsed_keys = {}; + let originals = {}; + let moriginals = {}; + for (var def in defs) { + var dd = {}; + dd[def] = defs[def] + var x = TRANSFORM.run(dd, data); + parsed_keys[def] = x[def]; + originals[def] = data[def]; + data[def] = parsed_keys[def]; + } + + // 2. modify the data + for (var parsed_key in parsed_keys) { + moriginals[parsed_key] = TRANSFORM.memory[parsed_key]; + TRANSFORM.memory[parsed_key] = parsed_keys[parsed_key]; + originals[parsed_key] = data[parsed_key]; + data[parsed_key] = parsed_keys[parsed_key]; + } + + // 3. Pass it into TRANSFORM.run + var exp = TRANSFORM.run(real_template, data); + for (var e in exp) { + result[e] = exp[e]; + } + + // 4. Remove the data from memory + for (var parsed_key in parsed_keys) { + if (moriginals[parsed_key]) + TRANSFORM.memory[parsed_key] = moriginals[parsed_key]; + else + delete TRANSFORM.memory[parsed_key]; + + if (originals[parsed_key]) + data[parsed_key] = originals[parsed_key]; + else + delete data[parsed_key]; + } + } } else if (fun.name === '#concat') { if (Helper.is_array(template[key])) { result = [];