Updated with some modernity.
This commit is contained in:
parent
fac48b727a
commit
1c3103815f
|
@ -4,5 +4,6 @@
|
||||||
*.orig
|
*.orig
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
node_modules/*
|
node_modules/*
|
||||||
lib/
|
|
||||||
tmp/
|
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)
|
COFFEE= ./node_modules/.bin/coffee
|
||||||
lib_objects:= $(subst src/, lib/, $(lib_sources:%.coffee=%.js))
|
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
|
default: build
|
||||||
|
|
||||||
build: $(lib_objects) lib/tokenizer.js
|
build: $(lib_objects)
|
||||||
|
|
||||||
lib:
|
lib:
|
||||||
mkdir -p lib
|
mkdir -p lib
|
||||||
|
|
||||||
lib/tumble.js: lib src/tumble.peg
|
$(cof_objects): $(cof_sources)
|
||||||
./node_modules/.bin/pegjs src/tumble.peg lib/tumble.js
|
|
||||||
|
|
||||||
$(lib_objects): $(lib_sources)
|
|
||||||
@mkdir -p $(@D)
|
@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
|
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 $<
|
./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
|
### If
|
||||||
|
|
||||||
An "if:<name>" section can contain other objects, but the entirety of
|
An "if:<name>" section can contain other objects, but the entirety of
|
||||||
the section is only rendered if the current context scope contains the
|
the section is only rendered if the current context scope, and *only*
|
||||||
current name, and the value associated with that name is "true" in a
|
the current context scope, contains the current name, and the value
|
||||||
boolean context. You might use to show someone's name, if the name
|
associated with that name is "true" in a boolean context. You might
|
||||||
field is populated, and show nothing if it isn't.
|
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:
|
If your datasource returns:
|
||||||
|
|
||||||
|
@ -49,6 +52,12 @@ Then your template would use:
|
||||||
|
|
||||||
{if:name}Hello {name}!{/if:name}
|
{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
|
### Block
|
||||||
|
|
||||||
A "block:<name>" section can contain other objects, but the entirety
|
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",
|
"name": "tumble",
|
||||||
"description": "Trivial reimplementation of Tumbler template parser",
|
"description": "Trivial reimplementation of Tumbler template parser/renderer",
|
||||||
"version": "0.0.1",
|
"version": "0.1.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Kenneth \"Elf\" M. Sternberg",
|
"name": "Kenneth \"Elf\" M. Sternberg",
|
||||||
"email": "elf.sternberg@gmail.com",
|
"email": "elf.sternberg@gmail.com",
|
||||||
|
@ -9,11 +9,12 @@
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "ssh://elfstenberg@elfsternberg.com/home/elfsternberg/repos/tumble.git"
|
"url": "https://github.com/elfsternberg/tumble.git"
|
||||||
},
|
},
|
||||||
"licenses": [
|
"licenses": [
|
||||||
{
|
{
|
||||||
"type": "PRIVATE"
|
"type": "MIT",
|
||||||
|
"url": "https://raw.github.com/elfsternberg/tumble/master/LICENSE"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"main": "lib/tumble",
|
"main": "lib/tumble",
|
||||||
|
@ -27,9 +28,10 @@
|
||||||
"underscore": "1.4.x"
|
"underscore": "1.4.x"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"coffeescript": "1.6.x",
|
||||||
"pegjs": "0.7.x",
|
"pegjs": "0.7.x",
|
||||||
"mocha": "1.8.x",
|
"mocha": "1.8.x",
|
||||||
"chai": "1.5.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 -*-
|
// -*- 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
|
document
|
||||||
= ps:part*
|
= ps:part*
|
||||||
|
@ -9,8 +23,14 @@ part
|
||||||
|
|
||||||
tag_start "tag_start"
|
tag_start "tag_start"
|
||||||
= ld b:tagname ":" n:tagname rd
|
= ld b:tagname ":" n:tagname rd
|
||||||
|
&{ return is_valid_block_type(b); }
|
||||||
{ return {type: b, name: n }; }
|
{ 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
|
tag_end
|
||||||
= ld '/' b:tagname ":" n:tagname rd
|
= ld '/' b:tagname ":" n:tagname rd
|
||||||
{ return {type: b, name: n }; }
|
{ return {type: b, name: n }; }
|
||||||
|
@ -41,7 +61,6 @@ text
|
||||||
|
|
||||||
variable "variable"
|
variable "variable"
|
||||||
= ld t:tagname rd
|
= ld t:tagname rd
|
||||||
&{ return (t !== "render") }
|
|
||||||
{ return { unit: 'variable', name: t }; }
|
{ return { unit: 'variable', name: t }; }
|
||||||
|
|
||||||
block
|
block
|
|
@ -1,5 +1,4 @@
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
util = require 'util'
|
|
||||||
|
|
||||||
class Contexter
|
class Contexter
|
||||||
|
|
||||||
|
@ -9,45 +8,76 @@ class Contexter
|
||||||
@depth = 0
|
@depth = 0
|
||||||
|
|
||||||
has_any: (name) ->
|
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) ->
|
has_any_one: (name) ->
|
||||||
|
# Returns the most recent key seen on this stack, if any.
|
||||||
p = @has_any(name)
|
p = @has_any(name)
|
||||||
if p then p[name] else null
|
if p then p[name] else null
|
||||||
|
|
||||||
has: (name) ->
|
has: (name) ->
|
||||||
|
# Returns references ONLY from the most recent context.
|
||||||
if @stack[0][name]? then @stack[0][name] else null
|
if @stack[0][name]? then @stack[0][name] else null
|
||||||
|
|
||||||
get: (name, alt = '') ->
|
get: (name, alt = '') ->
|
||||||
|
# Scalars only
|
||||||
p = @has_any_one(name)
|
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) ->
|
once: (obj, cb) ->
|
||||||
|
# Create a new context, execute the block associated with that
|
||||||
|
# context, pop the context, and return the production.
|
||||||
@stack.unshift obj
|
@stack.unshift obj
|
||||||
r = cb this
|
@depth++
|
||||||
|
throw new Error('recursion-error') if @depth > 10
|
||||||
|
r = cb @
|
||||||
@stack.shift()
|
@stack.shift()
|
||||||
|
@depth--
|
||||||
r
|
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)
|
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) ->
|
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)
|
p = @has_any_one(name)
|
||||||
if p and _.isObject(p) then @once(p, cb) else ''
|
if p and _.isObject(p) then @once(p, cb) else ''
|
||||||
|
|
||||||
many: (name, cb) ->
|
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)
|
ps = @has(name)
|
||||||
if not (ps and _.isArray(ps))
|
return "" unless (ps and _.isArray(ps))
|
||||||
return ""
|
|
||||||
(_.map ps, (p) => @once(p, cb)).join('')
|
(_.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
|
@templates[name] = cb
|
||||||
""
|
return ""
|
||||||
|
|
||||||
render: (name) ->
|
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) ->
|
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'
|
fs = require 'fs'
|
||||||
path = require 'path'
|
path = require 'path'
|
||||||
|
|
||||||
tumble = require('../lib/tumble').parse;
|
tumble = require('../lib/lexer').parse;
|
||||||
parse = require('../lib/parser');
|
parse = require('../lib/parser');
|
||||||
|
|
||||||
test_data = JSON.parse(fs.readFileSync(path.join(__dirname, 'data.json'), 'utf-8'))
|
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
|
for data in test_data.data
|
||||||
do (data) ->
|
do (data) ->
|
||||||
it "should work with #{data.description}", ->
|
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
|
r.should.equal data.output
|
||||||
|
|
||||||
describe "Check for recursion", ->
|
describe "Check for recursion", ->
|
||||||
|
@ -31,4 +32,4 @@ describe "Check for recursion", ->
|
||||||
r = parse(tumble(data.input), data.data)
|
r = parse(tumble(data.input), data.data)
|
||||||
assert.ok false, "It did not throw the exception"
|
assert.ok false, "It did not throw the exception"
|
||||||
catch err
|
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"
|
"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",
|
"output": "FG",
|
||||||
"data": {
|
"data": {
|
||||||
"name": "G"
|
"name": "G"
|
||||||
|
|
Loading…
Reference in New Issue