diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8217b24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +node_modules +dump.rdb +npm-debug.log +*.tgz diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..28ced50 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,27 @@ +0.1.3 / 2012-04-10 +================== + +* New API for SocketStream 0.3 beta2 +* Now includes client code (Hogan VM) for client +* Improved error reporting in console +* Reset repo to remove rubbish + + +0.1.2 / 2012-03-17 +================== + +* Templates are now appended to `ss.tmpl` instead of global variables (e.g. `HT`) +* Tip: You may put `window.HT = ss.tmpl` into `entry.js` to avoid changing your code +* Reduced amount of code output for apps with many templates + + +0.1.1 / 2012-03-17 +================== + +* Upgraded `hogan` to 2.0.0 + + +0.1.0 / 2012-01-14 +================== + +* Initial commit \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ea25e4 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Hogan Template Engine wrapper for SocketStream 0.3 + +http://twitter.github.com/hogan.js/ + +Use pre-compiled Hogan (Mustache-compatible) client-side templates in your app to benefit from increased performance and a smaller client-side library download. + + +### Installation + +The `ss-hogan` module is installed by default with new apps created by SocketStream 0.3. If you don't already have it installed, add `ss-hogan` to your application's `package.json` file and then add this line to app.js: + +```javascript +ss.client.templateEngine.use(require('ss-hogan')); +``` + +Restart the server. From now on all templates will be pre-compiled and accessibale via the `ss.tmpl` object. + +Note: Hogan uses a small [client-side VM](https://raw.github.com/twitter/hogan.js/master/lib/template.js) which renders the pre-compiled templates. This file is included and automatically sent to the client. + + +### Usage + +E.g. a template placed in + + /client/templates/offers/latest.html + +Can be rendered in your browser with + +```javascript +// assumes var ss = require('socketstream') +var html = ss.tmpl['offers-latest'].render({name: 'Special Offers'}) +``` + + +### Options + +When experimenting with Hogan, or converting an app from one template type to another, you may find it advantageous to use multiple template engines and confine use of Hogan to a sub-directory of `/client/templates`. + +Directory names can be passed to the second argument as so: + +```javascript +ss.client.templateEngine.use(require('ss-hogan'), '/hogan-templates'); +``` \ No newline at end of file diff --git a/client.js b/client.js new file mode 100644 index 0000000..8491aad --- /dev/null +++ b/client.js @@ -0,0 +1,240 @@ +/* + * Copyright 2011 Twitter, Inc. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var Hogan = {}; + +(function (Hogan, useArrayBuffer) { + Hogan.Template = function (renderFunc, text, compiler, options) { + this.r = renderFunc || this.r; + this.c = compiler; + this.options = options; + this.text = text || ''; + this.buf = (useArrayBuffer) ? [] : ''; + } + + Hogan.Template.prototype = { + // render: replaced by generated code. + r: function (context, partials, indent) { return ''; }, + + // variable escaping + v: hoganEscape, + + // triple stache + t: coerceToString, + + render: function render(context, partials, indent) { + return this.ri([context], partials || {}, indent); + }, + + // render internal -- a hook for overrides that catches partials too + ri: function (context, partials, indent) { + return this.r(context, partials, indent); + }, + + // tries to find a partial in the curent scope and render it + rp: function(name, context, partials, indent) { + var partial = partials[name]; + + if (!partial) { + return ''; + } + + if (this.c && typeof partial == 'string') { + partial = this.c.compile(partial, this.options); + } + + return partial.ri(context, partials, indent); + }, + + // render a section + rs: function(context, partials, section) { + var tail = context[context.length - 1]; + + if (!isArray(tail)) { + section(context, partials, this); + return; + } + + for (var i = 0; i < tail.length; i++) { + context.push(tail[i]); + section(context, partials, this); + context.pop(); + } + }, + + // maybe start a section + s: function(val, ctx, partials, inverted, start, end, tags) { + var pass; + + if (isArray(val) && val.length === 0) { + return false; + } + + if (typeof val == 'function') { + val = this.ls(val, ctx, partials, inverted, start, end, tags); + } + + pass = (val === '') || !!val; + + if (!inverted && pass && ctx) { + ctx.push((typeof val == 'object') ? val : ctx[ctx.length - 1]); + } + + return pass; + }, + + // find values with dotted names + d: function(key, ctx, partials, returnFound) { + var names = key.split('.'), + val = this.f(names[0], ctx, partials, returnFound), + cx = null; + + if (key === '.' && isArray(ctx[ctx.length - 2])) { + return ctx[ctx.length - 1]; + } + + for (var i = 1; i < names.length; i++) { + if (val && typeof val == 'object' && names[i] in val) { + cx = val; + val = val[names[i]]; + } else { + val = ''; + } + } + + if (returnFound && !val) { + return false; + } + + if (!returnFound && typeof val == 'function') { + ctx.push(cx); + val = this.lv(val, ctx, partials); + ctx.pop(); + } + + return val; + }, + + // find values with normal names + f: function(key, ctx, partials, returnFound) { + var val = false, + v = null, + found = false; + + for (var i = ctx.length - 1; i >= 0; i--) { + v = ctx[i]; + if (v && typeof v == 'object' && key in v) { + val = v[key]; + found = true; + break; + } + } + + if (!found) { + return (returnFound) ? false : ""; + } + + if (!returnFound && typeof val == 'function') { + val = this.lv(val, ctx, partials); + } + + return val; + }, + + // higher order templates + ho: function(val, cx, partials, text, tags) { + var compiler = this.c; + var t = val.call(cx, text, function(t) { + return compiler.compile(t, {delimiters: tags}).render(cx, partials); + }); + this.b(compiler.compile(t.toString(), {delimiters: tags}).render(cx, partials)); + return false; + }, + + // template result buffering + b: (useArrayBuffer) ? function(s) { this.buf.push(s); } : + function(s) { this.buf += s; }, + fl: (useArrayBuffer) ? function() { var r = this.buf.join(''); this.buf = []; return r; } : + function() { var r = this.buf; this.buf = ''; return r; }, + + // lambda replace section + ls: function(val, ctx, partials, inverted, start, end, tags) { + var cx = ctx[ctx.length - 1], + t = null; + + if (!inverted && this.c && val.length > 0) { + return this.ho(val, cx, partials, this.text.substring(start, end), tags); + } + + t = val.call(cx); + + if (typeof t == 'function') { + if (inverted) { + return true; + } else if (this.c) { + return this.ho(t, cx, partials, this.text.substring(start, end), tags); + } + } + + return t; + }, + + // lambda replace variable + lv: function(val, ctx, partials) { + var cx = ctx[ctx.length - 1]; + var result = val.call(cx); + if (typeof result == 'function') { + result = result.call(cx); + } + result = result.toString(); + + if (this.c && ~result.indexOf("{\u007B")) { + return this.c.compile(result).render(cx, partials); + } + + return result; + } + + }; + + var rAmp = /&/g, + rLt = //g, + rApos =/\'/g, + rQuot = /\"/g, + hChars =/[&<>\"\']/; + + + function coerceToString(val) { + return String((val === null || val === undefined) ? '' : val); + } + + function hoganEscape(str) { + str = coerceToString(str); + return hChars.test(str) ? + str + .replace(rAmp,'&') + .replace(rLt,'<') + .replace(rGt,'>') + .replace(rApos,''') + .replace(rQuot, '"') : + str; + } + + var isArray = Array.isArray || function(a) { + return Object.prototype.toString.call(a) === '[object Array]'; + }; + +})(typeof exports !== 'undefined' ? exports : Hogan); diff --git a/engine.js b/engine.js new file mode 100644 index 0000000..0766ceb --- /dev/null +++ b/engine.js @@ -0,0 +1,44 @@ +// Hogan Template Engine wrapper for SocketStream 0.3 + +var fs = require('fs'), + path = require('path'), + hogan = require('hogan.js'); + +exports.init = function(ss, config) { + + // Send Hogan VM to the client + var clientCode = fs.readFileSync(path.join(__dirname, 'client.js'), 'utf8'); + ss.client.send('lib', 'hogan-template', clientCode); + + return { + + name: 'Hogan', + + // Opening code to use when a Hogan template is called for the first time + prefix: function() { + return ''; + }, + + // Compile template into a function and attach it to ss.tmpl + process: function(template, path, id) { + + var compiledTemplate; + + try { + compiledTemplate = hogan.compile(template, {asString: true}); + } catch (e) { + var message = '! Error compiling the ' + path + ' template into Hogan'; + console.log(String.prototype.hasOwnProperty('red') && message.red || message); + throw new Error(e); + compiledTemplate = '

Error

'; + } + + return 't[\'' + id + '\']=new ht(' + compiledTemplate + ');'; + } + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..292ec12 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "ss-hogan", + "author": "Owen Barnes ", + "description": "Hogan template engine wrapper providing server-side compiled templates for SocketStream apps", + "version": "0.1.3", + "main": "./engine.js", + "repository": { + "type" : "git", + "url": "https://github.com/socketstream/ss-hogan.git" + }, + "engines": { + "node": ">= 0.6.0" + }, + "dependencies": { + "hogan.js": "2.0.0" + }, + "devDependencies": {} +}