Compare commits

..

12 Commits

Author SHA1 Message Date
Elf M. Sternberg cdc62ad80f Updating merge isuse. 2016-06-29 08:10:57 -07:00
Elf M. Sternberg d286d05537 Revised some commentary.
I had an idea to try and merge the lexer and the parser.  It is, after
all, a PEG-generated lexer, and PEGs are supposed to generate parsers,
right?  But the elegance of the separation at this point is so nice and
straightforward that I don't think I win anything by merging them.
2016-06-29 07:36:58 -07:00
Elf M. Sternberg 1c3103815f Updated with some modernity. 2016-06-29 07:34:18 -07:00
Elf M. Sternberg ab9d81a323 Updating the package.json file. 2013-11-29 17:42:51 -08:00
Elf M. Sternberg 578150e389 Wrapped template descender in a recursion check. Fixed global leak in valid_block_type() check. Updated Readme to document if/when differences. 2013-06-12 09:51:00 -07:00
Elf M. Sternberg 8d949ce6f0 Removing unneeded 'util' require. Updating package manager. 2013-06-12 09:24:39 -07:00
Elf M. Sternberg 74a99da2ef Updated for the web. 2013-06-11 13:13:16 -07:00
Elf M. Sternberg 63ebf50ccb Support for 'each' (an early synonym for 'many') removed. 2013-06-11 09:24:41 -07:00
Elf M. Sternberg 98704f69ff Removed unneeded debugging line. 2013-06-11 09:23:43 -07:00
Elf M. Sternberg 2ff8172f33 Now have a 'when' (when the variable is true in *any* recent context) and 'if' (if the variable is true only in the *most* recent context). Added to the lexer a handler to determine if the block name is valid. This duplicates code from the parser, but it allows the lexer to tell the user when he's being dumb. 2013-06-11 09:05:32 -07:00
Elf M. Sternberg f65548f1c4 Needed if I'm ever going to make this NPM-able. 2013-06-10 15:55:27 -07:00
Elf M. Sternberg 669b9d1ca8 Updated to expose an API that I *hope* integrates with ExpressJS. 2013-06-10 15:54:42 -07:00
13 changed files with 1174 additions and 19 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

@ -22,7 +22,7 @@ lib:
$(cof_objects): $(cof_sources) $(cof_objects): $(cof_sources)
@mkdir -p $(@D) @mkdir -p $(@D)
$(COFFEE) -o $(@D) -c $< $(foreach source, $(cof_sources), $(COFFEE) -o $(@D) -c $(source); )
$(peg_objects): $(peg_sources) $(peg_objects): $(peg_sources)
@mkdir -p $(@D) @mkdir -p $(@D)

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

37
lib/engine.js Normal file
View File

@ -0,0 +1,37 @@
// Generated by CoffeeScript 1.6.1
(function() {
var fromFile, fs, parse, render, tumble;
tumble = require('./lexer').parse;
parse = require('./parser');
fs = require('fs');
render = function(str, options, callback) {
try {
return callback(null, parse(tumble(str), options));
} catch (err) {
return callback(err, null);
}
};
fromFile = function(path, options, callback) {
return fs.readFile(path, 'utf8', function(err, str) {
if (callback) {
if (err) {
return callback(err);
}
return callback(null, render(str, options, callback));
}
if (err) {
throw err;
}
});
};
fromFile.render = render;
module.exports = fromFile;
}).call(this);

850
lib/lexer.js Normal file
View File

