Commits

Kazunori Ninomiya committed 7d937e2 Merge

Release v0.1.0

Comments (0)

Files changed (36)

 .DS_Store
-node_modules/*
+node_modules
+npm-debug.log
+tmp
 !.gitkeep
+{
+  "curly": true,
+  "eqeqeq": true,
+  "immed": true,
+  "latedef": true,
+  "newcap": true,
+  "noarg": true,
+  "sub": true,
+  "undef": true,
+  "boss": true,
+  "eqnull": true,
+  "node": true,
+  "es5": true,
+  "laxcomma": true
+}
+npm-debug.log
+.DS_Store
+.gitignore
+.git
+.npmignore
+tmp
+node_modules
 module.exports = function(grunt)
 {
   grunt.initConfig({
-    ect: {
-      top: {
-        options: {
-          root: 'test/ect',
-        },
-        src:  'page',
-        dest: 'test/page.html',
-        variables: {
-          title : 'Hello, world!',
-          id : 'main',
-          links: [
-            { name : 'Google', url : 'http://google.com/' },
-            { name : 'Facebook', url : 'http://facebook.com/' },
-            { name : 'Twitter', url : 'http://twitter.com/' }
-          ],
-          upperHelper : function (string) {
-            return string.toUpperCase();
+      jshint: {
+          all: [
+              'Gruntfile.js'
+            , 'tasks/*.js'
+            , '<%= nodeunit.tests %>'
+          ]
+        , options: {
+            jshintrc: '.jshintrc'
           }
-        }
-      },
-    },
+      }
+
+      // Before generating any new files, remove any previously-created files.
+    , clean: {
+        tests: ['tmp']
+      }
+
+    , ect: {
+          renderCompatibility: {
+              options: {
+                root: 'test/fixtures/ect'
+              }
+            , src:  'page'
+            , dest: 'tmp/compatibility/page.html'
+            , variables: {
+                  title: 'Hello, world!'
+                , id: 'main'
+                , links: [
+                      { name: 'Google',   url: 'http://google.com/'   }
+                    , { name: 'Facebook', url: 'http://facebook.com/' }
+                    , { name: 'Twitter',  url: 'http://twitter.com/'  }
+                  ]
+                , upperHelper: function (string) {
+                    return string.toUpperCase();
+                  }
+              }
+          }
+        , renderDefault: {
+            files: {
+                // 1:1 render
+                'tmp/default/ect.txt': 'test/fixtures/ect1.ect'
+                // concat then render into single file
+              , 'tmp/default/concat.txt': [
+                    'test/fixtures/ect1.ect'
+                  , 'test/fixtures/ect2.ect'
+                ]
+            }
+          }
+        , renderCustomOptions1: {
+              options: {
+                root: 'test/fixtures'
+              }
+            , files: {
+                'tmp/customOptions1/concat.txt': '*.ect'
+              }
+          }
+        , renderCustomOptions2: {
+              options: {
+                variables: {
+                    title: 'Hello, world!'
+                  , id: 'main'
+                  , links: [
+                        { name: 'Google',   url: 'http://google.com/'   }
+                      , { name: 'Facebook', url: 'http://facebook.com/' }
+                      , { name: 'Twitter',  url: 'http://twitter.com/'  }
+                    ]
+                  , upperHelper: function (string) {
+                      return string.toUpperCase();
+                    }
+                }
+              }
+            , files: {
+                'tmp/customOptions2/page.html': 'test/fixtures/ect/page'
+              }
+          }
+          // use JavaScript object as root.
+        , renderCustomOptions3: {
+              options: {
+                  root: {
+                      layout: '<html><head><title>[%- @title %]</title></head><body>[% content %]</body></html>'
+                    , page: '[% extend "layout" %]<p>Page content</p>'
+                  }
+                , open: '[%'
+                , close: '%]'
+                , variables: {
+                    title: 'Hello, world!'
+                  }
+              }
+            , src:  'page'
+            , dest: 'tmp/customOptions3/page.html'
+          }
+        , renderEach: {
+              options: {
+                  variables: {
+                    title: 'Hello, world!'
+                  }
+                , ext: '.html'
+              }
+            , expand: true
+            , flatten: true
+            , cwd: 'test/fixtures/view'
+            , src: '*.html'
+            , dest: 'tmp/each'
+            , ext: '.html'
+          }
+      }
+
+      // Unit tests.
+    , nodeunit: {
+        tests: ['test/*_test.js']
+      }
+
   });
 
+  // Actually load this plugin's task(s).
   grunt.loadTasks('tasks');
+
+  // These plugins provide necessary tasks.
+  grunt.loadNpmTasks('grunt-contrib-jshint');
+  grunt.loadNpmTasks('grunt-contrib-clean');
+  grunt.loadNpmTasks('grunt-contrib-nodeunit');
+
+  // Whenever the "test" task is run, first clean the "tmp" dir, then run this
+  // plugin's task(s), then test the result.
+  grunt.registerTask('test', ['clean', 'ect', 'nodeunit']);
+
+  // By default, lint and run all tests.
+  grunt.registerTask('default', ['jshint', 'test']);
+
 };
+Copyright (c) 2013 2no
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
 
 install via npm
 
-    npm install grunt-ect
+```shell
+npm install grunt-ect
+```
 
 and in your Gruntfile.js file:
 
-    grunt.loadNpmTasks('grunt-ect');
-
-## Usage
-
-    grunt.initConfig({
-      ect: {
-        top: {
-          options: {
-            root: 'test/ect',
-          },
-          src:  'page',
-          dest: 'test/page.html',
-          variables: {
-            title : 'Hello, world!',
-            id : 'main',
-            links: [
-              { name : 'Google', url : 'http://google.com/' },
-              { name : 'Facebook', url : 'http://facebook.com/' },
-              { name : 'Twitter', url : 'http://twitter.com/' }
-            ],
-            upperHelper : function (string) {
-              return string.toUpperCase();
-            }
-          }
+```shell
+grunt.loadNpmTasks('grunt-ect');
+```
+
+### Prerelease version
+
+install via bitbucket repository
+
+```shell
+npm install git+https://2no@bitbucket.org/2no/grunt-ect.git#develop
+```
+
+## Usage Examples
+
+```js
+ect: {
+  render: {
+    files: {
+      'path/to/result.html': 'path/to/template.ect', // 1:1 render
+      'path/to/another.html': ['path/to/templates/*.ect', 'path/to/more/*.ect'] // render and concat into single file
+    },
+  },
+  renderCustomOptions1: {
+    options: {
+      root: 'path/to',
+      variables: {
+        title: 'Hello, world!',
+        id: 'main',
+        links: [
+          { name: 'Google',   url: 'http://google.com/'   },
+          { name: 'Facebook', url: 'http://facebook.com/' },
+          { name: 'Twitter',  url: 'http://twitter.com/'  },
+        ],
+        upperHelper: function (string) {
+          return string.toUpperCase();
         },
-        ...
       },
-      ...
-    });
+    },
+    files: {
+      'path/to/result.html': 'template.ect',
+    },
+  },
+  renderCustomOptions2: {
+    options: {
+      root: {
+        layout: '<html><head><title>[%- @title %]</title></head><body>[% content %]</body></html>',
+        page: '[% extend "layout" %]<p>Page content</p>',
+      },
+      open: '[%',
+      close: '%]',
+      variables: {
+        title: 'Hello, world!',
+      },
+    },
+    src:  'page',
+    dest: 'path/to/result.html',
+  },
+  renderEach: {
+    options: {
+      variables: {
+        title: 'Hello, world!',
+      },
+      ext: '.html',
+    },
+    expand: true,
+    flatten: true,
+    cwd: 'path/to',
+    src: '*.html',
+    dest: 'path/to',
+    ext: '.html',
+  },
+
+  // v0.0.1
+  renderCompatibility: {
+    options: {
+      root: 'path/to',
+    },
+    src:  'template',
+    dest: 'path/to/result.html',
+    variables: {
+      title: 'Hello, world!',
+      id: 'main',
+      links: [
+        { name: 'Google',   url: 'http://google.com/'   },
+        { name: 'Facebook', url: 'http://facebook.com/' },
+        { name: 'Twitter',  url: 'http://twitter.com/'  },
+      ],
+      upperHelper: function (string) {
+        return string.toUpperCase();
+      },
+    },
+  },
+}
+```
 
 run with:
 
-    grunt ect
+```shell
+grunt ect
+```
 
-## Prerelease version
+## Options
 
-install via bitbucket repository
+### Original Renderer Options
+
+* `root` — Templates root folder or JavaScript object containing templates
+* `ext` — Extension of templates, defaulting to `''` (not used for JavaScript objects as root)
+* `open` — Open tag, defaulting to `<%`
+* `close` — Closing tag, defaulting to `%>`
+
+### Added grunt-ect Options
+
+* `variables` — Data passed to template
+* `separator` — Concatenated files will be joined on this string, defaulting to `linefeed`
 
-    npm install git+https://2no@bitbucket.org/2no/grunt-ect.git#develop
+## Release History
 
+* 2013-04-23 v0.1.0 Support wildcard pattern and JavaScript object as root. Extension of the setting method.
+* 2013-04-05 v0.0.1 first version
 {
   "name": "grunt-ect",
   "description": "Static site compiler built around ect",
-  "version": "0.0.1",
+  "version": "0.1.0",
   "homepage": "https://bitbucket.org/2no/grunt-ect",
   "author": {
     "name": "2no",
+    "email": "kazunori.ninomiya@gmail.com",
     "url": "http://www.wakuworks.com/"
   },
   "repository": {
-    "type": "ssh",
+    "type": "git",
     "url": "ssh://git@bitbucket.org/2no/grunt-ect.git"
   },
   "bugs": {
     "url": "https://bitbucket.org/2no/grunt-ect/issues"
   },
-  "license": "MIT",
+  "licenses": [
+    {
+      "type": "MIT",
+      "url": "https://bitbucket.org/2no/grunt-ect/blob/master/LICENSE-MIT"
+    }
+  ],
   "main": "Gruntfile.js",
   "engines": {
     "node": ">= 0.8.0"
   },
+  "scripts": {
+    "test": "grunt test"
+  },
   "dependencies": {
-    "grunt": ">=0.4.0",
     "ect": "~0.4.11"
   },
   "devDependencies": {
-    "grunt": ">=0.4.0",
-    "ect": "~0.4.11"
+    "grunt-contrib-jshint": "~0.1.1",
+    "grunt-contrib-clean": "~0.4.0",
+    "grunt-contrib-nodeunit": "~0.1.2",
+    "grunt": "~0.4.1"
+  },
+  "peerDependencies": {
+    "grunt": "~0.4.1"
   },
   "keywords": [
-    "template",
-    "ect",
-    "grunt",
     "gruntplugin"
   ]
 }
+/*
+ * grunt-ect
+ * https://bitbucket.org/2no/grunt-ect
+ *
+ * Copyright (c) 2013 2no
+ * Licensed under the MIT license.
+ */
+
 'use strict';
 
 module.exports = function(grunt)
 {
-  var fs  = require('fs')
-    , ECT = require('ect');
+  var ECT  = require('ect')
+    , path = require('path')
+    , _ = grunt.util._
+    ;
 
-  grunt.registerMultiTask('ect', 'generates an html file from a ect template', function() {
-    var html
-      , renderer
-      , data    = this.data
-      , options = data.options || {}
-      , done    = this.async();
+  grunt.registerMultiTask('ect', 'generates an file from a ect template', function() {
+    var options = this.options({
+            variables: this.data.variables || {}
+          , separator: grunt.util.linefeed
+        })
+      , done = this.async()
+      ;
 
-    if (typeof options['watch'] !== 'undefined') {
+    if (typeof options.watch !== 'undefined') {
       delete options['watch'];
     }
+
+    grunt.verbose.writeflags(options, 'Options');
+
+    this.files.forEach(function(f) {
+      try {
+        var validFiles = getSrc(f, options);
+        writeFile(f.dest, concatOutput(validFiles, options));
+      }
+      catch (e) {
+        grunt.log.error(e);
+        done(false);
+      }
+    });
+
+    done(true);
+  });
+
+  function getSrc(file, options)
+  {
+    var re, length
+      , src  = file.src
+      , orig = file.orig
+      , root = options.root
+      ;
+
+    switch (grunt.util.kindOf(root)) {
+      case 'object':
+        re = orig.src.map(function(v) {
+          return new RegExp(v.replace('*', '.*'));
+        });
+        length = re.length;
+        src = Object.keys(root).filter(function(key) {
+          var i = length;
+          while (i--) {
+            if (re[i].test(key)) {
+              return true;
+            }
+          }
+          return false;
+        });
+        break;
+
+      case 'string':
+        if (typeof file.cwd === 'undefined') {
+          file.cwd = root;
+          src = grunt.file.expandMapping(orig.src, null, {
+            cwd: file.cwd,
+            flatten: file.flatten,
+            ext: file.ext,
+          }).map(function(v) {
+            return v.src[0];
+          });
+        }
+        /* falls through */
+      default:
+        src = removeInvalidFiles(src);
+        break;
+    }
+
+    return src;
+  }
+
+  function removeInvalidFiles(files)
+  {
+    return files.filter(function(filepath) {
+      if (!grunt.file.exists(filepath)) {
+        grunt.log.warn('Source file "' + filepath + '" not found."');
+        return false;
+      }
+      else {
+        return true;
+      }
+    });
+  }
+
+  function concatOutput(files, options)
+  {
+    return files.map(function(src) {
+      return render(src, options);
+    }).join(grunt.util.normalizelf(options.separator));
+  }
+
+  function render(src, options)
+  {
+    var renderer, root;
+
+    options = _.clone(options);
+    switch (grunt.util.kindOf(options.root)) {
+      case 'object':
+        break;
+
+      case 'string':
+        root = path.join(options.root, path.sep);
+        if (grunt.file.doesPathContain(root, src)) {
+          src = path.relative(root, src);
+        }
+        break;
+
+      default:
+        options.root = path.dirname(src);
+        src = path.basename(src);
+        break;
+    }
+
     renderer = new ECT(options);
+    return renderer.render(src, options.variables);
+  }
 
-    try {
-      html = renderer.render(data.src, data.variables || {});
-      grunt.file.write(data.dest, html);
-      grunt.log.writeln("HTML written to '"+ data.dest +"'");
-      done(true);
+  function warnOnEmptyFile(path)
+  {
+    grunt.log.warn('Destination (' + path + ') not written because rendered files were empty.');
+  }
+
+  function writeFile(path, output)
+  {
+    if (output.length < 1) {
+      warnOnEmptyFile(path);
     }
-    catch (e) {
-      grunt.log.error(e);
-      done(false);
+    else {
+      grunt.file.write(path, output);
+      grunt.log.writeln('File ' + path + ' created.');
     }
-  });
+  }
 };

test/ect/footer

-<div id="footer">
-	<div class="left">Generated by ECT</div>
-	<% content 'footer-info' %>
-</div>

test/ect/layout

-<!DOCTYPE html>
-<html>
-	<head>
-		<title><%- @title %></title>
-	</head>
-	<body>
-		<% content %>
-		<% include 'footer' %>
-	</body>
-</html>

test/ect/list

-<% linkHelper = (link) -> %>
-	<li><a href="<%- link.url %>"><%- link.name %></a></li>
-<% end %>
-
-<% if @links?.length : %>
-	<ul>
-		<% for link in @links : %>
-			<%- linkHelper link %>
-		<% end %>
-	</ul>
-<% else : %>
-	<p>List is empty</p>
-<% end %>

test/ect/page

-<% extend 'layout' %>
-
-<div id="<%- @id %>">
-	<h1><%- @upperHelper @title %></h1>
-	<% include 'list' %>
-</div>
-
-<% block 'footer-info' : %>
-	<div class="right">page: main</div>
-<% end %>
+'use strict';
+
+var grunt = require('grunt');
+
+function readFile(file)
+{
+  var contents = grunt.file.read(file);
+
+  if (process.platform === 'win32') {
+    contents = contents.replace(/\r\n/g, '\n');
+  }
+
+  return contents;
+}
+
+function assertFileEquality(test, pathToActual, pathToExpected, message)
+{
+  test.equal(readFile(pathToExpected), readFile(pathToActual), message);
+}
+
+exports.ect = {
+    renderCompatibility: function(test)
+    {
+      test.expect(1);
+
+      assertFileEquality(test,
+        'tmp/compatibility/page.html',
+        'test/expected/compatibility/page.html',
+        'Should render ect to single file.');
+
+      test.done();
+    }
+  , renderDefault: function(test)
+    {
+      test.expect(2);
+      
+      assertFileEquality(test,
+        'tmp/default/ect.txt',
+        'test/expected/default/ect.txt',
+        'Should render ect to single file.');
+
+      assertFileEquality(test,
+        'tmp/default/concat.txt',
+        'test/expected/default/concat.txt',
+        'Should render ect files to concatenate them into a single file.');
+
+      test.done();
+    }
+  , renderCustomOptions1: function(test)
+    {
+      test.expect(1);
+
+      assertFileEquality(test,
+        'tmp/customOptions1/concat.txt',
+        'test/expected/customOptions1/concat.txt',
+        'Should render ect files to concatenate them into a single file.');
+
+      test.done();
+    }
+  , renderCustomOptions2: function(test)
+    {
+      test.expect(1);
+
+      assertFileEquality(test,
+        'tmp/customOptions2/page.html',
+        'test/expected/customOptions2/page.html',
+        'Should render ect to single file.');
+
+      test.done();
+    }
+  , renderCustomOptions3: function(test)
+    {
+      test.expect(1);
+
+      assertFileEquality(test,
+        'tmp/customOptions3/page.html',
+        'test/expected/customOptions3/page.html',
+        'Should render ect to single file.');
+
+      test.done();
+    }
+  , renderEach: function(test)
+    {
+      test.expect(5);
+
+      assertFileEquality(test,
+        'tmp/each/footer.html',
+        'test/expected/each/footer.html',
+        'Should render ect to single file.');
+
+      assertFileEquality(test,
+        'tmp/each/header.html',
+        'test/expected/each/header.html',
+        'Should render ect to single file.');
+
+      assertFileEquality(test,
+        'tmp/each/layout.html',
+        'test/expected/each/layout.html',
+        'Should render ect to single file.');
+
+      assertFileEquality(test,
+        'tmp/each/page.html',
+        'test/expected/each/page.html',
+        'Should render ect to single file.');
+
+      assertFileEquality(test,
+        'tmp/each/sublayout.html',
+        'test/expected/each/sublayout.html',
+        'Should render ect to single file.');
+
+      test.done();
+    }
+};

test/expected/compatibility/page.html

+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Hello, world!</title>
+	</head>
+	<body>
+		
+<div id="main">
+	<h1>HELLO, WORLD!</h1>
+	
+	<ul>
+						<li><a href="http://google.com/">Google</a></li>
+						<li><a href="http://facebook.com/">Facebook</a></li>
+						<li><a href="http://twitter.com/">Twitter</a></li>
+			</ul>
+</div>
+
+		<div id="footer">
+	<div class="left">Generated by ECT</div>
+		<div class="right">page: main</div>
+</div>
+	</body>
+</html>

test/expected/customOptions1/concat.txt

+Test 0 1 2 3 4 5 6 7 8 9 10
+Test 10 9 8 7 6 5 4 3 2 1 0

test/expected/customOptions2/page.html

+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Hello, world!</title>
+	</head>
+	<body>
+		
+<div id="main">
+	<h1>HELLO, WORLD!</h1>
+	
+	<ul>
+						<li><a href="http://google.com/">Google</a></li>
+						<li><a href="http://facebook.com/">Facebook</a></li>
+						<li><a href="http://twitter.com/">Twitter</a></li>
+			</ul>
+</div>
+
+		<div id="footer">
+	<div class="left">Generated by ECT</div>
+		<div class="right">page: main</div>
+</div>
+	</body>
+</html>

test/expected/customOptions3/page.html

+<html><head><title>Hello, world!</title></head><body><p>Page content</p></body></html>

test/expected/default/concat.txt

+Test 0 1 2 3 4 5 6 7 8 9 10
+Test 10 9 8 7 6 5 4 3 2 1 0

test/expected/default/ect.txt

+Test 0 1 2 3 4 5 6 7 8 9 10

test/expected/each/footer.html

+<p>Footer content</p>

test/expected/each/header.html

+<h1>Page title</h1>

test/expected/each/layout.html

+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Hello, world!</title>
+	</head>
+	<body>
+		<header><h1>Page title</h1>
+</header>
+		<section></section>
+		<section></section>
+		<section></section>
+		<footer><p>Footer content</p>
+</footer>
+	</body>
+</html>

test/expected/each/page.html

+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Hello, world!</title>
+	</head>
+	<body>
+		<header><h1>Page title</h1>
+</header>
+		<section><div>
+<p>
+Page content
+</p>
+</div>
+</section>
+		<section>redefined side1 content
+</section>
+		<section>side2 content
+</section>
+		<footer><p>Footer content</p>
+</footer>
+	</body>
+</html>

test/expected/each/sublayout.html

+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Hello, world!</title>
+	</head>
+	<body>
+		<header><h1>Page title</h1>
+</header>
+		<section><div>
+</div>
+</section>
+		<section>side1 content
+</section>
+		<section></section>
+		<footer><p>Footer content</p>
+</footer>
+	</body>
+</html>

test/fixtures/ect/footer

+<div id="footer">
+	<div class="left">Generated by ECT</div>
+	<% content 'footer-info' %>
+</div>

test/fixtures/ect/layout

+<!DOCTYPE html>
+<html>
+	<head>
+		<title><%- @title %></title>
+	</head>
+	<body>
+		<% content %>
+		<% include 'footer' %>
+	</body>
+</html>

test/fixtures/ect/list

+<% linkHelper = (link) -> %>
+	<li><a href="<%- link.url %>"><%- link.name %></a></li>
+<% end %>
+
+<% if @links?.length : %>
+	<ul>
+		<% for link in @links : %>
+			<%- linkHelper link %>
+		<% end %>
+	</ul>
+<% else : %>
+	<p>List is empty</p>
+<% end %>

test/fixtures/ect/page

+<% extend 'layout' %>
+
+<div id="<%- @id %>">
+	<h1><%- @upperHelper @title %></h1>
+	<% include 'list' %>
+</div>
+
+<% block 'footer-info' : %>
+	<div class="right">page: main</div>
+<% end %>

test/fixtures/ect1.ect

+Test<% for num in [0..10] : %> <%- num %><% end %>

test/fixtures/ect2.ect

+Test<% for num in [10..0] : %> <%- num %><% end %>

test/fixtures/view/footer.html

+<p>Footer content</p>

test/fixtures/view/header.html

+<h1>Page title</h1>

test/fixtures/view/layout.html

+<!DOCTYPE html>
+<html>
+	<head>
+		<title><%- @title %></title>
+	</head>
+	<body>
+		<header><% include 'header' %></header>
+		<section><% content %></section>
+		<section><% content 'side1' %></section>
+		<section><% content 'side2' %></section>
+		<footer><% include 'footer' %></footer>
+	</body>
+</html>

test/fixtures/view/page.html

+<% extend 'sublayout' %>
+<p>
+Page content
+</p>
+<% block 'side1' : %>
+redefined side1 content
+<% end %>
+<% block 'side2' : %>
+side2 content
+<% end %>

test/fixtures/view/sublayout.html

+<% extend 'layout' %>
+<div>
+<% content() %>
+</div>
+<% block 'side1' : %>
+side1 content
+<% end %>

test/page.html

-<!DOCTYPE html>
-<html>
-	<head>
-		<title>Hello, world!</title>
-	</head>
-	<body>
-		
-<div id="main">
-	<h1>HELLO, WORLD!</h1>
-	
-	<ul>
-						<li><a href="http://google.com/">Google</a></li>
-						<li><a href="http://facebook.com/">Facebook</a></li>
-						<li><a href="http://twitter.com/">Twitter</a></li>
-			</ul>
-</div>
-
-		<div id="footer">
-	<div class="left">Generated by ECT</div>
-		<div class="right">page: main</div>
-</div>
-	</body>
-</html>