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.
Plug-ins for Gulp, Grunt, Webpack and others
Gitter - here you can ask any question
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.
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.
'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.
Static site generation : plugins have the option to call the compiled template at the time of translation and return the result of its work. The plugin will calculate the main template itself , or you can specify it explicitly.
<script>
, or as a module (using Webpack, Browserify, RequireJS, or any other module management system).Here I will go over the basic concepts, and if you have questions, welcome to the documentation or to Gitter .
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.
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 .
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.
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)
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>
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
Rich set of built-in directives
In Snakeskin, there are: directives semantically equivalent to operators in JS, such as if
, for
, var
, return
, etc; directives specific to the template language and simplifying the markup of XML-like structures: tag
, attr
, doctype
, comment
and others; directives for asynchronous template generation: await
, yield
, parallel
, waterfall
; and many others .
Hostess for a note : Snakeskin is still not JavaScript, so some directives in the nuances may not work the way similar operators work in JS; for example, variables declared via var
a block scope (this is how the let
from ES2015 works). In the with
directive, the architectural flaws of the operator of the same name from JS are eliminated altogether, which makes its use within the SS quite good practice, and simply simplifies and speeds up code writing.
Filters are present in one form or another in most template engines, but in SS they are part of the core language, so you can use them literally everywhere: when creating variables, in cycles, when declaring block and template arguments, in directives ... In general, in general everywhere.
- 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 .
Bidirectional modular integration with JS
You can import SS templates into a JS program, and Snakeskin can import JavaScript modules (using the import
directive), supporting all the main types of modules: umd , amd , commonjs , native and global .
- namespace myApp - import { readdirSync } from 'fs' /// ./foo - template main((str|trim), name = ('World'|lower)) - forEach readdirSync('./foo') => dirname {dirname}
Powerful template localization mechanism
Availability of special tools for generating code of other template languages
For example, SS plug-ins for building systems (Gulp and others) have a mode in which the Snakeskin template immediately returns React.Element.
Template generation for React
- 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
.
Still worth a look at the section " Working with whitespace characters ."
Support for sticky links (links to parent class)
The mechanism is similar to that used in CSS preprocessors. Convenient if you adhere to the BEM approach. The principle of operation is the following: if during the tag declaration you specify the name of a class that begins with the symbol “&”, then it will be replaced with the closest parent class that was declared without this symbol:
- namespace myApp - template main() < .hello /// hello__wrap < .&__wrap /// hello__cont < .&__cont
Many Snakeskin directives support an interpolation mechanism, i.e., throwing dynamic template values ​​into directives, for example:
- 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=" ">
Thanks to the decorators' mechanism, it is easy to integrate additional modules in Snakeskin - for example, a typographer:
- 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() - !
SS allows you to create generator templates and async templates, plus contains a number of directives for the convenient use of the popular async library.
- 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 ."
Customizable rendering
Out of the box, Snakeskin supports four rendering modes: string (by default), Buffer, DocumentFragment, and JSX; You can also add your renderer — for example, to generate a custom Virtual DOM.
Informative error messages
Snakeskin translator has a powerful code debugger that helps to find the most syntax and logical errors when translating templates.
Support for all major build systems
Good codebase
Snakeskin is completely written in ES2015, contains a large number of tests and passes the most rigorous verification of Google Closure Compiler in ADVANCED
mode. The code is well documented in accordance with Google's JSDoc standard.
Detailed and clear documentation
Which, by the way, is written on Snakeskin .
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