📜 ⬆️ ⬇️

We write your JavaScript template engine

A great variety has been written on the topic of template-writing articles, including here, in Habré.
Previously, it seemed to me that it would be very difficult to do something “on my knee”.
But it so happened that they sent me a test task.
Write, they say, JavaScript template engine, according to such a scenario, then you come to the interview.
The demand, of course, was excessive, and at first I decided to just ignore it.
But from sports interest decided to try.
It turned out that not everything is so difficult.

Actually, if you're interested, then under the cut are some notes and conclusions on the creation process.

For those who just look: the result , the cat .
')


Given:

The original template is JS String (), and the data is JS Object ().
Blocks of the form {% name%} body {% /%} , unlimited nesting is possible .
If the name value is a list , all elements are displayed, otherwise, if not undefined, one element is displayed.
Type substitutions : {{name}} .
In blocks and substitutions it is possible to use points as a name, for example {{.}} Or {%.%} , Where the point will be the current element of the top -level object.
There are more comments - this is {# any comment w \ wo multiline #} .
For the values ​​themselves, filters are possible, specified by a colon: {{.: Trim: capitalize ...}} .

It should work as:

var str = render (tpl, obj); 


Prove:
+1 to self-esteem .

UPD 2 : I’ll say at once to “clearly understand” what is there and why, you need to start doing it, preferably with debugger.
UPD 3 : It is disassembled "on the fingers." There is another place to “optimize”. But it will be much less obvious.

Let's get started

Because the initial pattern is a string, then you can enjoy the benefits of regulars.

For starters, you can remove comments so that they do not shine:

 // to cut the comments tpl = tpl.replace ( /\{#[^]*?#\}/g, '' ); 


Hint: [^] means any character, * - as many times as you like.

Now you can think about how we parse the "clean" result.
Since the blocks are possible nested, I suggest keeping everything in the form of a tree.
At each level of the tree will be JS Array (), whose elements may contain a similar structure.

To create this array you need to separate flies from cutlets.
For this, I used String.split () and String.match () .

We will also need a deep search by a string val name inside the object obj .

Applied option getObjDeep:
 var deeps = function (obj, val) { var hs = val.split('.'); var len = hs.length; var deep; var num = 0; for (var i = 0; i < len; i++) { var el = hs[i]; if (deep) { if (deep[el]) { deep = deep[el]; num++; } } else { if (obj[el]) { deep = obj[el]; num++; } } } if (num == len) { return deep; } else { return undefined; } }; 



And right here I will say THANK YOU, subzey for greedy quantificator fix .

UPD 1 : thank you lynx1983 for Issue # 2 .

So, we divide the line into parts parts and the elements of matches:

 //   : // , , , // ,   ,    var ptn = /\{\%\s*[a-zA-Z0-9._/:-]+?\s*\%\}/g; //   var parts = tpl.split (ptn); //   var matches = tpl.match (ptn); 


For debriefing, we need two arrays.
In one we will store the blocks, in the other there will be the current element from the loop by matches.

 //   var blocks = []; //  var curnt = []; if( matches ){ // .. .. null var len = matches.length; for ( var i = 0; i < len; i++ ) { //  {%  %},    trim var str = matches[i].replace (/^\{\%\s*|\s*\%\}$/g, ''); if (str === '/') { // finalise block // ... } else { // make block // ... } // ... 


Here blocks is the final array with selected blocks, and curnt is an array with the current nesting.

At each step of the loop, we determine that now at str, the beginning of the block or the end.
If the beginning of the block, i.e. str! == '/' , then create a new element and push it into an array.
And still push it in curnt, since we need to understand at what level we are.
Along the way, we write the lines themselves into a block.
Accordingly, if we have an empty curnt, then we are at the zero level of the tree.
If curnt is not empty, then you need to put the last curnt element in the nested element.

 //    var cln = curnt.length; if (cln == 0) { // ..   ,         blocks.push ( struct ); //   ,    curnt.push ( struct ); } else { //    nested    curnt[cln - 1].nest.push ( struct ); //    ""      curnt var last = curnt[cln - 1].nest.length - 1; curnt.push ( curnt[cln - 1].nest [ last ] ); } 


Appropriately, each element of the array is a minimum:

 var struct = { //  obj   cnt: deeps( obj, str ), //   nest: [], //      be4e: parts[ i + 1 ], // str -- ,     // cnt -- -,       af3e: { cnt: null, str: '' } }; 


Because we may have a situation when there is something else after the block, then here af3e.str should be a line immediately following {% /%} of the current block. We will put down all the necessary links at the time of the completion of the block, so more clearly.
At the same time, we delete the last element of the curnt element.

 if (str === '/') { //   curnt //   //    curnt [cln - 1].af3e = { cnt: ( curnt [ cln - 2 ] ? curnt [ cln - 2 ].cnt : obj ), str: parts[ i + 1 ] }; curnt.pop(); 


Now we can assemble a one-dimensional array, in which there will be all the necessary substrings with their current obj.
To do this, you need to "parse" the resulting blocks, given that there may be lists.
It will take a bit of recursion, but in general it will not be so difficult.

 //        var stars = [ [ parts[0], obj ] ]; parseBlocks( blocks, stars ); 


Approximate view parseBlocks ()
 var parseBlocks = function ( blocks, stars ) { var len = blocks.length; for (var i = 0; i < len; i++) { var block = blocks [i]; //    obj   if (block.cnt) { var current = block.cnt; //   switch ( Object.prototype.toString.call( current ) ) { //     case '[object Array]': var len1 = current.length; for ( var k = 0; k < len1; k++ ) { //   stars       stars.push ( [ block.be4e, current[k] ] ); //    parseBlocks( block.nest, stars ); } break; //     case '[object Object]': for (var k in current) { if (current.hasOwnProperty(k)) { //   stars       stars.push ( [ block.be4e, current[k] ] ); //    parseBlocks( block.nest, stars ); } } break; //       ,    default: stars.push ( [ block.be4e, current ] ); parseBlocks( block.nest, stars ); } //   stars ,      stars.push ( [ block.af3e.str, block.af3e.cnt ] ); } } }; 



Next, we parse the resulting stars element by element and, putting the result in a string, we get the final result:

 var pstr = []; var len = stars.length; for ( var i = 0; i < len; i++ ) { pstr.push( parseStar ( stars[i][0], stars[i][1] ) ); } // : return pstr.join (''); 


Approximate view parseStar ()
 var parseStar = function ( part, current ) { var str = ''; //   var ptn = /\{\{\s*.+?\s*\}\}/g; var parts = part.split (ptn); var matches = part.match (ptn); //    str += parts[0]; if (matches) { var len = matches.length; for (var i = 0; i < len; i++) { //     var match = matches [i]; //     trim var el = match.replace(/^\{\{\s*|\s*\}\}$/g, ''); var strel = ''; //      var deep = deeps( current, el ); //  ,      deep && ( strel += deep ); str += strel; } if (len > 0) { str += parts[ len ]; } } return str; } 



The code given is slightly less than the final result.
So, for example, I did not show what to do with the current element, if it is given a dot.
I also did not cite filter processing.
In addition, in the final version, I “from myself” added to the processing of situations when the “current element” or “value for” are functions.

But my goal was to show the very concept ...

And the result, as already mentioned at the beginning of the article, can be found here .
The final example is here .

I hope someone will come in handy.
Thanks for attention!

:)

Source: https://habr.com/ru/post/201684/


All Articles