Permalink
Browse files

don't symlink vacuum

  • Loading branch information...
1 parent a09d61a commit b07e7235e72368cd69f271bbffae94d3060bcd14 @thejh committed Dec 30, 2011
View
@@ -0,0 +1,4 @@
+Copyright (c) 2011 Jann Horn
+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.
@@ -0,0 +1,122 @@
+`vacuum` is another node.js module for templating.
+
+Goals
+=====
+This is not necessarily what the code already does, e.g. I have no idea whether it's fast.
+
+ - be fast
+ - be secure
+ - be streaming (e.g. send out a static head while the DB is still looking up some data)
+ - be easy to understand (no big pile of special cases)
+
+Basic usage
+===========
+Look into the "example" folder for a working example.
+
+Setup code:
+
+```js
+var vacuum = require('vacuum')
+// Load all .html files from that folder and register them by name.
+var renderTemplate = vacuum.loadSync(__dirname+'/templates')
+```
+
+Rendering a template to a HTTP response (`article.html` is the file name of the template):
+
+```js
+renderTemplate('article', {articleID: articleID, title: articleTitle}, httpResponse)
+```
+
+The template files are normal HTML with some special-syntax tags inside. Example:
+
+```html
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ {var name="title"}
+ </title>
+ </head>
+ <body>
+ {childblock of="document"}
+ </body>
+</html>
+```
+
+This could be a HTML document template. It only contains bodyless special tags, the syntax for them is
+`{tagName key1="value1" key2="value2" ...}`. The tag name determines which template should be inserted.
+There are two kinds of templates:
+
+ - template files (like this one)
+ - template functions (like `var` and `childblock`)
+
+Template functions are JS functions that can be used inside of templates. Because of them, there's
+something called "context". In the `renderTemplate` example above, the initial context is
+`{articleID: articleID, title: articleTitle}`, but context can also be changed by template functions - however, these changes
+only affect descendants of that template function. Attributes also change the context - in the
+HTML template above, the `{var name="title"}` inclusion calls the `var` template with the context
+`{articleID: articleID, title: articleTitle, name: 'title'}`. The `var` template then does (this is
+somewhat simplified) `chunk(context[context.name]); done()`.
+
+Here's an example that uses the HTML template defined above:
+
+ {#document title="Test"}
+ Hello You!
+ {/document}
+
+This example contains an inclusion with body - it has an opening tag with `#` and a closing tag with `/`.
+The body of the inclusion becomes a template which is given to `document`'s context as `$block`.
+`document` can then do things with it - it could e.g. call it multiple times with different contexts.
+However, here it's only used with the `{childblock}` default template function.
+
+
+Making your own template functions
+==================================
+
+You can make your own template functions by attaching them to `renderTemplate`:
+
+```js
+renderTemplate.connection = function CONNECTION(template, functions, context, chunk, done) {
+ var connection = vacuum.getFromContext(context, 'name')
+ var address = connection.remoteAddress
+ chunk(address+':'+connection.remotePort)
+ done()
+}
+```
+
+As you can see, there are five parameters.
+
+`template` is a code representation of the tag used to
+reference this template. If the tag has a body, that body is stored in the `parts` property of
+`template`.
+
+`functions` is the same as your `renderTemplate`.
+
+`context` is the context (a modified copy of the context of the template inclusions parent).
+
+`chunk` is the function used to write rendered data as a string. It takes one argument.
+
+`done` is a normal callback - call it without arguments for success (after you've finished all
+calls to `chunk`), call it with an error if an error occurs.
+
+Default functions
+=================
+You can make your own template functions, but there are also some defaults:
+
+foreach
+-------
+This calls the body that was given to it for each element in the given array.
+
+Important context variables:
+
+ - `list` - name of the context variable which contains the array
+ - `element` - name of the context variable inside of which the element from the array should be stored
+
+var
+---
+This prints the value from the variable whose name is stored in `name`.
+
+childblock
+----------
+This takes the template stored in `$block_<of>` on the context and renders it. It needs a `of` context variable that specifies
+the template from whose inclusion the child block should come.
@@ -0,0 +1,23 @@
+var vacuum = require('../')
+ , http = require('http')
+
+var renderTemplate = vacuum.loadSync(__dirname+'/templates')
+
+renderTemplate.connection = function CONNECTION(template, functions, context, chunk, done) {
+ var connection = vacuum.getFromContext(context, 'name')
+ var address = connection.remoteAddress
+ chunk(address+':'+connection.remotePort)
+ done()
+}
+
+var server = http.createServer(function(req, res) {
+ var page = req.url.slice(1).split(/[?\/]/)[0]
+ if (page === 'test') {
+ renderTemplate('test', {request: req}, res)
+ } else {
+ res.writeHead(404, 'what the hell?')
+ res.end("couldn't find that stuff")
+ }
+})
+server.listen(9876)
+console.log('listening on 9876')
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ {var name="title"}
+ </title>
+ </head>
+ <body>
+ {childblock of="document"}
+ </body>
+</html>
@@ -0,0 +1,5 @@
+{#document title="Test"}
+ Hello You!
+ <br>
+ Your connection is: {connection name="request.connection"}
+{/document}
@@ -0,0 +1,43 @@
+var vacuum = require('./')
+
+exports.foreach = function FOREACH(template, functions, context, chunk, done) {
+ if (context.element == null) throw new Error('"element" value is necessary')
+
+ var list = vacuum.getFromContext(context, 'list')
+ if (!Array.isArray(list)) throw new Error('context[context.list] is not an array (context['+JSON.stringify(context.list)+'] is a '+(typeof list)+')')
+
+ var templateCopy = {}
+ vacuum.copyProps(templateCopy, template)
+ delete templateCopy.type
+
+ var contexts = list.map(function(element) {
+ var copy = {}
+ vacuum.copyProps(copy, context)
+ copy[context.element] = element
+ return copy
+ })
+
+ vacuum.renderTemplate({parts: repeat(templateCopy, list.length)}, functions, contexts, chunk, done)
+
+ function repeat(value, count) {
+ var arr = []
+ while (count--) arr.push(value)
+ return arr
+ }
+}
+
+exports.var = function VAR(template, functions, context, chunk, done) {
+ var value = vacuum.getFromContext(context, 'name')
+ chunk(value)
+ done()
+}
+
+exports.childblock = function CHILDBLOCK(_, functions, context, chunk, done) {
+ var template = {}
+ if (!context.of) throw new Error('context must have "of"')
+ vacuum.copyProps(template, context['$block_'+context.of])
+ delete template.type
+ if (!template.parts) throw new Error('template must have "parts", only has '+Object.keys(template).join(','))
+
+ vacuum.renderTemplate(template, functions, context, chunk, done)
+}
@@ -0,0 +1,145 @@
+exports.renderTemplate = renderTemplate
+exports.compileTemplate = compileTemplate
+exports.copyProps = copyProps
+exports.loadSync = require('./loader').loadSync
+exports.getFromContext = getFromContext
+
+var _reString = '("([^"\\\\]|\\\\.)*")'
+var _reAttribute = '(?:([a-zA-Z0-9_]+)=' + _reString + ')'
+var _reTag = '{([#/]?)([a-zA-Z0-9_]+)((\\s+' + _reAttribute + ')*)\\s*}'
+var tagRegex = new RegExp(_reTag, 'g')
+var argRegex = new RegExp(_reAttribute, 'g')
+
+function compileTemplate(text) {
+ var reResult
+ , lastResult
+ , parents = []
+ , currentNode = {parts: []}
+ , i = 0
+ // WARNING: DO NOT BREAK FROM THIS LOOP OR THE exec()==null WILL GET YOU THE NEXT TIME!
+ while (reResult = tagRegex.exec(text)) {
+ currentNode.parts.push(text.slice(i, reResult.index))
+ i = reResult.index + reResult[0].length
+ lastResult = reResult
+ var flags = reResult[1]
+ , tagtype = reResult[2]
+ , argsstr = reResult[3]
+ if (flags === '/') {
+ var parent = parents.pop()
+ parent.parts.push(currentNode)
+ currentNode = parent
+ continue
+ }
+ var argmap = [], argReResult
+ while (argReResult = argRegex.exec(argsstr)) {
+ argmap.push({name: argReResult[1], value: JSON.parse(argReResult[2])})
+ }
+ if (flags === '#') {
+ parents.push(currentNode)
+ currentNode = {type: tagtype, args: argmap, parts: []}
+ } else {
+ currentNode.parts.push({type: tagtype, args: argmap})
+ }
+ }
+ var lastTextIndex = lastResult ? (lastResult.index + lastResult[0].length) : 0
+ currentNode.parts.push(text.slice(lastTextIndex))
+ return currentNode
+}
+
+function getFromContext(context, nameVar, permissive) {
+ if (!has(context, nameVar)) throw new Error('no own property '+JSON.stringify(nameVar))
+ var name = context[nameVar].split('.')
+ , part
+ , obj = context
+ while ((part = name.shift()) != null) {
+ if (!has(obj, part)) {
+ if (permissive) return void 0
+ throw new Error('no own property '+JSON.stringify(part)+
+ (typeof obj === 'object' ?
+ (', only '+Object.keys(obj).join())
+ :
+ (', its a '+typeof obj)
+ )
+ )
+ }
+ obj = obj[part]
+ }
+ return obj
+}
+
+function renderTemplate(template, functions, context, chunk, done) {
+ var completelyDone = 0
+ // shows which parts won't emit any more data
+ var doneParts = template.parts.map(function(part) { return typeof part === 'string' })
+ // one string per part, empty if this is active
+ var partBuffers = template.parts.map(function(part) { return typeof part === 'string' ? part : '' })
+
+ function gotChunk(partIndex, str) {
+ if (partIndex === completelyDone) return chunk(str)
+ if (partIndex < completelyDone) throw new Error("can't safely write that anymore, you're too late")
+ if (doneParts[partIndex]) throw new Error("that part is already flagged as done, there can't be any more data chunks")
+ partBuffers[partIndex] += str
+ }
+
+ function partComplete(partIndex, err) {
+ if (err) return done(err)
+ if (doneParts[partIndex]) throw new Error("you already flagged it as complete, doing it twice is an error")
+ if (partIndex < completelyDone) throw new Error("WTF?")
+ doneParts[partIndex] = true
+ if (partIndex === completelyDone) flush()
+ }
+
+ function flush() {
+ while (doneParts[completelyDone]) {
+ if (partBuffers[completelyDone] !== '') {
+ chunk(partBuffers[completelyDone])
+ partBuffers[completelyDone] = null
+ }
+ completelyDone++
+ }
+ if (completelyDone === template.parts.length) return done()
+ if (partBuffers[completelyDone] !== '') {
+ chunk(partBuffers[completelyDone])
+ partBuffers[completelyDone] = null
+ }
+ }
+
+ flush()
+
+ template.parts.forEach(function(part, i) {
+ if (typeof part !== 'object') return
+ var childContext = {}
+ if (Array.isArray(context)) {
+ copyProps(childContext, context[i])
+ } else {
+ copyProps(childContext, context)
+ }
+ contextOverwrite(childContext, part.args)
+ if (part.type != null) {
+ if (!has(functions, part.type)) throw new Error('unknown function "'+part.type+'"')
+ if (part.parts) childContext['$block_'+part.type] = part
+ var fn = functions[part.type]
+ fn(part, functions, childContext, gotChunk.bind(null, i), partComplete.bind(null, i))
+ } else {
+ renderTemplate(part, functions, childContext, gotChunk.bind(null, i), partComplete.bind(null, i))
+ }
+ })
+}
+
+function copyProps(target, source) {
+ Object.keys(source).forEach(function(key) {
+ target[key] = source[key]
+ })
+}
+
+function contextOverwrite(context, source) {
+ source.forEach(function(e) {
+ context[e.name] = e.value
+ })
+}
+
+function has(obj, prop) {
+ return Object.prototype.hasOwnProperty.call(obj, prop)
+}
+
+// console.log(JSON.stringify(compileTemplate('abc{foo}def{bar qux="5"}ghi')))
Oops, something went wrong.

0 comments on commit b07e723

Please sign in to comment.