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
npm-debug.log
node_modules/*
lib/
tmp/
src/*.js
lib/*.js

View File

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

View File

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

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

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 -*-
{
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

View File

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

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

View File

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