Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 132 lines (87 sloc) 5.161 kB
778b73d @creationix Add initial readme text
authored
1 # Grain Templating System
2
e014129 @creationix After a long break, working on this again...
authored
3 All template languages have 5 parts in common.
778b73d @creationix Add initial readme text
authored
4
5 - Static content
6 - Parameters
7 - Dynamic content
8 - Loops/Conditionals
9 - Asynchronous parts
10
11 Ok, maybe the last one isn't that common, but in a NodeJS world where nothing blocks it's required for many use cases. Partial templates require usually that another external resource get loaded and compiled. If this resource is loaded over some IO then it's an asynchronous operation. Also it would be nice to be able to stream content to the browser as information is known.
12
623563b @creationix Implement the first version of the helper lib.
authored
13 Grain is both a spec for interfaces between template languages and a helper library to help write compilers that follow the spec. Included is a sample template language called `Corn` that implements the spec using `grain`.
14
e014129 @creationix After a long break, working on this again...
authored
15 ## Inversion of control
16
17 One way to handle the async nature of retrieving data is to pre-calculate all the data that's needed for a template to render, and then as the last step pass it to the template function which returns the output text in a single sync function call. This works and is very simple from the point of view of the author of the template language, but it's a lot of burden on the person using the template language.
18
19 My proposed method is to pass the template function an hash of values and data providing functions. The template language will call the data providers as it needs data. If they are async functions, then it will hold that place in the template and defer rendering of that part till the callback comes back with the data. Then the template will output it's result after the pending function calls have finished.
20
21 ## Template language compiler module interface
22
23 All grain engines need to comply with this simple interface to allow interoperability between frameworks and projects.
24
25 ### Compiler
26
27 The module itself is a function that takes in template source code as text and returns compiled function that renders that template on demand.
28
29 function compile(text) -> function fn(locals, callback|stream)
30 module.exports = compile
31
373ff6c @creationix Linkify the link.
authored
32 The returned function accepts two arguments, they are `locals` and then either a `callback` function or a `stream` instance. The properties of `locals` are available within the template as local variables, usually using `with`.
e014129 @creationix After a long break, working on this again...
authored
33
34 ### Callback
35
36 If a callback is provided it's interface will be:
37
38 function callback(err, result) { ... }
39
40 Where `err` will contain an instance of `Error` if an exception happens while rendering, otherwise, `err` will be falsy and result will contain the final output of the entire template.
41
42 ### Stream
43
44 If a stream if given instead, it will `emit("data", chunk)` as chunks of output are finished. It will `emit('end')` when done, and `emit('error', err)` where there is an exception.
45
46 ### Currying
47
48 The compile function will act as if it's a curried function and accept the locals and callback|stream parameters directly.
49
50 function callback(text, locals, callback|stream)
51
52 ### Helpers
53
54 The module itself will also have a `helpers` object that gets mixed into every locals parameter. A simple way to implement this is on the first line of the generated code in `fn` do:
55
56 locals.__proto__ = module.exports.helpers;
57
58 ## Sample code
59
73c7421 @creationix Inline the examples.
authored
60 Suppose this simple template language where `@foo` is the variable `foo` and `@bar()` calls the data provider `bar()` and inserts the result inline.
61
953df07 @creationix Polish grain and make corn available via npm
authored
62 I would require my compiler like this (corn is included as a sample):
73c7421 @creationix Inline the examples.
authored
63
953df07 @creationix Polish grain and make corn available via npm
authored
64 var compiler = require('grain/corn');
73c7421 @creationix Inline the examples.
authored
65
66 Now let's suppose this simple template:
67
68 var template = "Hello @planet, my name is @name() and I am @age() years old.";
69
70 With this sample data:
71
72 var data = {
73 // Value
74 planet: "world",
75 // Async getter
76 name: function name(callback) {
77 process.nextTick(function () {
78 callback(null, "Tim Caswell");
79 });
80 },
81 // sync getter
82 age: function age() {
83 return 28;
84 }
85 };
86
87 To compile the template it's simply:
88
89 var fn = compiler(template);
90
91 And then to render it do:
92
93 fn(data, function (err, text) {
94 if (err) {
95 console.log("ERROR " + err.stack);
96 return;
97 }
98 console.log("OUTPUT " + text);
99 });
100
101 Or to render with a stream:
102
103 var stream = new process.EventEmitter();
104 fn(data, stream);
105 stream.addListener('data', function (data) {
106 console.log("DATA " + JSON.stringify(data.toString()));
107 });
108 stream.addListener('end', function () {
109 console.log("END");
110 });
111 stream.addListener('error', function (err) {
112 console.log("ERROR " + err.stack);
113 });
114
115 Also you can compile and render it in one shot:
116
117 compiler(template, data, callback);
118
119 If I wanted a helper that did partials, I would add it like this:
120
121 var fs = require('fs');
122 compiler.helpers = {
123 partial: function (filename, data, callback) {
953df07 @creationix Polish grain and make corn available via npm
authored
124 fs.readFile(filename, 'utf8', function (err, text) {
73c7421 @creationix Inline the examples.
authored
125 if (err) { callback(err); return; }
126 compiler(text, data, callback);
127 });
128 }
129 }
130
131 And then it would be available within the template as `partial()` without me having to pass it into the data hash every time.
Something went wrong with that request. Please try again.