📜 ⬆️ ⬇️

Template Description Language Snakeskin

Snakeskin

This is Frank, a snake-cowboy who loves templates.




Hello! I want to talk about my development - the programming language of text templates "Snakeskin". The project is more than three years old, with all childhood illnesses, I suppose, he successfully recovered from his illness (and was cured), so I want to share the result.


Demo


Main repository


Documentation


Plug-ins for Gulp, Grunt, Webpack and others


Gitter - here you can ask any question


A bit of history


When I worked at Yandex (four years ago), one of the main topics for heated discussions at coffee points we had with colleagues were template makers: we discussed the advantages and disadvantages of existing solutions, some even developed their own.


In the department, TemplateToolkit2 was the main one - a template engine popular in particular among Perl developers, and the client used the simplest MicroTemplate (by John Rezig). Even at that time, XSLT-like engines were actively forcing, but for a number of reasons (discussion of which is beyond the scope of this article) they did not fit us. From time to time we experimented with others: Handlebars, Dust, Closure Templates, plus our bikes, of course ... All this led to the presence of a whole zoo of template engines in the project.


My favorite was Google Closure Templates: it was close to me as a programmer, because the template was positioned as a function that simply returns a string, plus very good features for those times; but I was very upset by the need to edit the Java code in order to add some banal filter, and the translation speed was not so hot (it was really felt).


And I wanted to make my own Closure Templates with blackjack and whores : it is natural that it was written in JS and, as a result, open to modifications without having to know Java. Plus, I liked the template inheritance model, based on static blocks, which I spied in Django Templates (hence the name - reference to Python) - this was the basis of the existing inheritance system.


The prototype I sketched for three days: it was a terrible hard-code on regulars in seven hundred lines of code. I played a little with the result, shared it with my colleagues, got some feedback, but decided to move on. Refactor this case, corrected bugs, added new ones opportunities. After a week of development, I released version 2 - in fact, the same hardcode on regulars, but more stable and fichastey. It could already be used.


After working for a while with the result and releasing a dozen updates, I rubbed my hands and sat at the computer with the thought “It's time to make things right”, and a month later I released the 3rd version: I threw out the hardcode, rewrote the code on ES6 ( at that time there were no normal translators, so I also wrote my own translator (again, with terrible hardcodes on regulars - yes, I love regulars)), added the construction of the tree during parsing and many new features.


The version came out stable, powerful and, in fact, was a Closure Templates on steroids. I was pleased with the result and began to use Snakeskin in my personal projects, occasionally releasing new updates and patches.


A little later, I met HAML and Jade, I liked their approach to syntax, and it was decided to add something similar to Snakeskin (the result of this decision was the Jade-like syntax). After several months of active development, I released the fourth version, which became a truly milestone in the history of the language and determined its further development. The fifth and sixth were nothing more than a modification of the fourth version, but with the breaking changes that were necessary, and since I chose SemVer as the versioning pattern for Snakeskin, I had to use the major version.


I used SS6 for quite a long time and in various projects, my friends and colleagues also began to use it - as a result, after some time, a list of complaints had accumulated - not very long, but still: there were many features, they appeared in a rather chaotic manner , and “conflicts” between directives became visible. The reason for this was the absence of any initial specification of the language - the development was proceeding as the appearance of the "hoteles".


I decided that it was impossible to continue living like this - you need to standardize everything and remove garbage. The development dragged on for a year and a half (of which, however, the active was a maximum of six months - the lack of free time had an effect), but in the end, the most stable and well thought out release of Snakeskin turned out: version 7; and i'm sincerely proud of him.


First look


The most suitable for Snakeskin seems to me the definition that it is just “sugar” over JS, like CoffeeScript or TypeScript, but it has a rather narrow specialization: writing templates. Of course, it is possible to write on the SS at least the entire application as a whole, but it will be, heh, not very convenient. SS is intended for use with the primary language - mainly JS:


select.ss


