Permalink
Browse files

Initial Commit

  • Loading branch information...
0 parents commit 0eac08ccadd82c4d57580ddb67da8dab1d52c7df @skid skid committed May 2, 2011
Showing with 949 additions and 0 deletions.
  1. +169 −0 README.md
  2. +132 −0 helpers.js
  3. +100 −0 index.js
  4. +13 −0 package.json
  5. +153 −0 parser.js
  6. +239 −0 tags.js
  7. +8 −0 tests/extends_1.html
  8. +7 −0 tests/extends_2.html
  9. +9 −0 tests/extends_base.html
  10. +7 −0 tests/include_base.html
  11. +12 −0 tests/included.html
  12. +1 −0 tests/included_2.html
  13. +40 −0 tests/tests.js
  14. +59 −0 widgets.js
@@ -0,0 +1,169 @@
+# Node-T
+
+A fast django-like templating engine for node.js.
+
+Node-T is a templating engine inspired by the django syntax. It has a few extensions and the templates are compiled to native javascript functions which make them really fast. For now it's synchronous, but once a template is read and compiled, it is cached in memory.
+
+### Example template code
+
+ <html>
+ <body>
+ <h1>Example</h1>
+ {% for name in names %}
+ <p>
+ {{forloop.counter}}
+ {# This is a comment #}
+ {{name}}{% if name == "Django" %} Reinhardt{% end %}
+ </p>
+ {% end %}
+ </body>
+ </html>
+
+### Example node code
+
+ var template = require('node-t');
+ var tmpl = template.fromFile("/path/to/template.html");
+ console.log( tmpl.render({names: ["Duke", "Django", "Louis"]}) );
+
+### How it works
+
+Node-T reads template files and translates them into javascript functions using the Function constructor. When we later render a template we call the evaled function passing a context object as an argument. This makes the rendering very fast. The template tags are defined as strings of Javascript code - which is a bit ugly, but there are helpers that will make writing tags easier for you.
+
+The slots system will allow you to define your own HTML snippets that will be rendered with a special context.
+
+## The API
+
+You have 2 methods for creating a template object:
+
+ var template = require('node-slots');
+ template.fromFile("path/to/template/file.html");
+ template.fromString("Template string here");
+
+Both of them will give you a template object on which you call the render method passing it a map of context values.
+
+ var tmpl = template.fromFile("path/to/template/file.html");
+ var renderdHtml = tmpl.render({});
+
+## Template syntax
+
+You should be familiar with the [1]"Django template syntax". Here I'll just sum up the diferences:
+
+- There are no filters implemented yet
+- Tags like {% for %} and {% if %} are closed with a simple {% end %} tag
+- Not all tags are implemented
+- Some extra tags are implemented
+- Syntax for some tags is a bit different.
+
+Here's a list of currently implemented tags:
+
+### Variable tags
+
+Used to print a variable to the template. If the variable is not in the context we don't get an error, rather an empty string. You can use dot notation to access object proerties or array memebers.
+
+ <p>First Name: {{users.0.first_name}}</p>
+
+### Comment tags
+
+Comment tags are simply ignored. Comments can't span multitple lines.
+
+ {# This is a comment #}
+
+### Logic tags
+
+#### extends / block
+
+Check django's template inheritance system for more info. Unlike django, the block tags are terminated with {% end %}, not with {% endblock %}
+
+#### include
+
+Includes a template in it's place. The template is rendered within the current context. Does not requre closing with {% end %}.
+
+ {% include template_path %}
+ {% include "path/to/template.js" %}
+
+#### for
+
+You can iterate arrays and objects. Access the current iteration index through 'forloop.index' which is available inside the loop.
+
+ {% for x in y %}
+ <p>{% forloop.index %}</p>
+ {% end %}
+
+#### if
+
+Supports the following expressions. No else tag yet.
+
+ {% if x %}{% end %}
+ {% if !x %}{% end %}
+ {% if x operator y %}
+ Operators: ==, !=, <, <=, >, >=, ===, !==, in
+ The 'in' operator checks for presence in arrays too.
+ {% end %}
+ {% if x == 'five' %}
+ The operands can be also be string or number literals
+ {% end %}
+
+#### slot
+
+Slots are parts of a page that where you want highly customized content that depends heavily on dynamic data. They work in pair with widget functions that you can write yourself.
+
+Template code
+
+ <div>
+ {% slot main %}
+ </div>
+ <div>
+ {% slot sidebar %}
+ </div>
+
+Node.js code
+
+ context.slots = {
+ main: [
+ "<h1>This is a paragraph as a normal string.</h1>", // String
+
+ { tagname: 'analytics', // Widget object
+ uaCode: 'UA-XXXXX-X' },
+ ],
+
+ sidebar: [
+ "<h3>Navigation</h3>", // String
+
+ { tagname: 'navigation', // Widget object
+ links: [
+ '/home',
+ '/about',
+ '/kittens'
+ ]}
+ ]
+ }
+
+ context.widgets = {
+ analytics: function(context){
+ // this inside widget functions is bound to the widget object
+ return "<script>..." + this.uaCode + "...</script>";
+ },
+ navigation: function(context){
+ var i, html = "";
+ for( i=0; i<this.links; i++ )
+ html += "<a href='" + links[i] + "'>" + links[i] + "</a>";
+ return html;
+ }
+ }
+
+ template.render(context)
+
+Result
+
+ <div>
+ <h1>This is a paragraph as a normal string.</h1>
+ <script>...UA-XXXXX-X...</script>
+ </div>
+ <div>
+ <h3>Navigation</h3>
+ <a href='/home'>/home</a>
+ <a href='/about'>/about</a>
+ <a href='/kittens'>/kittens</a>
+ </div>
+
+[1]: http://djangoproject.com/
@@ -0,0 +1,132 @@
+// Checks if the string is a number literal
+var NUMBER_LITERAL = /^\d+([.]\d+)?$/;
+// Checks if there are unescaped single quotes (the string needs to be reversed first)
+var UNESCAPED_QUOTE = /'(?!\\)/;
+// Checks if there are unescaped double quotes (the string needs to be reversed first)
+var UNESCAPED_DQUOTE = /"(?!\\)/;
+// Valid Javascript name: 'name' or 'property.accessor.chain'
+var VALID_NAME = /^([$A-Za-z_]+[$A-Za-z_0-9]*)(\.?([$A-Za-z_]+[$A-Za-z_0-9]*))*$/;
+// Valid Javascript name: 'name'
+var VALID_SHORT_NAME = /^[$A-Za-z_]+[$A-Za-z_0-9]*$/;
+// Javascript keywords can't be a name: 'for.is_invalid' as well as 'for' but not 'for_' or '_for'
+var KEYWORDS = /^(Array|RegExpt|Object|String|Number|Math|Error|break|continue|do|for|new|case|default|else|function|in|return|typeof|while|delete|if|switch|var|with)(?=(\.|$))/;
+// Valid block name
+var VALID_BLOCK_NAME = /^[A-Za-z]+[A-Za-z_0-9]*$/;
+
+// Returns TRUE if the passed string is a valid javascript number or string literal
+function isLiteral( string ){
+ var literal = false;
+ // Check if it's a number literal
+ if( NUMBER_LITERAL.test( string ) ){
+ literal = true;
+ }
+
+ // Check if it's a valid string literal (throw exception otherwise)
+ else if( (string[0] === string[string.length-1]) && (string[0] === "'" || string[0] === '"') ) {
+ var teststr = string.substr( 1, string.length-2 ).split("").reverse().join("");
+ if( string[0] === "'" && UNESCAPED_QUOTE.test( teststr ) || string[1] === '"' && UNESCAPED_DQUOTE.test( teststr ) ){
+ throw new Error("Invalid string literal. Unescaped quote (" + string[0] + ") found.");
+ }
+ literal = true;
+ }
+
+ return literal;
+}
+
+// Returns TRUE if the passed string is a valid javascript string literal
+function isStringLiteral( string ){
+ // Check if it's a valid string literal (throw exception otherwise)
+ if( (string[0] === string[string.length-1]) && (string[0] === "'" || string[0] === '"') ) {
+ var teststr = string.substr( 1, string.length-2 ).split("").reverse().join("");
+ if( string[0] === "'" && UNESCAPED_QUOTE.test( teststr ) || string[1] === '"' && UNESCAPED_DQUOTE.test( teststr ) ){
+ throw new Error("Invalid string literal. Unescaped quote (" + string[0] + ") found.");
+ }
+ return true;
+ }
+ return false;
+}
+
+// Variable names starting with __ are reserved.
+function isValidName( string ){
+ return VALID_NAME.test(string) && !KEYWORDS.test(string) && string.substr(0,2) !== "__";
+}
+
+// Variable names starting with __ are reserved.
+function isValidShortName( string ){
+ return VALID_SHORT_NAME.test(string) && !KEYWORDS.test(string) && string.substr(0,2) !== "__";
+}
+
+// Checks if a name is a vlaid block name
+function isValidBlockName( string ){
+ return VALID_BLOCK_NAME.test(string);
+}
+
+/**
+ * Returns a valid javascript code that will
+ * check if a variable (or property chain) exists
+ * in the evaled context. For example:
+ * check( "foo.bar.baz" )
+ * will return the following string:
+ * "typeof foo !== 'undefined' && typeof foo.bar !== 'undefined' && typeof foo.bar.baz !== 'undefined'"
+ */
+exports.check = function( variable, context ){
+ /* 'this' inside of the render function is bound to the tag closure which is meaningless, so we can't use it.
+ * '__this' is bound to the original template whose render function we called.
+ * Using 'this' in the HTML templates will result in '__this.__currentContext'. This is an additional context
+ * for binding data to a specific template - e.g. binding widget data.
+ */
+ variable = variable.replace(/^this/, '__this.__currentContext');
+
+ if( isLiteral( variable ) )
+ return "(true)";
+
+ var props = variable.split("."), chain = "", output = [];
+
+ if( typeof context === 'string' && context.length )
+ props.unshift( context );
+
+ props.forEach(function(prop){
+ chain += (chain ? "." + prop : prop);
+ output.push( "typeof " + chain + " !== 'undefined'" )
+ });
+ return "(" + output.join(" && ") + ")";
+}
+
+/**
+ * Returns an escaped string (safe for evaling). If context is passed
+ * then returns a concatenation of context and the escaped variable name.
+ */
+exports.escape = function( variable, context ){
+ /* 'this' inside of the render function is bound to the tag closure which is meaningless, so we can't use it.
+ * '__this' is bound to the original template whose render function we called.
+ * Using 'this' in the HTML templates will result in '__this.__currentContext'. This is an additional context
+ * for binding data to a specific template - e.g. binding widget data.
+ */
+ variable = variable.replace(/^this/, '__this.__currentContext');
+
+ if( isLiteral( variable ) )
+ variable = "(" + variable + ")";
+
+ else if( typeof context === 'string' && context.length ){
+ variable = context + '.' + variable;
+ }
+
+ return variable.replace(/\n/g, '\\n').replace(/\r/g, '\\r');
+}
+
+/**
+ * Merges b into a and returns a
+ */
+exports.merge = function(a, b){
+ if (a && b)
+ for (var key in b) {
+ a[key] = b[key];
+ }
+ return a;
+};
+
+exports.isLiteral = isLiteral;
+exports.isValidName = isValidName;
+exports.isValidShortName = isValidShortName;
+exports.isValidBlockName = isValidBlockName;
+exports.isStringLiteral = isStringLiteral;
Oops, something went wrong.

0 comments on commit 0eac08c

Please sign in to comment.