Updated with some modernity.

This commit is contained in:
Elf M. Sternberg 2016-06-29 07:34:18 -07:00
parent fac48b727a
commit 1c3103815f
12 changed files with 170 additions and 36 deletions

3
.gitignore vendored
View File

@ -4,5 +4,6 @@
*.orig *.orig
npm-debug.log npm-debug.log
node_modules/* node_modules/*
lib/
tmp/ tmp/
src/*.js
lib/*.js

View File

@ -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 $<

View File

@ -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

8
bin/activate Normal file
View File

@ -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

16
bin/devserver Executable file
View File

@ -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")

View File

@ -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"]
} }

20
src/engine.coffee Normal file
View File

@ -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

View File

@ -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

View File

@ -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) ->

10
src/tumble.coffee Normal file
View File

@ -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
}

View File

@ -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."

View File

@ -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"