📜 ⬆️ ⬇️

MaskJS, let's talk about the template engine, or a new bike



Here, finally got his hands to share with people one of the many bicycles (as they now call personal practices). Before Habrakat a couple of pros and cons of this solution:
Of the benefits:

Of the disadvantages:

If the topic is interesting -


Introduction

Let me immediately apologize for the grammar / style. I want to write everything very briefly, but at the same time, so that everyone understands me, and also correctly understood. You are not compilers who literally “understand” everything - but each with its own experience, judgment and so on. And since I have little experience in writing articles, as well as experience in writing in Russian, this throws away at the roots the hope of writing everything as I imagine it. But I will try, and you do not swear.
')
A bit of history

A couple of years ago, I needed a function like String.format('N: #{a} : #{b}',{a:1,b:2}) .
Soon I realized that using it for formatting html, I, for the most part, have my hands tied. Indeed, one would like formatting by condition, conditional visibility and lists. Looking at different template engines, I felt a wild disgust for a mixture of html and javascript , plus using with(){} and eval/new Function('') also did not please. Thinking that "I need something just a little bit" and decided to write for myself. Thus were born two tags list and visible and the format of the form #{a==1?1:-1} . It was not enough for me to only find these tags, then well String.format . And now, for a year and a half, this engine carried its service perfectly - it was faster and faster, more reliable and more reliable.

"And we are always a little ..."

Sadly, but we are so arranged - well, at least I am. I wanted it even faster, even more extensible and so that no further mash is html / javascript . At that moment, I knew for sure: I want custom tags - so that I don’t have to write the same html structure ten times without placeholders (so that after inserting it into the house, it will render) . And as soon as I sat down to finish the parser to handle additional tags, then, as luck would have it, the desire for art, which began to rattle, aroused, " Rewrite. Rewrite the code. It's the Ford Mondeo 93rd. And now even the millennium has long been. Yes and the syntax of the other templates is necessary. Have you not seen CoffeeScript Sass / Less ZenCoding? Rewrite, who they say, otherwise I will not let you fall asleep - just know . " And under this pressure, I could not resist. But still the main priority, it was the speed of the engine - no one needs a beautiful, but with 100 hp car at the start.

Getting down to business: Wood

Since we use our own syntax, we need to convert it into a tree. We have two types of nodes: Tag = {tagName:'someTagName', attr: { key:'value'}, nodes:[] } and Literal = {content:'Some String'} . And since after 1.5 years of using the old template engine, I never remember that I would substitute template data in the name of the tag, or attribute name, for simplicity of the template and analyzer, we make it possible to insert data only into them. Therefore, the nodes will be of the following form: Tag: = {tagName:'name', attr: { key:('value'||function)}, nodes:[] } and Literal: = {content:('Some String'||function)} . Where function is a function that substitutes the template data and it is only for those values ​​that require them. So we planted a tree, so far nothing complicated ( further it will not be more difficult either ).

Analyzer / Parser

Interesting moments:

The very parsing is painfully simple - it is 40 lines. (the parsing of attributes should not be made into a separate function, but the visibility would be lost)
Piece of code
 var current = T; for (; T.index < T.length; T.index++) { var c = T.template.charCodeAt(T.index); switch (c) { case 32: //" " continue; case 39: // "'"   T.index++; var content = T.sliceToChar("'"); //  sliceToChar  indexOf    'escape character' //  . indexOf  ,  ,      charCodeAt/charAt/[] if (~content.indexOf('#{')) content = T.serialize == null ? this.toFunction(content) : { template: content }; current.nodes.push({ content: content }); if (current.__single) { //   ,   ,   ;  div > ul > li > span > 'Some' if (current == null) continue; do (current = current.parent) while (current != null && current.__single != null); } continue; case 62: /* '>' */ current.__single = true; continue; case 123: /* '{' */ continue; case 59: /* ';' */ case 125: /* '}' */ if (current == null) continue; //   ; ,   } -    do(current = current.parent) while (current != null && current.__single != null); continue; } //    -  tag   var start = T.index; do(c = T.template.charCodeAt(++T.index)) while (c !== 32 && c !== 35 && c !== 46 && c !== 59 && c !== 123); /** while !: ' ', # , . , ; , { */ var tag = { tagName: T.template.substring(start, T.index), parent: current }; current.nodes.push(tag); current = tag; this.parseAttributes(T, current); //    ; > {,     T.index--; } 



Constructor

Having a tree, it’s not good to build an html string to insert into a document, you need to build a documentFragment right away (although function renderHtml left the function renderHtml too, just in case) . With this we greatly compensate the time spent on parsing.
The process itself is again trivial:
Part of the code
 function buildDom(node, values, container) { if (container == null) container = document.createDocumentFragment(); if (node instanceof Array) { for (var i = 0, length = node.length; i < length; i++) buildDom(node[i], values, container); return container; } if (CustomTags.all[node.tagName] != null) { var custom = new CustomTags.all[node.tagName](); for (var key in node) custom[key] = node[key]; custom.render(values, container); return container; } if (node.content != null) { //  container.appendChild(document.createTextNode(typeof node.content === 'function' ? node.content(values) : node.content)); return container; } var tag = document.createElement(node.tagName); for (var key in node.attr) { var value = typeof node.attr[key] == 'function' ? node.attr[key](values) : node.attr[key]; if (value) tag.setAttribute(key, value); } if (node.nodes != null) { buildDom(node.nodes, values, tag); } container.appendChild(tag); return container; } 



Custom controls

As can be seen from the above code, custom controls appear on the scene. If the constructor encounters a registered tag handler, it will create a handler object, make a shallow copy values ​​of attr and nodes and pass the assembly context to the render function. That is, our control should implement in its prototype the function .render(currentValues, container)

toFunction (templateString)

Actually, this is where the magic happens. True magic is not called, but I would like to. In fact, here we get the parts to insert our data, this

, we process them and we insert into template.

Well, it seems all lit up. In examples it is possible to see more interesting, in the same place implementation date, through controls. See also the sources for the examples page.


Examples
Sources



offtop:
In the arsenal, there are still a lot of “greats”, for example, IncludeJS - it looks like Require, but with a bunch of “goodies”. If there is interest in such things (these are not release libraries for production), I will also post it on the githab and write an article.

Good luck!

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


All Articles