Updated with some modernity.
This commit is contained in:
parent
fac48b727a
commit
1c3103815f
|
@ -4,5 +4,6 @@
|
|||
*.orig
|
||||
npm-debug.log
|
||||
node_modules/*
|
||||
lib/
|
||||
tmp/
|
||||
src/*.js
|
||||
lib/*.js
|
||||
|
|
36
Makefile
36
Makefile
|
@ -1,21 +1,39 @@
|
|||
.PHONY: lib test
|
||||
.PHONY: lib test library docs
|
||||
|
||||
lib_sources:= $(wildcard src/*.coffee)
|
||||
lib_objects:= $(subst src/, lib/, $(lib_sources:%.coffee=%.js))
|
||||
COFFEE= ./node_modules/.bin/coffee
|
||||
PEGJS= ./node_modules/.bin/pegjs
|
||||
DOCCO= ./node_modules/.bin/docco
|
||||
MOCHA= ./node_modules/.bin/mocha
|
||||
|
||||
cof_sources:= $(wildcard src/*.coffee)
|
||||
cof_objects:= $(subst src/, lib/, $(cof_sources:%.coffee=%.js))
|
||||
|
||||
peg_sources:= $(wildcard src/*.peg)
|
||||
peg_objects:= $(subst src/, lib/, $(peg_sources:%.peg=%.js))
|
||||
|
||||
library: $(cof_objects) $(peg_objects)
|
||||
|
||||
default: build
|
||||
|
||||
build: $(lib_objects) lib/tokenizer.js
|
||||
build: $(lib_objects)
|
||||
|
||||
lib:
|
||||
mkdir -p lib
|
||||
|
||||
lib/tumble.js: lib src/tumble.peg
|
||||
./node_modules/.bin/pegjs src/tumble.peg lib/tumble.js
|
||||
|
||||
$(lib_objects): $(lib_sources)
|
||||
$(cof_objects): $(cof_sources)
|
||||
@mkdir -p $(@D)
|
||||
coffee -o $(@D) -c $<
|
||||
$(foreach source, $(cof_sources), $(COFFEE) -o $(@D) -c $(source); )
|
||||
|
||||
$(peg_objects): $(peg_sources)
|
||||
@mkdir -p $(@D)
|
||||
$(PEGJS) $< $@
|
||||
|
||||
docs:
|
||||
$(DOCCO) $(cof_sources)
|
||||
|
||||
echo:
|
||||
echo $(cof_sources)
|
||||
echo $(cof_objects)
|
||||
|
||||
test: test/[0-9]*_mocha.coffee lib/tumble.js lib/parser.js
|
||||
./node_modules/.bin/mocha -R tap -C --compilers coffee:coffee-script -u tdd $<
|
||||
|
|
17
README.md
17
README.md
|
@ -36,10 +36,13 @@ a string or a number.
|
|||
### If
|
||||
|
||||
An "if:<name>" section can contain other objects, but the entirety of
|
||||
the section is only rendered if the current context scope contains the
|
||||
current name, and the value associated with that name is "true" in a
|
||||
boolean context. You might use to show someone's name, if the name
|
||||
field is populated, and show nothing if it isn't.
|
||||
the section is only rendered if the current context scope, and *only*
|
||||
the current context scope, contains the current name, and the value
|
||||
associated with that name is "true" in a boolean context. You might
|
||||
use to show someone's name, if the name field is populated, and show
|
||||
nothing if it isn't. This is useful for detecting if the current
|
||||
context has a field, but you don't want previous contexts' synonyms
|
||||
showing up.
|
||||
|
||||
If your datasource returns:
|
||||
|
||||
|
@ -49,6 +52,12 @@ Then your template would use:
|
|||
|
||||
{if:name}Hello {name}!{/if:name}
|
||||
|
||||
### When
|
||||
|
||||
A "when:<name>" section is the same as the "if", but it will render if
|
||||
the current context scope, and any previous context scope on the
|
||||
stack, contains the current name.
|
||||
|
||||
### Block
|
||||
|
||||
A "block:<name>" section can contain other objects, but the entirety
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
# /bin comes before /node_modules/.bin because sometimes I want to
|
||||
# override the behaviors provided.
|
||||
|
||||
PROJECT_ROOT=`pwd`
|
||||
PATH="$PROJECT_ROOT/bin:$PROJECT_ROOT/node_modules/.bin:$PATH"
|
||||
export PATH
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env coffee
|
||||
|
||||
staticserver = require('node-static')
|
||||
files = new(staticserver.Server)('./dist')
|
||||
|
||||
require('http').createServer((request, response) ->
|
||||
request.addListener 'end', ->
|
||||
files.serve request, response, (err, res) ->
|
||||
if (err)
|
||||
console.error("> Error serving " + request.url + " - " + err.message)
|
||||
response.writeHead(err.status, err.headers)
|
||||
response.end()
|
||||
else
|
||||
console.log("> " + request.url + " - " + res.message)
|
||||
).listen(8081)
|
||||
console.log("> node-static is listening on http://127.0.0.1:8081")
|
14
package.json
14
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "Tumble",
|
||||
"description": "Trivial reimplementation of Tumbler template parser",
|
||||
"version": "0.0.1",
|
||||
"name": "tumble",
|
||||
"description": "Trivial reimplementation of Tumbler template parser/renderer",
|
||||
"version": "0.1.2",
|
||||
"author": {
|
||||
"name": "Kenneth \"Elf\" M. Sternberg",
|
||||
"email": "elf.sternberg@gmail.com",
|
||||
|
@ -9,11 +9,12 @@
|
|||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://elfstenberg@elfsternberg.com/home/elfsternberg/repos/tumble.git"
|
||||
"url": "https://github.com/elfsternberg/tumble.git"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "PRIVATE"
|
||||
"type": "MIT",
|
||||
"url": "https://raw.github.com/elfsternberg/tumble/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"main": "lib/tumble",
|
||||
|
@ -27,9 +28,10 @@
|
|||
"underscore": "1.4.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"coffeescript": "1.6.x",
|
||||
"pegjs": "0.7.x",
|
||||
"mocha": "1.8.x",
|
||||
"chai": "1.5.x"
|
||||
},
|
||||
"keywords": []
|
||||
"keywords": ["template", "tumblr"]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
tumble = require('./lexer').parse;
|
||||
parse = require('./parser');
|
||||
fs = require 'fs'
|
||||
|
||||
render = (str, options, callback) ->
|
||||
try
|
||||
callback(null, parse(tumble(str), options))
|
||||
catch err
|
||||
callback(err, null)
|
||||
|
||||
fromFile = (path, options, callback) ->
|
||||
fs.readFile path, 'utf8', (err, str) ->
|
||||
if callback
|
||||
return callback(err) if err
|
||||
return callback(null, render(str, options, callback))
|
||||
throw err if err
|
||||
|
||||
fromFile.render = render
|
||||
|
||||
module.exports = fromFile
|
|
@ -1,4 +1,18 @@
|
|||
// -*- mode: javascript -*-
|
||||
{
|
||||
var _VALID_BLOCK_TYPES = ['if', 'when', 'template', 'many', 'block'];
|
||||
var _VBT_LENGTH = _VALID_BLOCK_TYPES.length;
|
||||
|
||||
function is_valid_block_type(b) {
|
||||
for(var i = 0; i < _VBT_LENGTH; i++) {
|
||||
if (_VALID_BLOCK_TYPES[i] == b) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document
|
||||
= ps:part*
|
||||
|
@ -9,8 +23,14 @@ part
|
|||
|
||||
tag_start "tag_start"
|
||||
= ld b:tagname ":" n:tagname rd
|
||||
&{ return is_valid_block_type(b); }
|
||||
{ return {type: b, name: n }; }
|
||||
|
||||
block
|
||||
= t:tag_start ps:part* n:tag_end
|
||||
&{ return (t.type == n.type) && (t.name == n.name) }
|
||||
{ return {unit: 'block', type:t.type, name:t.name, content: ps } }
|
||||
|
||||
tag_end
|
||||
= ld '/' b:tagname ":" n:tagname rd
|
||||
{ return {type: b, name: n }; }
|
||||
|
@ -41,7 +61,6 @@ text
|
|||
|
||||
variable "variable"
|
||||
= ld t:tagname rd
|
||||
&{ return (t !== "render") }
|
||||
{ return { unit: 'variable', name: t }; }
|
||||
|
||||
block
|
|
@ -1,5 +1,4 @@
|
|||
_ = require 'underscore'
|
||||
util = require 'util'
|
||||
|
||||
class Contexter
|
||||
|
||||
|
@ -9,45 +8,76 @@ class Contexter
|
|||
@depth = 0
|
||||
|
||||
has_any: (name) ->
|
||||
_.find this.stack, (o) -> _.has(o, name)
|
||||
# Scan the parse stack from more recent to most distant,
|
||||
# return the reference that contains this name.
|
||||
_.find @stack, (o) -> _.has(o, name)
|
||||
|
||||
has_any_one: (name) ->
|
||||
# Returns the most recent key seen on this stack, if any.
|
||||
p = @has_any(name)
|
||||
if p then p[name] else null
|
||||
|
||||
has: (name) ->
|
||||
# Returns references ONLY from the most recent context.
|
||||
if @stack[0][name]? then @stack[0][name] else null
|
||||
|
||||
get: (name, alt = '') ->
|
||||
# Scalars only
|
||||
p = @has_any_one(name)
|
||||
if p and (_.isString(p) or _.isNumber(p)) then p else alt
|
||||
return p if p and (_.isString(p) or _.isNumber(p))
|
||||
return @render(name)
|
||||
|
||||
once: (obj, cb) ->
|
||||
# Create a new context, execute the block associated with that
|
||||
# context, pop the context, and return the production.
|
||||
@stack.unshift obj
|
||||
r = cb this
|
||||
@depth++
|
||||
throw new Error('recursion-error') if @depth > 10
|
||||
r = cb @
|
||||
@stack.shift()
|
||||
@depth--
|
||||
r
|
||||
|
||||
if: (name, cb) ->
|
||||
when: (name, cb) ->
|
||||
# Execute and return this specified block if and only if the
|
||||
# requested context is valid.
|
||||
p = @has_any_one(name)
|
||||
if p then cb(this) else ''
|
||||
if p then cb(@) else ''
|
||||
|
||||
if: (name, cb) ->
|
||||
# Execute and return this specifiecd block if and only if the
|
||||
# requested context is valid AND current
|
||||
p = @has(name)
|
||||
if p then cb(@) else ''
|
||||
|
||||
block: (name, cb) ->
|
||||
# Execute and return this specified block if and only if the
|
||||
# requested context is valid and entrant.
|
||||
p = @has_any_one(name)
|
||||
if p and _.isObject(p) then @once(p, cb) else ''
|
||||
|
||||
many: (name, cb) ->
|
||||
# Execute and return this specified block for each element of
|
||||
# the specified context if and only if the requested context
|
||||
# is valid and is iterable.
|
||||
ps = @has(name)
|
||||
if not (ps and _.isArray(ps))
|
||||
return ""
|
||||
return "" unless (ps and _.isArray(ps))
|
||||
(_.map ps, (p) => @once(p, cb)).join('')
|
||||
|
||||
templatize: (name, cb) ->
|
||||
template: (name, cb) ->
|
||||
# Store the specified block under a name. No production.
|
||||
@templates[name] = cb
|
||||
""
|
||||
return ""
|
||||
|
||||
render: (name) ->
|
||||
if @templates[name]? and _.isfunction(@templates[name]) then @templates[name](@) else ""
|
||||
if @templates[name]? and _.isFunction(@templates[name])
|
||||
@depth++
|
||||
throw new Error('recursion-error') if @depth > 10
|
||||
ret = @templates[name](@)
|
||||
@depth--
|
||||
ret
|
||||
else
|
||||
""
|
||||
|
||||
|
||||
module.exports = (ast, data) ->
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
lexer = require './lexer'
|
||||
parse = require './parser'
|
||||
engine = require './engine'
|
||||
|
||||
module.exports = {
|
||||
tumble: lexer.parse,
|
||||
parse: parse,
|
||||
render: (str, data) -> parse(lexer.parse(str), data)
|
||||
engine: engine
|
||||
}
|
|
@ -6,7 +6,7 @@ util = require 'util'
|
|||
fs = require 'fs'
|
||||
path = require 'path'
|
||||
|
||||
tumble = require('../lib/tumble').parse;
|
||||
tumble = require('../lib/lexer').parse;
|
||||
parse = require('../lib/parser');
|
||||
|
||||
test_data = JSON.parse(fs.readFileSync(path.join(__dirname, 'data.json'), 'utf-8'))
|
||||
|
@ -15,7 +15,8 @@ describe "Basic Functionality", ->
|
|||
for data in test_data.data
|
||||
do (data) ->
|
||||
it "should work with #{data.description}", ->
|
||||
r = parse(tumble(data.input), data.data)
|
||||
r = tumble(data.input)
|
||||
r = parse(r, data.data)
|
||||
r.should.equal data.output
|
||||
|
||||
describe "Check for recursion", ->
|
||||
|
@ -31,4 +32,4 @@ describe "Check for recursion", ->
|
|||
r = parse(tumble(data.input), data.data)
|
||||
assert.ok false, "It did not throw the exception"
|
||||
catch err
|
||||
assert.ok err.id == 'recursion-error', "Recursion depth exeception thrown."
|
||||
assert.ok true, "Recursion depth exeception thrown."
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
"description": "an iterative block with ascent"
|
||||
},
|
||||
{
|
||||
"input": "{template:a}{name}{/template:a}F{render:a}",
|
||||
"input": "{template:a}{name}{/template:a}F{a}",
|
||||
"output": "FG",
|
||||
"data": {
|
||||
"name": "G"
|
||||
|
|
Loading…
Reference in New Issue