- namespace select - template main(options) < select - forEach options => el < option value = ${el.value} {el.label} 

select.js


 import { select } from 'select.ss'; class Select { constructor(options) { this.template = select.main(options); } } const newSelect = new Select([{value: 1, label: ''}, {value: 2, label: ''}]) 

Here, the main file on JS is connected as a module file on Snakeskin (for example, a plug-in for WebPack gives such a seamless integration). From it we import the namespace select , and declare the class Select . When creating the Select instance, we execute the main function (into which the main template was translated), and assign the result of its work to the template property - for newSelect it will be like this:


 <select> <option value="1">  </option> <option value="2">  </option> </select> 

As you can see, SS is translated to JS (if specifically, to ES5), which is then very easy to use in the main code.


If we talk about why I started doing Snakeskin - the main motivation was the desire to have a template language with powerful code reuse capabilities that can be used on the server and on the client at the same time without having to change the template code. Then, of course, new requirements for the language began to appear and ideas in the style of “should I add such a feature here” - all this, creatively and logically meaningful, and made Snakeskin the way you see it now.


One of the “time requirements”, for example, was the need for seamless integration with frameworks and libraries that have their own template language (like Angular or React — well, I prefer Vue ) —and now Snakeskin is doing great.


An example of using SS to create Angular templates:


 - namespace myApp - template main() < label Name: < input type = text | ng-model = yourName | placeholder = Enter a name here < hr < h1 Hello {{yourName}}! 

The result of the main


 <label> Name: </label> <input type="text" ng-model="yourName" placeholder="Enter a name here"> <hr> <h1> Hello {{yourName}}! </h1> 

Snakeskin significantly reduces the amount of code, allows you to reuse layout elements (through inheritance, composition, impurities, etc.), while Angular implements data-binding. From a technical point of view, SS generates a template that then uses Angular.


Where to use



 'use strict'; const http = require('http'); const ss = require('snakeskin'); //    //     - const tpls = ss.compileFile('./myTpls.ss'); http.createServer((req, res) => { res.writeHead(200, {'Content-Type': 'text/html'}); //   foo    res.write(tpls.foo('bar', 'bla')); res.end(); }).listen(8888); 

Of course, in practice it will be a server-side framework like Express or Koa, but it doesn’t matter. Also, templates can (and preferably) be pre-translated using a plugin for Gulp or Grunt and include the resulting files, well, or, as above, use a WebPack.



Overview of the language


Here I will go over the basic concepts, and if you have questions, welcome to the documentation or to Gitter .


Main


Templates


As already mentioned several times, the Snakeskin template after the broadcast becomes a JavaScript function:


 - namespace myApp - template main() Hello world! 

after the broadcast will turn into something like:


 if (exports.myApp === 'undefined') { var myApp = exports.myApp = {}; } exports.myApp.main = function main() { return 'Hello world!'; } 

Of course, this is a simplified code, but in general it looks like this.


Syntax


SS supports 2 different types of syntax:



 {namespace myApp} {template main(name = 'world')} Hello {name}! {/template} 

This mode is useful for generating text with control spaces, for example Python code Markdown.


Note : to generate text where curly brackets are often used, there is a special mechanism in SS.



 - namespace myApp - template main(name = 'world') Hello {name}! 

The main advantages of this syntax are brevity and clarity. Ideal for generating XML-like structures.


SS also supports mixed syntax:


 - namespace myApp {template hello(name = 'world')} Hello {name}! {/template} - template main(name) += myApp.hello(name) 

Learn more about syntax and its types .


Code-reuse tools


Inheritance


In SS, each template is a class, that is, it has methods and properties, and it can be inherited from another template. A child template can override inherited parent methods and properties and add new ones.


