Passes the first tests.
This commit is contained in:
parent
6e00e56e84
commit
a28a1f5d36
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2012 Elf M. Sternberg
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
13
Makefile
13
Makefile
|
@ -5,16 +5,19 @@ lib_objects:= $(subst src/, lib/, $(lib_sources:%.coffee=%.js))
|
||||||
|
|
||||||
default: build
|
default: build
|
||||||
|
|
||||||
build: $(lib_objects) lib/parser.js
|
build: $(lib_objects) lib/tokenizer.js
|
||||||
|
|
||||||
lib/parser.js: src/parser.peg
|
lib:
|
||||||
./node_modules/.bin/pegjs src/parser.peg lib/parser.js
|
mkdir -p lib
|
||||||
|
|
||||||
$(lib_objects): lib/%.js: src/%.coffee
|
lib/tumble.js: lib src/tumble.peg
|
||||||
|
./node_modules/.bin/pegjs src/tumble.peg lib/tumble.js
|
||||||
|
|
||||||
|
$(lib_objects): lib lib/%.js: src/%.coffee
|
||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
coffee -o $(@D) -c $<
|
coffee -o $(@D) -c $<
|
||||||
|
|
||||||
test: test/[0-9]*_mocha.coffee lib/tumble.js lib/parser.js
|
test: test/[0-9]*_mocha.coffee lib/tumble.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 $<
|
||||||
|
|
||||||
|
|
||||||
|
|
241
README.md
241
README.md
|
@ -1,120 +1,173 @@
|
||||||
|
# Tumble - Generalized, hyper-simple templating engine
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
I like Tumblr's templating system, with its simplicity and brevity.
|
||||||
|
The idea that, for small projects, one should embed everything in a
|
||||||
|
single page and switch sections on and off as needed appeals to me, so
|
||||||
|
I decided to write my own implementation of the Tumblr parser.
|
||||||
|
|
||||||
Tumble is an implementation of the Tumblr parser/compiler/renderer,
|
Tumble is an implementation of the Tumblr parser/compiler/renderer,
|
||||||
with keyword substitutions suitable to my needs on my story website.
|
with keyword substitutions suitable to my needs on my story website.
|
||||||
The idea is that the database side will produce an object consisting
|
The idea is that the database side will produce an object consisting
|
||||||
of the title of a series
|
of variable, object, and array substitutions, and that the
|
||||||
|
corresponding template will describe how to unpack and illustrate that
|
||||||
|
object.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
There are five kinds of objects in my templating language: plain text,
|
||||||
|
variables, "if", "block", and "many" sections.
|
||||||
|
|
||||||
|
### Plain Text
|
||||||
|
|
||||||
|
Plain text is just that, the text of your template without any
|
||||||
|
substitutions.
|
||||||
|
|
||||||
|
### Variable
|
||||||
|
|
||||||
|
Tumble tags are contained in { } entries. Sadly, there's no current
|
||||||
|
way to escape this.
|
||||||
|
|
||||||
|
A variable is a Tumble tag without a colon in it. It refers to a valid
|
||||||
|
name in the current context scope (more on that below) that is either
|
||||||
|
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.
|
||||||
|
|
||||||
|
If your datasource returns:
|
||||||
|
|
||||||
|
obj = { "name": "Mr. Smith"}
|
||||||
|
|
||||||
|
Then your template would use:
|
||||||
|
|
||||||
|
{if:name}Hello {name}!{/if:name}
|
||||||
|
|
||||||
|
### Block
|
||||||
|
|
||||||
|
A "block:<name>" section can contain other objects, but the entirety
|
||||||
|
of the section is only rendered if the current context scope contains
|
||||||
|
the current name, a value exists for that name, and the value is an
|
||||||
|
object itself. When the block is entered, the object referred to
|
||||||
|
becomes the top of the current context scope. You might use it to
|
||||||
|
render "next/prev" blocks in a webcomic.
|
||||||
|
|
||||||
|
If your datasource returns:
|
||||||
|
|
||||||
|
obj = { "next": {"title": "The Next Story"},
|
||||||
|
"prev": {"title": "The Previous Story"}}
|
||||||
|
|
||||||
|
Then your template would use:
|
||||||
|
|
||||||
|
{block:next}The next story is entitled {title}{/block:next}
|
||||||
|
{block:prev}The next story is entitled {title}{/block:prev}
|
||||||
|
|
||||||
|
### Many
|
||||||
|
|
||||||
|
A "many:<name>" section can contain other objects, but the entirety of
|
||||||
|
the section is only rendered if the current context scope contains the
|
||||||
|
current name, a value exists for that name, and the value is an array
|
||||||
|
that itself contains objects. When the block is entered, each object
|
||||||
|
in the named array is serially made the top object of the current
|
||||||
|
context scope, the section is rendered, and the object is popped off
|
||||||
|
the context scope stack once more. You might use it to render a
|
||||||
|
series of titles in a series:
|
||||||
|
|
||||||
|
If your datasource returns:
|
||||||
|
|
||||||
|
obj = {
|
||||||
|
"series":
|
||||||
|
[ {"title": "A Story"},
|
||||||
|
{"title": "A Second Story"},
|
||||||
|
{"title": "A Third Story"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Then you could render a list of your stories this way:
|
||||||
|
|
||||||
|
{if:series}
|
||||||
|
<h1>Table of Contents:</h1>
|
||||||
|
<ul>
|
||||||
|
{many:series}<li>{title}</li>{/many:series}
|
||||||
|
</ul>
|
||||||
|
{/if:series}
|
||||||
|
|
||||||
|
|
||||||
block:Series
|
### The current context scope
|
||||||
Must be found within a block:toc
|
|
||||||
It must contain the special tag {titles} as a child tag.
|
|
||||||
|
|
||||||
This will be rendered if this is a series page.
|
The Tumble parser is intended to render a website for series and
|
||||||
|
stories. Both of which have titles, so you might have an object that
|
||||||
|
says:
|
||||||
|
|
||||||
block:Subseries
|
obj = {
|
||||||
|
title: "An awesome series",
|
||||||
|
author: "Twilight Sparkle",
|
||||||
|
stories: [{title: "The first awesome story"},
|
||||||
|
{title: "The second awesome story"}]
|
||||||
|
}
|
||||||
|
|
||||||
Must be found within a block:toc
|
In both "block" and "many", the current context descends into these
|
||||||
It must contain the special tag {titles} as a child tag.
|
objects in a deterministic way. While inside a block or many, when
|
||||||
|
searching for a variable substitution (and *only* variable
|
||||||
|
substitutions), the context handler will scan upwards from the current
|
||||||
|
context scope to the root to find a possible substitution.
|
||||||
|
|
||||||
This will be rendered for any subseries of the main series. This
|
For example
|
||||||
will recurse to a maximum depth of four. Users can use clever CSS
|
|
||||||
to make this look awesome. There may be settings in the database
|
|
||||||
that prevent subseries recursion deliberately.
|
|
||||||
|
|
||||||
block:Story
|
<h1>{title}</h1>
|
||||||
Has two meanings, based on context.
|
{if:stories}
|
||||||
|
<p>Table of Contents:</p>
|
||||||
|
<ul>
|
||||||
|
{many:stories}<li>{title} by {author}</li>{/many:stories}
|
||||||
|
</ul>
|
||||||
|
{/if:stories}
|
||||||
|
|
||||||
If it is found in the *document* (i.e. *not* within a , it is treated at a story block,
|
The first "title" will be the series title, but the titles in the
|
||||||
and will only be rendered if this is a story page.
|
"many" block will be story titles, in order. Because each story does
|
||||||
|
not have an "author" block, the context will scan up to the parent
|
||||||
|
scope and find the author's name.
|
||||||
|
|
||||||
If it is found in a *series* or *subseries* block, it's contents
|
## Requirements
|
||||||
are rendered during the rendering of a series or subseries for
|
|
||||||
each story found.
|
|
||||||
|
|
||||||
block:Next
|
nodejs & npm.
|
||||||
Valid only in a block:story
|
|
||||||
Will render if there is a "next" story in the parent series.
|
|
||||||
|
|
||||||
block:Prev
|
Underscore is a dependency.
|
||||||
Valid only in a block:story
|
|
||||||
Will render if there is a "previous" story in the parent series.
|
|
||||||
|
|
||||||
block:Title
|
PegJS is a required build tool. Mocha & Chai are used for testing.
|
||||||
Will render if there is a title in the current context
|
|
||||||
|
|
||||||
block:Blurb
|
All of these are specified in the package.json file.
|
||||||
Will render if there is a blurb.
|
|
||||||
|
|
||||||
block:Excerpt
|
## LICENSE AND COPYRIGHT NOTICE: NO WARRANTY GRANTED OR IMPLIED
|
||||||
Will render if there is an excerpt
|
|
||||||
|
|
||||||
Variables:
|
Copyright (c) 2012 Elf M. Sternberg
|
||||||
|
|
||||||
Title
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
The title in the current context.
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
Body
|
The above copyright notice and this permission notice shall be included in
|
||||||
The body of the story. Only available in top-level block:story.
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
SeriesTitle
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
The title of the series in the current context.
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
- Elf M. Sternberg <elf@pendorwright.com>
|
||||||
|
|
||||||
MainSeriesTitle
|
|
||||||
The title of the top-level series for the current page.
|
|
||||||
|
|
||||||
AuthorsName
|
|
||||||
|
|
||||||
Blurb
|
|
||||||
Valid in a series or series/story
|
|
||||||
|
|
||||||
Excerpt
|
|
||||||
Valid in a series or series/story
|
|
||||||
|
|
||||||
URL
|
|
||||||
Context-sensitive. In a Story, refers to the URL used to access
|
|
||||||
that story. In a Next or Prev, refers to the URL of the next or
|
|
||||||
previous story, respectively.
|
|
||||||
|
|
||||||
SeriesURL
|
|
||||||
|
|
||||||
MainSeriesURL
|
|
||||||
|
|
||||||
AuthorsURL
|
|
||||||
|
|
||||||
# {block:Series}{URL}{Title}{Contents}{/block:Series}
|
|
||||||
|
|
||||||
# > Handle these first
|
|
||||||
|
|
||||||
Minus the actual content of a template, the HTML that we use to build
|
|
||||||
every page, a document tends to look like this:
|
|
||||||
|
|
||||||
{URL}
|
|
||||||
{SeriesTitle}
|
|
||||||
{AuthorName}
|
|
||||||
{AuthorDescription}
|
|
||||||
|
|
||||||
{block:IfSeries}
|
|
||||||
{Title}
|
|
||||||
{Description}
|
|
||||||
{block:TableOfContents}
|
|
||||||
{block:Story}{URL}{Title}{/block:Story}
|
|
||||||
{/block:TableOfContents}
|
|
||||||
{/block:IfSeries}
|
|
||||||
|
|
||||||
The important trick here is that the TableOfContents will be recursed
|
|
||||||
wherever the {Contents} block is seen, up to a maximum depth of four.
|
|
||||||
IfContents will be true if this is a subseries and there is content of
|
|
||||||
a subseries; the reason for this is to prevent the rendering of an
|
|
||||||
empty subseries.
|
|
||||||
|
|
||||||
|
|
||||||
{block:IfStory}
|
|
||||||
{Title}
|
|
||||||
{Body}
|
|
||||||
{block:Next}{URL}{Title}{/block:Next}
|
|
||||||
{block:Prev}{URL}{Title}{/block:Prev}
|
|
||||||
{Pubdate}
|
|
||||||
{block:IfLicense}{License}{/block:IfLicense}
|
|
||||||
{Copyright}
|
|
||||||
{block:/IfStory}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -29,9 +29,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pegjs": "0.7.x",
|
"pegjs": "0.7.x",
|
||||||
"mocha": "1.8.x",
|
"mocha": "1.8.x",
|
||||||
"chai": "1.5.x",
|
"chai": "1.5.x"
|
||||||
"coffee-script": "1.6.x",
|
|
||||||
"docco": "0.3.x"
|
|
||||||
},
|
},
|
||||||
"keywords": []
|
"keywords": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
document
|
|
||||||
= p:part* { return p }
|
|
||||||
|
|
||||||
part
|
|
||||||
= text / variable / block
|
|
||||||
|
|
||||||
text
|
|
||||||
= b:(!tag c:. {return c})+
|
|
||||||
{ return { type: "text", text: b.join('') }; }
|
|
||||||
|
|
||||||
variable "variable"
|
|
||||||
= t:tag_start rd
|
|
||||||
{ return { type: "variable", name: t }; }
|
|
||||||
|
|
||||||
tag_start "tag_start"
|
|
||||||
= ld n:tagname
|
|
||||||
{ return n; }
|
|
||||||
|
|
||||||
tagname "tagname"
|
|
||||||
= t:[a-zA-Z]+
|
|
||||||
{ return t.join(''); }
|
|
||||||
|
|
||||||
block "block"
|
|
||||||
= t:block_tag_start p:part* n:block_end_tag
|
|
||||||
&{ return t == n }
|
|
||||||
{ return { type: "block", name: n, data: p }; }
|
|
||||||
|
|
||||||
block_tag_start "tag_start"
|
|
||||||
= ld "block:" n:tagname rd
|
|
||||||
{ return n; }
|
|
||||||
|
|
||||||
block_end_tag
|
|
||||||
= ld "/block:" n:tagname rd
|
|
||||||
{ return n; }
|
|
||||||
|
|
||||||
tag
|
|
||||||
= ld (!rd !eol [a-zA-Z\:\/])+ rd
|
|
||||||
|
|
||||||
ld
|
|
||||||
= "{"
|
|
||||||
|
|
||||||
rd
|
|
||||||
= "}"
|
|
||||||
|
|
||||||
eol
|
|
||||||
= "\n"
|
|
||||||
/ "\r\n"
|
|
||||||
/ "\r"
|
|
||||||
/ "\u2028"
|
|
||||||
/ "\u2029"
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
tumble = require('./parser')
|
|
||||||
util = require('util')
|
|
||||||
|
|
||||||
module.exports = (template) ->
|
|
||||||
|
|
||||||
ast = tumble.parse(template)
|
|
||||||
|
|
||||||
# Using the AST, return a function that will render each component
|
|
||||||
# of the AST out, as long as the data provided to the AST makes
|
|
||||||
# sens.
|
|
||||||
#
|
|
||||||
|
|
||||||
(content) ->
|
|
||||||
subtypes = (name) ->
|
|
||||||
return 'cond'
|
|
||||||
|
|
||||||
handler = (content, ast) ->
|
|
||||||
|
|
||||||
isLegal = (name) -> true
|
|
||||||
|
|
||||||
{
|
|
||||||
'text': () ->
|
|
||||||
ast.text
|
|
||||||
|
|
||||||
'variable': () ->
|
|
||||||
return '' if not (isLegal(ast.name) and content.hasOwnProperty(ast.name))
|
|
||||||
content[ast.name]
|
|
||||||
|
|
||||||
'block': () ->
|
|
||||||
return '' if not (isLegal(ast.name) and content.hasOwnProperty(ast.name))
|
|
||||||
f = {
|
|
||||||
'cond': () -> handler(content[ast.name], ast.data)
|
|
||||||
'loop': () -> (handler(c, ast.data) for c in content[ast.name])
|
|
||||||
}[subtypes(ast.name)]
|
|
||||||
console.log("F:", f)
|
|
||||||
f()
|
|
||||||
}[obj.type]()
|
|
||||||
|
|
||||||
(handler(i) for i in ast).join("")
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
{
|
||||||
|
var _ = require('underscore');
|
||||||
|
var depth = 0;
|
||||||
|
|
||||||
|
var Contexter = function(c) {
|
||||||
|
this.content = c
|
||||||
|
this.stack = [c];
|
||||||
|
this.templates = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
_.extend(Contexter.prototype, {
|
||||||
|
has_any: function(name) {
|
||||||
|
return _.find(this.stack, function(o) { return _.has(o, name); });
|
||||||
|
},
|
||||||
|
|
||||||
|
has: function(name) {
|
||||||
|
if (typeof this.stack[0][name] != 'undefined') {
|
||||||
|
return this.stack[0][name];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
get: function(name) {
|
||||||
|
var p = this.has_any(name);
|
||||||
|
if (p && (_.isString(p[name]) || _.isNumber(p[name]))) {
|
||||||
|
return p[name];
|
||||||
|
}
|
||||||
|
if (arguments.length > 1) {
|
||||||
|
return arguments[1];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
once: function(obj, cb) {
|
||||||
|
this.stack.unshift(obj);
|
||||||
|
var r = cb(this);
|
||||||
|
this.stack.shift();
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
|
||||||
|
if: function(name, cb) {
|
||||||
|
var p = this.has_any(name);
|
||||||
|
if (p && p[name]) {
|
||||||
|
return cb(this);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
descend: function(name, cb) {
|
||||||
|
var p = this.has(name);
|
||||||
|
if (p && _.isObject(p)) {
|
||||||
|
return this.once(p, cb);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
many: function(name, cb) {
|
||||||
|
var ps = this.has(name),
|
||||||
|
_this = this;
|
||||||
|
if (ps && _.isArray(ps)) {
|
||||||
|
return _.map(ps, function(p) {
|
||||||
|
return _this.once(p, cb);
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
templatize: function(name, cb) {
|
||||||
|
this.templates[name] = cb;
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
template_render: function(name) {
|
||||||
|
if (this.templates[name] && _.isFunction(this.templates[name])) {
|
||||||
|
return this.templates[name](this);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
= ps:document_part* {
|
||||||
|
return function(content) {
|
||||||
|
var context = new Contexter(content);
|
||||||
|
return _.map(ps, function(p) {
|
||||||
|
return p(context);
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
document_part
|
||||||
|
= iterative / descendant / conditional / variable / text
|
||||||
|
|
||||||
|
|
||||||
|
text
|
||||||
|
= b:(!tag c:. {return c})+ {
|
||||||
|
return (function() {
|
||||||
|
var t = b.join("");
|
||||||
|
return function(content) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
variable "variable"
|
||||||
|
= t:tag_start rd {
|
||||||
|
return function(content) { return content.get(t, ""); };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
simple_part
|
||||||
|
= descendant / variable / conditional / text
|
||||||
|
|
||||||
|
|
||||||
|
conditional
|
||||||
|
= t:ifblock_tag_start ps:simple_part* n:ifblock_tag_end
|
||||||
|
&{ return (t == n) }
|
||||||
|
{
|
||||||
|
return function(content) {
|
||||||
|
return content.if(t, function(c) {
|
||||||
|
return _.map(ps, function(p) {
|
||||||
|
return p(c);
|
||||||
|
}).join('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ifblock_tag_start "tag_start"
|
||||||
|
= ld "if:" n:tagname rd
|
||||||
|
{ return n; }
|
||||||
|
|
||||||
|
|
||||||
|
ifblock_tag_end
|
||||||
|
= ld "/if:" n:tagname rd
|
||||||
|
{ return n; }
|
||||||
|
|
||||||
|
|
||||||
|
descendant
|
||||||
|
= n:blockblock_tag_start ps:simple_part* t:blockblock_tag_end
|
||||||
|
&{ return (t == n) }
|
||||||
|
{
|
||||||
|
return function(content) {
|
||||||
|
return content.descend(t, function(c) {
|
||||||
|
return _.map(ps, function(p) {
|
||||||
|
return p(c);
|
||||||
|
}).join('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
blockblock_tag_start
|
||||||
|
= ld "block:" n:tagname rd
|
||||||
|
{ return n; }
|
||||||
|
|
||||||
|
|
||||||
|
blockblock_tag_end
|
||||||
|
= ld "/block:" n:tagname rd
|
||||||
|
{ return n; }
|
||||||
|
|
||||||
|
|
||||||
|
iterative
|
||||||
|
= t:loopblock_tag_start ps:simple_part* n:loopblock_tag_end
|
||||||
|
&{ return t == n }
|
||||||
|
{
|
||||||
|
return function(content) {
|
||||||
|
return content.many(t, function(c) {
|
||||||
|
return _.map(ps, function(p) {
|
||||||
|
return p(c);
|
||||||
|
}).join('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loopblock_tag_start "tag_start"
|
||||||
|
= ld "many:" n:tagname rd
|
||||||
|
{ return n; }
|
||||||
|
|
||||||
|
|
||||||
|
loopblock_tag_end
|
||||||
|
= ld "/many:" n:tagname rd
|
||||||
|
{ return n; }
|
||||||
|
|
||||||
|
|
||||||
|
tag_start "tag_start"
|
||||||
|
= ld n:tagname
|
||||||
|
{ return n; }
|
||||||
|
|
||||||
|
|
||||||
|
tagname "tagname"
|
||||||
|
= t:[a-zA-Z]+
|
||||||
|
{ return t.join(''); }
|
||||||
|
|
||||||
|
|
||||||
|
tag
|
||||||
|
= ld (!rd !eol [a-zA-Z\:\/])+ rd
|
||||||
|
|
||||||
|
|
||||||
|
ld
|
||||||
|
= "{"
|
||||||
|
|
||||||
|
|
||||||
|
rd
|
||||||
|
= "}"
|
||||||
|
|
||||||
|
|
||||||
|
eol
|
||||||
|
= "\n"
|
||||||
|
/ "\r\n"
|
||||||
|
/ "\r"
|
||||||
|
/ "\u2028"
|
||||||
|
/ "\u2029"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ assert = chai.assert
|
||||||
expect = chai.expect
|
expect = chai.expect
|
||||||
should = chai.should()
|
should = chai.should()
|
||||||
|
|
||||||
tumble = require('../lib/tumble')
|
tumble = require('../lib/tumble').parse;
|
||||||
|
|
||||||
test_data = [
|
test_data = [
|
||||||
{
|
{
|
||||||
|
@ -30,12 +30,60 @@ test_data = [
|
||||||
'description': "two simple substitutions"
|
'description': "two simple substitutions"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
'input': '<ul>{block:Stories}{Title}{/block:Stories}</ul>'
|
'input': '<ul>{if:title}{title}BBB{/if:title}</ul>'
|
||||||
'output': '<ul>AAABBB</ul>'
|
'output': '<ul>AAABBB</ul>'
|
||||||
'data': {'stories': {'title': 'AAA'}},
|
'data': {'title': 'AAA'}
|
||||||
'description': "a conditional block"
|
'description': "a conditional block"
|
||||||
}]
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
'input': '<ul>{if:title}{title}BBB{/if:title}</ul>'
|
||||||
|
'output': '<ul></ul>'
|
||||||
|
'data': {'title': ''}
|
||||||
|
'description': "a conditional block with no input"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
'input': '<ul>{block:stories}{title}{/block:stories}</ul>'
|
||||||
|
'output': '<ul></ul>'
|
||||||
|
'data': {'stories': {'title': ''}}
|
||||||
|
'description': "a descendent block"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
'input': '<ul>{block:stories}{title}BBB{/block:stories}</ul>'
|
||||||
|
'output': '<ul>AAABBB</ul>'
|
||||||
|
'data': {'stories': {'title': 'AAA'}}
|
||||||
|
'description': "a descendent block 2"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
'input': '<ul>{many:stories}{title}{/many:stories}</ul>'
|
||||||
|
'output': '<ul></ul>'
|
||||||
|
'data': {'stories': [{'title': ''}]}
|
||||||
|
'description': "an iterative block"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
'input': '<ul>{many:stories}{title}BBB{/many:stories}</ul>'
|
||||||
|
'output': '<ul>AAABBBCCCBBB</ul>'
|
||||||
|
'data': {'stories': [{'title': 'AAA'}, {'title': 'CCC'}]},
|
||||||
|
'description': "an iterative block 2"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
'input': '<ul>{author}{many:stories}{title}BBB{author}{/many:stories}</ul>'
|
||||||
|
'output': '<ul>DDDAAABBBDDDCCCBBBDDD</ul>'
|
||||||
|
'data': {'author': 'DDD', 'stories': [{'title': 'AAA'}, {'title': 'CCC'}]},
|
||||||
|
'description': "an iterative block with ascent"
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
describe "Basic Functionality", ->
|
describe "Basic Functionality", ->
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" type="text/css" href="css/aimee.css" title="Style"></link>
|
|
||||||
<title>Elf Sternberg | Aimee' | {block:title}{title}{/block:title}</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="container">
|
|
||||||
<div id="header">
|
|
||||||
<h1>Aimee</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{block:index}
|
|
||||||
<div id="content">
|
|
||||||
|
|
||||||
<p>The Aimee' series grew out of a longstanding desire on my part to
|
|
||||||
write stories outside of the comforting future depicted in the
|
|
||||||
Journal Entries. The fantasy setting allowed me to get away with
|
|
||||||
a number of tropes and scenes that I would otherwise never inflict
|
|
||||||
on my favorite characters. It has, like the Journal Entries,
|
|
||||||
grown from one kind of fantasy into another, one where meaning
|
|
||||||
matters.</p>
|
|
||||||
|
|
||||||
<div id="toc">
|
|
||||||
{block:contents}
|
|
||||||
{block:series}
|
|
||||||
<h2>{title}</h2>
|
|
||||||
{/block:series}
|
|
||||||
{block:story}
|
|
||||||
<div class="title"><a href="{url}" class="tanch"> </a></div>
|
|
||||||
{/block:story}
|
|
||||||
{/block:contents}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/block:index}
|
|
||||||
|
|
||||||
{block:story}
|
|
||||||
<h1>{title}</h1>
|
|
||||||
{body}
|
|
||||||
|
|
||||||
<div id="nextprevblock">
|
|
||||||
{block:next}<h4>Next: <a href="{url}">{title}</a></h4>{/block:next}
|
|
||||||
{block:prev}<h4>Previous: <a href="{url}">{title}</a></h4>{/block:prev}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="copytitle">
|
|
||||||
<h5>{title}</h5>
|
|
||||||
|
|
||||||
<div id="canchor">
|
|
||||||
<a rel="license" href="http://creativecommons.org/licenses/by-nd-nc/1.0/"><img alt=
|
|
||||||
"Creative Commons License" src="images/somerights20.png"></a>
|
|
||||||
</div>is copyright © {pubdate|"F j, Y"} Elf Mathieu Sternberg and is available
|
|
||||||
under a <a rel="license" href=
|
|
||||||
"http://creativecommons.org/licenses/by-nd-nc/1.0/">Creative Commons License</a>.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- /Creative Commons License --><!--
|
|
||||||
<rdf:RDF xmlns="http://web.resource.org/cc/"
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
||||||
<Work rdf:about="">
|
|
||||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/Text" />
|
|
||||||
<license rdf:resource="http://creativecommons.org/licenses/by-nd-nc/1.0/" />
|
|
||||||
</Work>
|
|
||||||
|
|
||||||
<License rdf:about="http://creativecommons.org/licenses/by-nd-nc/1.0/">
|
|
||||||
<permits rdf:resource="http://web.resource.org/cc/Reproduction" />
|
|
||||||
<permits rdf:resource="http://web.resource.org/cc/Distribution" />
|
|
||||||
<requires rdf:resource="http://web.resource.org/cc/Notice" />
|
|
||||||
<requires rdf:resource="http://web.resource.org/cc/Attribution" />
|
|
||||||
<prohibits rdf:resource="http://web.resource.org/cc/CommercialUse" />
|
|
||||||
</License>
|
|
||||||
</rdf:RDF>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- Piwik -->
|
|
||||||
<script type="text/javascript">
|
|
||||||
var pkBaseURL = (("https:" == document.location.protocol) ? "https://elfsternberg.com/piwik/" : "http://elfsternberg.com/piwik/");
|
|
||||||
document.write(unescape("%3Cscript src='" + pkBaseURL + "piwik.js' type='text/javascript'%3E%3C/script%3E"));
|
|
||||||
</script><script type="text/javascript">
|
|
||||||
try {
|
|
||||||
var piwikTracker = Piwik.getTracker(pkBaseURL + "piwik.php", 2);
|
|
||||||
piwikTracker.trackPageView();
|
|
||||||
piwikTracker.enableLinkTracking();
|
|
||||||
} catch( err ) {}
|
|
||||||
</script><noscript><p><img src="http://elfsternberg.com/piwik/piwik.php?idsite=2" style="border:0" alt="" /></p></noscript>
|
|
||||||
<!-- End Piwik Tag -->
|
|
||||||
</div>
|
|
||||||
{/block:story}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
Loading…
Reference in New Issue