@ -0,0 +1,850 @@
module.exports = (function() {
/*
* Generated by PEG.js 0.7.0.
*
* http://pegjs.majda.cz/
*/
function peg$subclass(child, parent) {
function ctor() { this.constructor = child; }
ctor.prototype = parent.prototype;
child.prototype = new ctor();
}
function SyntaxError(expected, found, offset, line, column) {
function buildMessage(expected, found) {
function stringEscape(s) {
function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); }
return s
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\x08/g, '\\b')
.replace(/\t/g, '\\t')
.replace(/\n/g, '\\n')
.replace(/\f/g, '\\f')
.replace(/\r/g, '\\r')
.replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); })
.replace(/[\x10-\x1F\x80-\xFF]/g, function(ch) { return '\\x' + hex(ch); })
.replace(/[\u0180-\u0FFF]/g, function(ch) { return '\\u0' + hex(ch); })
.replace(/[\u1080-\uFFFF]/g, function(ch) { return '\\u' + hex(ch); });
}
var expectedDesc, foundDesc;
switch (expected.length) {
case 0:
expectedDesc = "end of input";
break;
case 1:
expectedDesc = expected[0];
break;
default:
expectedDesc = expected.slice(0, -1).join(", ")
+ " or "
+ expected[expected.length - 1];
}
foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input";
return "Expected " + expectedDesc + " but " + foundDesc + " found.";
}
this.expected = expected;
this.found = found;
this.offset = offset;
this.line = line;
this.column = column;
this.name = "SyntaxError";
this.message = buildMessage(expected, found);
}
peg$subclass(SyntaxError, Error);
function parse(input) {
var options = arguments.length > 1 ? arguments[1] : {},
peg$startRuleFunctions = { document: peg$parsedocument },
peg$startRuleFunction = peg$parsedocument,
peg$c0 = [],
peg$c1 = function(ps) { return { unit: "block", name: "document", content: ps }; },
peg$c2 = "tag_start",
peg$c3 = null,
peg$c4 = ":",
peg$c5 = "\":\"",
peg$c6 = function(b, n) { return is_valid_block_type(b); },
peg$c7 = "",
peg$c8 = function(b, n) { return {type: b, name: n }; },
peg$c9 = "/",
peg$c10 = "\"/\"",
peg$c11 = "tagname",
peg$c12 = /^[a-zA-Z]/,
peg$c13 = "[a-zA-Z]",
peg$c14 = function(t) { return t.join(''); },
peg$c15 = /^[a-zA-Z:\/]/,
peg$c16 = "[a-zA-Z:\\/]",
peg$c17 = "{",
peg$c18 = "\"{\"",
peg$c19 = "}",
peg$c20 = "\"}\"",
peg$c21 = "\n",
peg$c22 = "\"\\n\"",
peg$c23 = "\r\n",
peg$c24 = "\"\\r\\n\"",
peg$c25 = "\r",
peg$c26 = "\"\\r\"",
peg$c27 = "\u2028",
peg$c28 = "\"\\u2028\"",
peg$c29 = "\u2029",
peg$c30 = "\"\\u2029\"",
peg$c31 = "any character",
peg$c32 = function(c) {return c},
peg$c33 = function(bs) { return { unit: 'text', content: bs.join('') } },
peg$c34 = "variable",
peg$c35 = function(t) { return { unit: 'variable', name: t }; },
peg$c36 = function(t, ps, n) { return (t.type == n.type) && (t.name == n.name) },
peg$c37 = function(t, ps, n) { return {unit: 'block', type:t.type, name:t.name, content: ps } },
peg$currPos = 0,
peg$reportedPos = 0,
peg$cachedPos = 0,
peg$cachedPosDetails = { line: 1, column: 1, seenCR: false },
peg$maxFailPos = 0,
peg$maxFailExpected = [],
peg$silentFails = 0,
peg$result;
if ("startRule" in options) {
if (!(options.startRule in peg$startRuleFunctions)) {
throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
}
peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
}
function text() {
return input.substring(peg$reportedPos, peg$currPos);
}
function offset() {
return peg$reportedPos;
}
function line() {
return peg$computePosDetails(peg$reportedPos).line;
}
function column() {
return peg$computePosDetails(peg$reportedPos).column;
}
function peg$computePosDetails(pos) {
function advance(details, startPos, endPos) {
var p, ch;
for (p = startPos; p < endPos; p++) {
ch = input.charAt(p);
if (ch === "\n") {
if (!details.seenCR) { details.line++; }
details.column = 1;
details.seenCR = false;
} else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") {
details.line++;
details.column = 1;
details.seenCR = true;
} else {
details.column++;
details.seenCR = false;
}
}
}
if (peg$cachedPos !== pos) {
if (peg$cachedPos > pos) {
peg$cachedPos = 0;
peg$cachedPosDetails = { line: 1, column: 1, seenCR: false };
}
advance(peg$cachedPosDetails, peg$cachedPos, pos);
peg$cachedPos = pos;
}
return peg$cachedPosDetails;
}
function peg$fail(expected) {
if (peg$currPos < peg$maxFailPos) { return; }
if (peg$currPos > peg$maxFailPos) {
peg$maxFailPos = peg$currPos;
peg$maxFailExpected = [];
}
peg$maxFailExpected.push(expected);
}
function peg$cleanupExpected(expected) {
var i = 0;
expected.sort();
while (i < expected.length) {
if (expected[i - 1] === expected[i]) {
expected.splice(i, 1);
} else {
i++;
}
}
}
function peg$parsedocument() {
var s0, s1, s2;
s0 = peg$currPos;
s1 = [];
s2 = peg$parsepart();
while (s2 !== null) {
s1.push(s2);
s2 = peg$parsepart();
}
if (s1 !== null) {
peg$reportedPos = s0;
s1 = peg$c1(s1);
}
if (s1 === null) {
peg$currPos = s0;
s0 = s1;
} else {
s0 = s1;
}
return s0;
}
function peg$parsepart() {
var s0;
s0 = peg$parseblock();
if (s0 === null) {
s0 = peg$parsevariable();
if (s0 === null) {
s0 = peg$parsetext();
}
}
return s0;
}
function peg$parsetag_start() {
var s0, s1, s2, s3, s4, s5, s6;
peg$silentFails++;
s0 = peg$currPos;
s1 = peg$parseld();
if (s1 !== null) {
s2 = peg$parsetagname();
if (s2 !== null) {
if (input.charCodeAt(peg$currPos) === 58) {
s3 = peg$c4;
peg$currPos++;
} else {
s3 = null;
if (peg$silentFails === 0) { peg$fail(peg$c5); }
}
if (s3 !== null) {
s4 = peg$parsetagname();
if (s4 !== null) {
s5 = peg$parserd();
if (s5 !== null) {
peg$reportedPos = peg$currPos;
s6 = peg$c6(s2,s4);
if (s6) {
s6 = peg$c7;
} else {
s6 = peg$c3;
}
if (s6 !== null) {
peg$reportedPos = s0;
s1 = peg$c8(s2,s4);
if (s1 === null) {
peg$currPos = s0;
s0 = s1;
} else {
s0 = s1;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
peg$silentFails--;
if (s0 === null) {
s1 = null;
if (peg$silentFails === 0) { peg$fail(peg$c2); }
}
return s0;
}
function peg$parsetag_end() {
var s0, s1, s2, s3, s4, s5, s6;
s0 = peg$currPos;
s1 = peg$parseld();
if (s1 !== null) {
if (input.charCodeAt(peg$currPos) === 47) {
s2 = peg$c9;
peg$currPos++;
} else {
s2 = null;
if (peg$silentFails === 0) { peg$fail(peg$c10); }
}
if (s2 !== null) {
s3 = peg$parsetagname();
if (s3 !== null) {
if (input.charCodeAt(peg$currPos) === 58) {
s4 = peg$c4;
peg$currPos++;
} else {
s4 = null;
if (peg$silentFails === 0) { peg$fail(peg$c5); }
}
if (s4 !== null) {
s5 = peg$parsetagname();
if (s5 !== null) {
s6 = peg$parserd();
if (s6 !== null) {
peg$reportedPos = s0;
s1 = peg$c8(s3,s5);
if (s1 === null) {
peg$currPos = s0;
s0 = s1;
} else {
s0 = s1;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
return s0;
}
function peg$parsetagname() {
var s0, s1, s2;
peg$silentFails++;
s0 = peg$currPos;
s1 = [];
if (peg$c12.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = null;
if (peg$silentFails === 0) { peg$fail(peg$c13); }
}
if (s2 !== null) {
while (s2 !== null) {
s1.push(s2);
if (peg$c12.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = null;
if (peg$silentFails === 0) { peg$fail(peg$c13); }
}
}
} else {
s1 = peg$c3;
}
if (s1 !== null) {
peg$reportedPos = s0;
s1 = peg$c14(s1);
}
if (s1 === null) {
peg$currPos = s0;
s0 = s1;
} else {
s0 = s1;
}
peg$silentFails--;
if (s0 === null) {
s1 = null;
if (peg$silentFails === 0) { peg$fail(peg$c11); }
}
return s0;
}
function peg$parsetag() {
var s0, s1, s2, s3, s4, s5, s6;
s0 = peg$currPos;
s1 = peg$parseld();
if (s1 !== null) {
s2 = [];
s3 = peg$currPos;
s4 = peg$currPos;
peg$silentFails++;
s5 = peg$parserd();
peg$silentFails--;
if (s5 === null) {
s4 = peg$c7;
} else {
peg$currPos = s4;
s4 = peg$c3;
}
if (s4 !== null) {
s5 = peg$currPos;
peg$silentFails++;
s6 = peg$parseeol();
peg$silentFails--;
if (s6 === null) {
s5 = peg$c7;
} else {
peg$currPos = s5;
s5 = peg$c3;
}
if (s5 !== null) {
if (peg$c15.test(input.charAt(peg$currPos))) {
s6 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s6 = null;
if (peg$silentFails === 0) { peg$fail(peg$c16); }
}
if (s6 !== null) {
s4 = [s4, s5, s6];
s3 = s4;
} else {
peg$currPos = s3;
s3 = peg$c3;
}
} else {
peg$currPos = s3;
s3 = peg$c3;
}
} else {
peg$currPos = s3;
s3 = peg$c3;
}
if (s3 !== null) {
while (s3 !== null) {
s2.push(s3);
s3 = peg$currPos;
s4 = peg$currPos;
peg$silentFails++;
s5 = peg$parserd();
peg$silentFails--;
if (s5 === null) {
s4 = peg$c7;
} else {
peg$currPos = s4;
s4 = peg$c3;
}
if (s4 !== null) {
s5 = peg$currPos;
peg$silentFails++;
s6 = peg$parseeol();
peg$silentFails--;
if (s6 === null) {
s5 = peg$c7;
} else {
peg$currPos = s5;
s5 = peg$c3;
}
if (s5 !== null) {
if (peg$c15.test(input.charAt(peg$currPos))) {
s6 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s6 = null;
if (peg$silentFails === 0) { peg$fail(peg$c16); }
}
if (s6 !== null) {
s4 = [s4, s5, s6];
s3 = s4;
} else {
peg$currPos = s3;
s3 = peg$c3;
}
} else {
peg$currPos = s3;
s3 = peg$c3;
}
} else {
peg$currPos = s3;
s3 = peg$c3;
}
}
} else {
s2 = peg$c3;
}
if (s2 !== null) {
s3 = peg$parserd();
if (s3 !== null) {
s1 = [s1, s2, s3];
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
return s0;
}
function peg$parseld() {
var s0;
if (input.charCodeAt(peg$currPos) === 123) {
s0 = peg$c17;
peg$currPos++;
} else {
s0 = null;
if (peg$silentFails === 0) { peg$fail(peg$c18); }
}
return s0;
}
function peg$parserd() {
var s0;
if (input.charCodeAt(peg$currPos) === 125) {
s0 = peg$c19;
peg$currPos++;
} else {
s0 = null;
if (peg$silentFails === 0) { peg$fail(peg$c20); }
}
return s0;
}
function peg$parseeol() {
var s0;
if (input.charCodeAt(peg$currPos) === 10) {
s0 = peg$c21;
peg$currPos++;
} else {
s0 = null;
if (peg$silentFails === 0) { peg$fail(peg$c22); }
}
if (s0 === null) {
if (input.substr(peg$currPos, 2) === peg$c23) {
s0 = peg$c23;
peg$currPos += 2;
} else {
s0 = null;
if (peg$silentFails === 0) { peg$fail(peg$c24); }
}
if (s0 === null) {
if (input.charCodeAt(peg$currPos) === 13) {
s0 = peg$c25;
peg$currPos++;
} else {
s0 = null;
if (peg$silentFails === 0) { peg$fail(peg$c26); }
}
if (s0 === null) {
if (input.charCodeAt(peg$currPos) === 8232) {
s0 = peg$c27;
peg$currPos++;
} else {
s0 = null;
if (peg$silentFails === 0) { peg$fail(peg$c28); }
}
if (s0 === null) {
if (input.charCodeAt(peg$currPos) === 8233) {
s0 = peg$c29;
peg$currPos++;
} else {
s0 = null;
if (peg$silentFails === 0) { peg$fail(peg$c30); }
}
}
}
}
}
return s0;
}
function peg$parsetext() {
var s0, s1, s2, s3, s4;
s0 = peg$currPos;
s1 = [];
s2 = peg$currPos;
s3 = peg$currPos;
peg$silentFails++;
s4 = peg$parsetag();
peg$silentFails--;
if (s4 === null) {
s3 = peg$c7;
} else {
peg$currPos = s3;
s3 = peg$c3;
}
if (s3 !== null) {
if (input.length > peg$currPos) {
s4 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s4 = null;
if (peg$silentFails === 0) { peg$fail(peg$c31); }
}
if (s4 !== null) {
peg$reportedPos = s2;
s3 = peg$c32(s4);
if (s3 === null) {
peg$currPos = s2;
s2 = s3;
} else {
s2 = s3;
}
} else {
peg$currPos = s2;
s2 = peg$c3;
}
} else {
peg$currPos = s2;
s2 = peg$c3;
}
if (s2 !== null) {
while (s2 !== null) {
s1.push(s2);
s2 = peg$currPos;
s3 = peg$currPos;
peg$silentFails++;
s4 = peg$parsetag();
peg$silentFails--;
if (s4 === null) {
s3 = peg$c7;
} else {
peg$currPos = s3;
s3 = peg$c3;
}
if (s3 !== null) {
if (input.length > peg$currPos) {
s4 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s4 = null;
if (peg$silentFails === 0) { peg$fail(peg$c31); }
}
if (s4 !== null) {
peg$reportedPos = s2;
s3 = peg$c32(s4);
if (s3 === null) {
peg$currPos = s2;
s2 = s3;
} else {
s2 = s3;
}
} else {
peg$currPos = s2;
s2 = peg$c3;
}
} else {
peg$currPos = s2;
s2 = peg$c3;
}
}
} else {
s1 = peg$c3;
}
if (s1 !== null) {
peg$reportedPos = s0;
s1 = peg$c33(s1);
}
if (s1 === null) {
peg$currPos = s0;
s0 = s1;
} else {
s0 = s1;
}
return s0;
}
function peg$parsevariable() {
var s0, s1, s2, s3;
peg$silentFails++;
s0 = peg$currPos;
s1 = peg$parseld();
if (s1 !== null) {
s2 = peg$parsetagname();
if (s2 !== null) {
s3 = peg$parserd();
if (s3 !== null) {
peg$reportedPos = s0;
s1 = peg$c35(s2);
if (s1 === null) {
peg$currPos = s0;
s0 = s1;
} else {
s0 = s1;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
peg$silentFails--;
if (s0 === null) {
s1 = null;
if (peg$silentFails === 0) { peg$fail(peg$c34); }
}
return s0;
}
function peg$parseblock() {
var s0, s1, s2, s3, s4;
s0 = peg$currPos;
s1 = peg$parsetag_start();
if (s1 !== null) {
s2 = [];
s3 = peg$parsepart();
while (s3 !== null) {
s2.push(s3);
s3 = peg$parsepart();
}
if (s2 !== null) {
s3 = peg$parsetag_end();
if (s3 !== null) {
peg$reportedPos = peg$currPos;
s4 = peg$c36(s1,s2,s3);
if (s4) {
s4 = peg$c7;
} else {
s4 = peg$c3;
}
if (s4 !== null) {
peg$reportedPos = s0;
s1 = peg$c37(s1,s2,s3);
if (s1 === null) {
peg$currPos = s0;
s0 = s1;
} else {
s0 = s1;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
} else {
peg$currPos = s0;
s0 = peg$c3;
}
return s0;
}
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;
}
peg$result = peg$startRuleFunction();
if (peg$result !== null && peg$currPos === input.length) {
return peg$result;
} else {
peg$cleanupExpected(peg$maxFailExpected);
peg$reportedPos = Math.max(peg$currPos, peg$maxFailPos);
throw new SyntaxError(
peg$maxFailExpected,
peg$reportedPos < input.length ? input.charAt(peg$reportedPos) : null,
peg$reportedPos,
peg$computePosDetails(peg$reportedPos).line,
peg$computePosDetails(peg$reportedPos).column
);
}
}
return {
SyntaxError: SyntaxError,
parse : parse
};
})();

174
lib/parser.js Normal file
View File

@ -0,0 +1,174 @@
// Generated by CoffeeScript 1.6.1
(function() {
var Contexter, _;
_ = require('underscore');
Contexter = (function() {
function Contexter(content) {
this.content = content;
this.stack = [this.content];
this.templates = {};
this.depth = 0;
}
Contexter.prototype.has_any = function(name) {
return _.find(this.stack, function(o) {
return _.has(o, name);
});
};
Contexter.prototype.has_any_one = function(name) {
var p;
p = this.has_any(name);
if (p) {
return p[name];
} else {
return null;
}
};
Contexter.prototype.has = function(name) {
if (this.stack[0][name] != null) {
return this.stack[0][name];
} else {
return null;
}
};
Contexter.prototype.get = function(name, alt) {
var p;
if (alt == null) {
alt = '';
}
p = this.has_any_one(name);
if (p && (_.isString(p) || _.isNumber(p))) {
return p;
}
return this.render(name);
};
Contexter.prototype.once = function(obj, cb) {
var r;
this.stack.unshift(obj);
this.depth++;
if (this.depth > 10) {
throw new Error('recursion-error');
}
r = cb(this);
this.stack.shift();
this.depth--;
return r;
};
Contexter.prototype.when = function(name, cb) {
var p;
p = this.has_any_one(name);
if (p) {
return cb(this);
} else {
return '';
}
};
Contexter.prototype["if"] = function(name, cb) {
var p;
p = this.has(name);
if (p) {
return cb(this);
} else {
return '';
}
};
Contexter.prototype.block = function(name, cb) {
var p;
p = this.has_any_one(name);
if (p && _.isObject(p)) {
return this.once(p, cb);
} else {
return '';
}
};
Contexter.prototype.many = function(name, cb) {
var ps,
_this = this;
ps = this.has(name);
if (!(ps && _.isArray(ps))) {
return "";
}
return (_.map(ps, function(p) {
return _this.once(p, cb);
})).join('');
};
Contexter.prototype.template = function(name, cb) {
this.templates[name] = cb;
return "";
};
Contexter.prototype.render = function(name) {
var ret;
if ((this.templates[name] != null) && _.isFunction(this.templates[name])) {
this.depth++;
if (this.depth > 10) {
throw new Error('recursion-error');
}
ret = this.templates[name](this);
this.depth--;
return ret;
} else {
return "";
}
};
return Contexter;
})();
module.exports = function(ast, data) {
var cmd, context, o;
context = new Contexter(data);
cmd = function(o) {
switch (o.unit) {
case 'variable':
return function(context) {
return context.get(o.name);
};
case 'text':
return function(context) {
return o.content;
};
case 'block':
return function(context) {
return context[o.type](o.name, function(context) {
var p;
return ((function() {
var _i, _len, _ref, _results;
_ref = o.content;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
p = _ref[_i];
_results.push(cmd(p)(context));
}
return _results;
})()).join("");
});
};
}
};
return ((function() {
var _i, _len, _ref, _results;
_ref = ast.content;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
o = _ref[_i];
_results.push(cmd(o)(context));
}
return _results;
})()).join("");
};
}).call(this);

20
lib/tumble.js Normal file
View File

@ -0,0 +1,20 @@
// Generated by CoffeeScript 1.6.1
(function() {
var engine, lexer, parse;
lexer = require('./lexer');
parse = require('./parser');
engine = require('./engine');
module.exports = {
tumble: lexer.parse,
parse: parse,
render: function(str, data) {
return parse(lexer.parse(str), data);
},
engine: engine
};
}).call(this);

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.1.0", "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 }; }

View File

@ -1,5 +1,4 @@
_ = require 'underscore' _ = require 'underscore'
util = require 'util'
class Contexter class Contexter
@ -39,12 +38,18 @@ class Contexter
@depth-- @depth--
r r
if: (name, cb) -> when: (name, cb) ->
# Execute and return this specifiecd block if and only if the # Execute and return this specified block if and only if the
# requested context is valid. # requested context is valid.
p = @has_any_one(name) p = @has_any_one(name)
if p then cb(@) 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 # Execute and return this specified block if and only if the
# requested context is valid and entrant. # requested context is valid and entrant.
@ -56,8 +61,7 @@ class Contexter
# the specified context if and only if the requested context # the specified context if and only if the requested context
# is valid and is iterable. # 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('')
template: (name, cb) -> template: (name, cb) ->
@ -66,8 +70,16 @@ class Contexter
return "" 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
""
# This is really the compiler at this point...
module.exports = (ast, data) -> module.exports = (ast, data) ->
context = new Contexter(data) context = new Contexter(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'))