An example of template inheritance .


 - namespace myApp ///  sayHello  base ///  SS      , ///     --    , ///     - block base->sayHello(name = 'world') Hello {name}! - template base() - doctype < html < head ///   head ///     , ///        - block head < title ///   `title`,    - title = ' ' ? < body - block body ///   sayHello += self.sayHello() ///    - block child->sayHello() ///   sayHello  - super Hello people! ///    - block child->go() Let's go! ///  child   base - template child() extends myApp.base ///   - title = ' ' ///    - block body - super += self.go() 

The result of the child :


 <!DOCTYPE html> <html> <head> <title> </title> </head> <body> Hello world! Hello people! Let's go! </body> </html> 

When inheriting a template, the input parameters, the decorators of the template, and various modifiers are also inherited - here you can read more.


Composition


Since all the templates in Snakeskin are functions, then, naturally, any template can call any other template: for this is the call directive.


 - namespace myApp - template hello(name = 'world') Hello {name}! - template main(name) - call myApp.hello(name) ///    += myApp.hello(name) 

Pattern as value


In Snakeskin, you can assign a template to a variable or property of an object, pass it as an argument to a function, and so on.


 - namespace myApp - template wrap(content) < .wrapper {content} - template main(name) += myApp.wrap() < .hello Hello world! 

main execution result


 <div class="wrapper"> <div class="hello"> Hello world! </div> </div> 

Modules


Each file written in Snakeskin is a module: global variables are encapsulated in it, and all templates are exported. Modules can connect other modules using the include directive.


Thus, you can easily divide the code into logical parts, create plug-in libraries (and even, perhaps, frameworks), and generally follow the rule of “divide and conquer”.


math.ss


 - namespace math - template sum(a, b) {a + b} 

app.ss


 - namespace myApp - include './math' - template main() 1 + 2 = += math.sum(1, 2) 

Result of calling myApp.main


 1 + 2 = 3 

Nice buns



 - namespace myApp - template main((str|trim), name = ('World'|lower)) - var a = {foo: 'bar'} |json 

In SS out of the box there are many useful built-in filters , and if they are not enough, then add your own - elementary .



 - namespace myApp - import { readdirSync } from 'fs' ///    ./foo - template main((str|trim), name = ('World'|lower)) - forEach readdirSync('./foo') => dirname {dirname} 


 - namespace myComponent - template render() < .hello {{ this.name }} 

 import React from 'react'; import { myComponent } from './myComponent.ss'; const Foo = React.createClass({ render: myComponent.render }); 

For such seamless integration, when a template returns an element created with React, use a Webpack plugin with the jsx flag jsx .



 - namespace myApp - template main() < .hello /// hello__wrap < .&__wrap /// hello__cont < .&__cont 


 - namespace myApp - template main(area) < ${area ? 'textarea' : 'input'}.b-${area ? 'textarea' : 'input'}    

Depending on the value of the area result will look either like this (if area == true ):


 <textarea class="b-textarea">    </textarea> 

either way (with area == false ):


 <input class="b-input" value="  "> 


 - namespace demo - import Typograf from 'typograf' /// -    JS   SS - template typograf(params) - return () => target - return () => - return new Typograf(params).execute(target.apply(this, arguments)) ///   index     - @typograf({lang: 'ru'}) - template index()  -  ! 


 - namespace myApp - async template main(db) - forEach await db.getData() => el {el} - template *foo(data) - for var i = 0; i < data.length; i++ {data.value} - if i % 1e3 === 0 - yield 

Also look in the section " Directives for asynchronous work ."



Conclusion


I sincerely hope that Snakeskin interested you, you will try it and will enjoy using it.


I express my sincere appreciation to trikadin for help with writing and editing the article. By the way, this guy works as a front-end in Foodadil , and now they are implementing Snakeskin as their main web template language. He says that he is happy and does not understand how he lived without SS before :)


I also want to thank the team of the javascript.ru forum for ideas on language development and support.


About the bugs found, write to the Issues on the GitHub-e project, and ask the questions that arise either here in the comments or in Gitter 'e - I will always be happy to answer and explain.


Good luck!


')

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


All Articles