mustache-d-0.1.3/ 0000775 0000000 0000000 00000000000 13040475462 0013552 5 ustar 00root root 0000000 0000000 mustache-d-0.1.3/.gitignore 0000664 0000000 0000000 00000000105 13040475462 0015536 0 ustar 00root root 0000000 0000000 *.[oa]
*.so
*.lib
*.dll
*.exe
.dub/
__test__*__
dub.selections.json
mustache-d-0.1.3/.travis.yml 0000664 0000000 0000000 00000000146 13040475462 0015664 0 ustar 00root root 0000000 0000000 language: d
d:
- dmd
- gdc
- ldc
branches:
only:
- master
notifications:
email: true
mustache-d-0.1.3/README.markdown 0000664 0000000 0000000 00000002505 13040475462 0016255 0 ustar 00root root 0000000 0000000 [](https://travis-ci.org/repeatedly/mustache-d)
# Mustache for D
Mustache is a push-strategy (a.k.a logic-less) template engine.
# Features
* Variables
* Sections
* Lists
* Non-False Values
* Lambdas(half implementation)
* Inverted
* Comments
* Partials
# Usage
See example directory and DDoc comments.
## Mustache.Option
* ext(string)
File extenstion of Mustache template. Default is "mustache".
* path(string)
root path to read Mustache template. Default is "."(current directory).
* findPath(string delegate(string))
callback to dynamically find the path do a Mustache template. Default is none. Mutually exclusive with the `path` option.
* level(CacheLevel)
Cache level for Mustache's in-memory cache. Default is "check". See DDoc.
* handler(String delegate())
Callback delegate for unknown name. handler is called if Context can't find name. Image code is below.
if (followable context is nothing)
return handler is null ? null : handler();
# TODO
Working on CTFE.
# Link
* [{{ mustache }}](http://mustache.github.com/)
* [mustache(5) -- Logic-less templates.](http://mustache.github.com/mustache.5.html)
man page
# Copyright
Copyright (c) 2011 Masahiro Nakagawa
Distributed under the Boost Software License, Version 1.0.
mustache-d-0.1.3/dub.json 0000664 0000000 0000000 00000000646 13040475462 0015225 0 ustar 00root root 0000000 0000000 {
"name": "mustache-d",
"description": "Mustache template engine for D.",
"authors": ["Masahiro Nakagawa"],
"homepage": "https://github.com/repeatedly/mustache-d",
"license": "Boost Software License, Version 1.0",
"copyright": "Copyright (c) 2011-. Masahiro Nakagawa",
"buildRequirements": ["silenceDeprecations"],
"importPaths": ["src"],
"targetType": "library"
}
mustache-d-0.1.3/example/ 0000775 0000000 0000000 00000000000 13040475462 0015205 5 ustar 00root root 0000000 0000000 mustache-d-0.1.3/example/basic.d 0000664 0000000 0000000 00000000562 13040475462 0016436 0 ustar 00root root 0000000 0000000 import mustache;
import std.stdio;
alias MustacheEngine!(string) Mustache;
void main()
{
Mustache mustache;
auto context = new Mustache.Context;
context["name"] = "Chris";
context["value"] = 10000;
context["taxed_value"] = 10000 - (10000 * 0.4);
context.useSection("in_ca");
stdout.rawWrite(mustache.render("example/basic", context));
}
mustache-d-0.1.3/example/basic.mustache 0000664 0000000 0000000 00000000150 13040475462 0020015 0 ustar 00root root 0000000 0000000 Hello {{name}}
You have just won ${{value}}!
{{#in_ca}}
Well, ${{taxed_value}}, after taxes.
{{/in_ca}}
mustache-d-0.1.3/example/projects.d 0000664 0000000 0000000 00000002105 13040475462 0017201 0 ustar 00root root 0000000 0000000 // This example from https://github.com/defunkt/mustache/blob/master/examples/projects.mustache
import mustache;
import std.stdio;
struct Project
{
string name;
string url;
string description;
}
static Project[] projects = [
Project("dmd", "https://github.com/dlang/dmd", "dmd D Programming Language compiler"),
Project("druntime", "https://github.com/dlang/druntime", "Low level runtime library for the D programming language"),
Project("phobos", "https://github.com/dlang/phobos", "The standard library of the D programming language")
];
void main()
{
alias MustacheEngine!(string) Mustache;
Mustache mustache;
auto context = new Mustache.Context;
context["width"] = 4968;
foreach (ref project; projects) {
auto sub = context.addSubContext("projects");
sub["name"] = project.name;
sub["url"] = project.url;
sub["description"] = project.description;
}
mustache.path = "example";
mustache.level = Mustache.CacheLevel.no;
stdout.rawWrite(mustache.render("projects", context));
}
mustache-d-0.1.3/example/projects.mustache 0000664 0000000 0000000 00000000674 13040475462 0020600 0 ustar 00root root 0000000 0000000
projects
projects
mustache-d-0.1.3/example/whitespace_spec.d 0000664 0000000 0000000 00000003402 13040475462 0020517 0 ustar 00root root 0000000 0000000 import mustache;
import std.stdio;
alias MustacheEngine!(string) Mustache;
void main()
{
Mustache mustache;
auto context = new Mustache.Context;
context.useSection("boolean");
// from https://github.com/mustache/spec/blob/master/specs/sections.yml
assert(mustache.renderString(" | {{#boolean}}\t|\t{{/boolean}} | \n", context) == " | \t|\t | \n");
assert(mustache.renderString(" | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n", context) == " | \n | \n");
assert(mustache.renderString(" {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n", context) == " YES\n GOOD\n");
assert(mustache.renderString("#{{#boolean}}\n/\n {{/boolean}}", context) == "#\n/\n");
assert(mustache.renderString(" {{#boolean}}\n#{{/boolean}}\n/", context) == "#\n/");
auto expected = `This Is
A Line`;
auto t = `This Is
{{#boolean}}
{{/boolean}}
A Line`;
assert(mustache.renderString(t, context) == expected);
auto t2 = `This Is
{{#boolean}}
{{/boolean}}
A Line`;
assert(mustache.renderString(t, context) == expected);
// TODO: \r\n support
issue2();
issue9();
}
void issue2()
{
Mustache mustache;
auto context = new Mustache.Context;
context["module_name"] = "mustache";
context.useSection("static_imports");
auto text = `module {{module_name}};
{{#static_imports}}
/*
* Auto-generated static imports
*/
{{/static_imports}}`;
assert(mustache.renderString(text, context) == `module mustache;
/*
* Auto-generated static imports
*/
`);
}
void issue9()
{
Mustache mustache;
auto context = new Mustache.Context;
context.useSection("section");
auto text = `FOO
{{#section}}BAR{{/section}}`;
assert(mustache.renderString(text, context) == `FOO
BAR`);
}
mustache-d-0.1.3/meson.build 0000664 0000000 0000000 00000001233 13040475462 0015713 0 ustar 00root root 0000000 0000000 project('mustache-d', 'd')
project_version = '0.1.1'
project_soversion = '0'
src_dir = include_directories('src/')
pkgc = import('pkgconfig')
mustache_src = [
'src/mustache.d'
]
install_headers(mustache_src, subdir: 'd/mustache-d')
mustache_lib = static_library('mustache-d',
[mustache_src],
include_directories: [src_dir],
install: true,
version: project_version,
soversion: project_soversion
)
pkgc.generate(name: 'mustache-d',
libraries: mustache_lib,
subdirs: 'd/mustache-d',
version: project_version,
description: 'Mustache template engine for D.'
)
mustache-d-0.1.3/mustache.html 0000664 0000000 0000000 00000055106 13040475462 0016260 0 ustar 00root root 0000000 0000000
mustache - D Programming Language - Digital Mars
mustache
Mustache template engine for D
Implemented according to
mustach(5) .
License: Boost License 1.0 .
Authors: Masahiro Nakagawa
class MustacheException : object.Exception;
Exception for Mustache
struct MustacheEngine (String = string) if (isSomeString!(String));
Core implementation of Mustache
String parameter means a string type to render.
Example:
alias MustacheEngine !(string) Mustache;
Mustache mustache;
auto context = new Mustache.Context;
context["name" ] = "Chris" ;
context["value" ] = 10000;
context["taxed_value" ] = 10000 - (10000 * 0.4);
context.useSection("in_ca" );
write(mustache.render("sample" , context));
sample.mustache:
Hello {{name}}
You have just won ${{value}}!
{{#in_ca}}
Well, ${{taxed_value}}, after taxes.
{{/in_ca}}
Output:
Hello Chris
You have just won 0000!
Well, 000, after taxes.
enum CacheLevel ;
Cache level for compile result
no
No caching
check
Caches compiled result and checks the freshness of template
once
Caches compiled result but not check the freshness of template
struct Option ;
Options for rendering
string ext ;
template file extenstion
string path ;
root path for template file searching
CacheLevel level ;
See CacheLevel
Handler handler ;
Callback handler for unknown name
class Context ;
Mustache context for setting values
Variable:
context["name" ] = "Chirs"
Lists section("addSubContext" name is drived from ctemplate's API):
foreach (name; ["resque" , "hub" , "rip" ]) {
auto sub = context.addSubContext("repo" );
sub["name" ] = name;
}
Variable section:
context["person?" ] = ["name" : "Jon" ];
Lambdas section:
context["Wrapped" ] = (string str) { return "<b>" ~ str ~ "</b>" ; };
Inverted section:
context["foo" ] = "bar" ;
const nothrow String opIndex (in String key );
Gets key 's value. This method does not search Section.
Parameters: String key
key string to search
Returns: a key associated value.
Throws: a RangeError if key does not exist.
void opIndexAssign (T)(T value , in String key );
Assigns value (automatically convert to String) to key field.
If you try to assign associative array or delegate,
This method assigns value as Section.
Parameters: value
some type value to assign
key
key string to assign
void useSection (in String key );
Enable key 's section.
Parameters: String key
key string to enable
NOTE:
I don't like this method, but D's typing can't well-handle Ruby's typing.
Context addSubContext (in String key , lazy size_t size = 1);
Adds new context to key 's section. This method overwrites with
list type if you already assigned other type to key 's section.
Parameters: String key
key string to add
size_t size
reserve size for avoiding reallocation
Returns: new Context object that added to key section list.
const const(string) ext ();
void ext (string ext );
Property for template extenstion
const const(string) path ();
void path (string path );
Property for template searche path
const const(CacheLevel) level ();
void level (CacheLevel level );
Property for cache level
const const(Handler) handler ();
void handler (Handler handler );
Property for callback handler
String render (in string name , in Context context );
Renders name template with context .
This method stores compile result in memory if you set check or once CacheLevel.
Parameters: string name
template name without extenstion
Context context
Mustache context for rendering
Returns: rendered result.
Throws: object.Exception if String alignment is mismatched from template file.
String renderString (in String src , in Context context );
string version of render .
Copyright Masahiro Nakagawa 2011-.
|
Page generated by
Ddoc .
mustache-d-0.1.3/posix.mak 0000664 0000000 0000000 00000001350 13040475462 0015405 0 ustar 00root root 0000000 0000000 # build mode: 32bit or 64bit
MODEL ?= $(shell getconf LONG_BIT)
DMD ?= dmd
LIB = libmustache.a
DFLAGS = -Isrc -m$(MODEL) -w -d #-property
ifeq ($(BUILD),debug)
DFLAGS += -g -debug
else
DFLAGS += -O -release -nofloat -inline -noboundscheck
endif
NAMES = mustache
FILES = $(addsuffix .d, $(NAMES))
SRCS = $(addprefix src/, $(FILES))
# DDoc
DOCS = $(addsuffix .html, $(NAMES))
DDOCFLAGS = -Dd. -c -o- std.ddoc -Isrc
target: $(LIB)
$(LIB):
$(DMD) $(DFLAGS) -lib -of$(LIB) $(SRCS)
doc:
$(DMD) $(DDOCFLAGS) $(SRCS)
clean:
rm $(DOCS) $(LIB)
MAIN_FILE = "empty_mustache_unittest.d"
unittest:
echo 'import mustache; void main(){}' > $(MAIN_FILE)
$(DMD) $(DFLAGS) -unittest -of$(LIB) $(SRCS) -run $(MAIN_FILE)
rm $(MAIN_FILE)
mustache-d-0.1.3/src/ 0000775 0000000 0000000 00000000000 13040475462 0014341 5 ustar 00root root 0000000 0000000 mustache-d-0.1.3/src/mustache.d 0000664 0000000 0000000 00000126674 13040475462 0016337 0 ustar 00root root 0000000 0000000 /**
* Mustache template engine for D
*
* Implemented according to mustach(5) .
*
* Copyright: Copyright Masahiro Nakagawa 2011-.
* License: Boost License 1.0 .
* Authors: Masahiro Nakagawa
*/
module mustache;
import std.algorithm : all;
import std.array; // empty, back, popBack, appender
import std.conv; // to
import std.datetime; // SysTime (I think std.file should import std.datetime as public)
import std.file; // read, timeLastModified
import std.path; // buildPath
import std.range; // isOutputRange
import std.string; // strip, chomp, stripLeft
import std.traits; // isSomeString, isAssociativeArray
static import std.ascii; // isWhite;
version(unittest) import core.thread;
/**
* Exception for Mustache
*/
class MustacheException : Exception
{
this(string messaage)
{
super(messaage);
}
}
/**
* Core implementation of Mustache
*
* $(D_PARAM String) parameter means a string type to render.
*
* Example:
* -----
* alias MustacheEngine!(string) Mustache;
*
* Mustache mustache;
* auto context = new Mustache.Context;
*
* context["name"] = "Chris";
* context["value"] = 10000;
* context["taxed_value"] = 10000 - (10000 * 0.4);
* context.useSection("in_ca");
*
* write(mustache.render("sample", context));
* -----
* sample.mustache:
* -----
* Hello {{name}}
* You have just won ${{value}}!
* {{#in_ca}}
* Well, ${{taxed_value}}, after taxes.
* {{/in_ca}}
* -----
* Output:
* -----
* Hello Chris
* You have just won $10000!
* Well, $6000, after taxes.
* -----
*/
struct MustacheEngine(String = string) if (isSomeString!(String))
{
static assert(!is(String == wstring), "wstring is unsupported. It's a buggy!");
public:
alias String delegate(String) Handler;
alias string delegate(string) FindPath;
/**
* Cache level for compile result
*/
static enum CacheLevel
{
no, /// No caching
check, /// Caches compiled result and checks the freshness of template
once /// Caches compiled result but not check the freshness of template
}
/**
* Options for rendering
*/
static struct Option
{
string ext = "mustache"; /// template file extenstion
string path = "."; /// root path for template file searching
FindPath findPath; /// dynamically finds the path for a name
CacheLevel level = CacheLevel.check; /// See CacheLevel
Handler handler; /// Callback handler for unknown name
}
/**
* Mustache context for setting values
*
* Variable:
* -----
* //{{name}} to "Chris"
* context["name"] = "Chirs"
* -----
*
* Lists section("addSubContext" name is drived from ctemplate's API):
* -----
* //{{#repo}}
* //{{name}}
* //{{/repo}}
* // to
* //resque
* //hub
* //rip
* foreach (name; ["resque", "hub", "rip"]) {
* auto sub = context.addSubContext("repo");
* sub["name"] = name;
* }
* -----
*
* Variable section:
* -----
* //{{#person?}}Hi {{name}}{{/person?}} to "Hi Jon"
* context["person?"] = ["name" : "Jon"];
* -----
*
* Lambdas section:
* -----
* //{{#wrapped}}awesome{{/wrapped}} to "awesome "
* context["Wrapped"] = (string str) { return "" ~ str ~ " "; };
* -----
*
* Inverted section:
* -----
* //{{#repo}}{{name}} {{/repo}}
* //{{^repo}}No repos :({{/repo}}
* // to
* //No repos :(
* context["foo"] = "bar"; // not set to "repo"
* -----
*/
static final class Context
{
private:
enum SectionType
{
nil, use, var, func, list
}
struct Section
{
SectionType type;
union
{
String[String] var;
String delegate(String) func; // func type is String delegate(String) delegate()?
Context[] list;
}
@trusted nothrow
{
this(bool u)
{
type = SectionType.use;
}
this(String[String] v)
{
type = SectionType.var;
var = v;
}
this(String delegate(String) f)
{
type = SectionType.func;
func = f;
}
this(Context c)
{
type = SectionType.list;
list = [c];
}
this(Context[] c)
{
type = SectionType.list;
list = c;
}
}
/* nothrow : AA's length is not nothrow */
@trusted @property
bool empty() const
{
final switch (type) {
case SectionType.nil:
return true;
case SectionType.use:
return false;
case SectionType.var:
return !var.length; // Why?
case SectionType.func:
return func is null;
case SectionType.list:
return !list.length;
}
}
/* Convenience function */
@safe @property
static Section nil() nothrow
{
Section result;
result.type = SectionType.nil;
return result;
}
}
const Context parent;
String[String] variables;
Section[String] sections;
public:
@safe
this(in Context context = null) nothrow
{
parent = context;
}
/**
* Gets $(D_PARAM key)'s value. This method does not search Section.
*
* Params:
* key = key string to search
*
* Returns:
* a $(D_PARAM key) associated value.
*
* Throws:
* a RangeError if $(D_PARAM key) does not exist.
*/
@safe
String opIndex(in String key) const nothrow
{
return variables[key];
}
/**
* Assigns $(D_PARAM value)(automatically convert to String) to $(D_PARAM key) field.
*
* If you try to assign associative array or delegate,
* This method assigns $(D_PARAM value) as Section.
*
* Arrays of Contexts are accepted, too.
*
* Params:
* value = some type value to assign
* key = key string to assign
*/
@trusted
void opIndexAssign(T)(T value, in String key)
{
static if (isAssociativeArray!(T))
{
static if (is(T V : V[K], K : String))
{
String[String] aa;
static if (is(V == String))
aa = value;
else
foreach (k, v; value) aa[k] = to!String(v);
sections[key] = Section(aa);
}
else static assert(false, "Non-supported Associative Array type");
}
else static if (isCallable!T)
{
import std.functional : toDelegate;
auto v = toDelegate(value);
static if (is(typeof(v) D == S delegate(S), S : String))
sections[key] = Section(v);
else static assert(false, "Non-supported delegate type");
}
else static if (isArray!T && !isSomeString!T)
{
static if (is(T : Context[]))
sections[key] = Section(value);
else static assert(false, "Non-supported array type");
}
else
{
variables[key] = to!String(value);
}
}
/**
* Enable $(D_PARAM key)'s section.
*
* Params:
* key = key string to enable
*
* NOTE:
* I don't like this method, but D's typing can't well-handle Ruby's typing.
*/
@safe
void useSection(in String key)
{
sections[key] = Section(true);
}
/**
* Adds new context to $(D_PARAM key)'s section. This method overwrites with
* list type if you already assigned other type to $(D_PARAM key)'s section.
*
* Params:
* key = key string to add
* size = reserve size for avoiding reallocation
*
* Returns:
* new Context object that added to $(D_PARAM key) section list.
*/
@trusted
Context addSubContext(in String key, lazy size_t size = 1)
{
auto c = new Context(this);
auto p = key in sections;
if (!p || p.type != SectionType.list) {
sections[key] = Section(c);
sections[key].list.reserve(size);
} else {
sections[key].list ~= c;
}
return c;
}
private:
/*
* Fetches $(D_PARAM)'s value. This method follows parent context.
*
* Params:
* key = key string to fetch
*
* Returns:
* a $(D_PARAM key) associated value. null if key does not exist.
*/
@trusted
String fetch(in String[] key, lazy Handler handler = null) const
{
assert(key.length > 0);
if (key.length == 1) {
auto result = key[0] in variables;
if (result !is null)
return *result;
if (parent !is null)
return parent.fetch(key, handler);
} else {
auto contexts = fetchList(key[0..$-1]);
foreach (c; contexts) {
auto result = key[$-1] in c.variables;
if (result !is null)
return *result;
}
}
return handler is null ? null : handler()(keyToString(key));
}
@trusted
const(Section) fetchSection()(in String[] key) const /* nothrow */
{
assert(key.length > 0);
// Ascend context tree to find the key's beginning
auto currentSection = key[0] in sections;
if (currentSection is null) {
if (parent is null)
return Section.nil;
return parent.fetchSection(key);
}
// Decend context tree to match the rest of the key
size_t keyIndex = 0;
while (currentSection) {
// Matched the entire key?
if (keyIndex == key.length-1)
return currentSection.empty ? Section.nil : *currentSection;
if (currentSection.type != SectionType.list)
return Section.nil; // Can't decend any further
// Find next part of key
keyIndex++;
foreach (c; currentSection.list)
{
currentSection = key[keyIndex] in c.sections;
if (currentSection)
break;
}
}
return Section.nil;
}
@trusted
const(Result) fetchSection(Result, SectionType type, string name)(in String[] key) const /* nothrow */
{
auto result = fetchSection(key);
if (result.type == type)
return result.empty ? null : mixin("result." ~ to!string(type));
return null;
}
alias fetchSection!(String[String], SectionType.var, "Var") fetchVar;
alias fetchSection!(Context[], SectionType.list, "List") fetchList;
alias fetchSection!(String delegate(String), SectionType.func, "Func") fetchFunc;
}
unittest
{
Context context = new Context();
context["name"] = "Red Bull";
assert(context["name"] == "Red Bull");
context["price"] = 275;
assert(context["price"] == "275");
{ // list
foreach (i; 100..105) {
auto sub = context.addSubContext("sub");
sub["num"] = i;
foreach (b; [true, false]) {
auto subsub = sub.addSubContext("subsub");
subsub["To be or not to be"] = b;
}
}
foreach (i, sub; context.fetchList(["sub"])) {
assert(sub.fetch(["name"]) == "Red Bull");
assert(sub["num"] == to!String(i + 100));
foreach (j, subsub; sub.fetchList(["subsub"])) {
assert(subsub.fetch(["price"]) == to!String(275));
assert(subsub["To be or not to be"] == to!String(j == 0));
}
}
}
{ // variable
String[String] aa = ["name" : "Ritsu"];
context["Value"] = aa;
assert(context.fetchVar(["Value"]) == cast(const)aa);
}
{ // func
auto func = function (String str) { return "" ~ str ~ " "; };
context["Wrapped"] = func;
assert(context.fetchFunc(["Wrapped"])("Ritsu") == func("Ritsu"));
}
{ // handler
Handler fixme = delegate String(String s) { assert(s=="unknown"); return "FIXME"; };
Handler error = delegate String(String s) { assert(s=="unknown"); throw new MustacheException("Unknow"); };
assert(context.fetch(["unknown"]) == "");
assert(context.fetch(["unknown"], fixme) == "FIXME");
try {
assert(context.fetch(["unknown"], error) == "");
assert(false);
} catch (const MustacheException e) { }
}
{ // subcontext
auto sub = new Context();
sub["num"] = 42;
context["a"] = [sub];
auto list = context.fetchList(["a"]);
assert(list.length == 1);
foreach (i, s; list)
assert(s["num"] == to!String(42));
}
}
private:
// Internal cache
struct Cache
{
Node[] compiled;
SysTime modified;
}
Option option_;
Cache[string] caches_;
public:
@safe
this(Option option) nothrow
{
option_ = option;
}
@property @safe nothrow
{
/**
* Property for template extenstion
*/
const(string) ext() const
{
return option_.ext;
}
/// ditto
void ext(string ext)
{
option_.ext = ext;
}
/**
* Property for template searche path
*/
const(string) path() const
{
return option_.path;
}
/// ditto
void path(string path)
{
option_.path = path;
}
/**
* Property for callback to dynamically search path.
* The result of the delegate should return the full path for
* the given name.
*/
FindPath findPath() const
{
return option_.findPath;
}
/// ditto
void findPath(FindPath findPath)
{
option_.findPath = findPath;
}
/**
* Property for cache level
*/
const(CacheLevel) level() const
{
return option_.level;
}
/// ditto
void level(CacheLevel level)
{
option_.level = level;
}
/**
* Property for callback handler
*/
const(Handler) handler() const
{
return option_.handler;
}
/// ditto
void handler(Handler handler)
{
option_.handler = handler;
}
}
/**
* Clears the intenal cache.
* Useful for forcing reloads when using CacheLevel.once.
*/
@safe
void clearCache()
{
caches_ = null;
}
/**
* Renders $(D_PARAM name) template with $(D_PARAM context).
*
* This method stores compile result in memory if you set check or once CacheLevel.
*
* Params:
* name = template name without extenstion
* context = Mustache context for rendering
*
* Returns:
* rendered result.
*
* Throws:
* object.Exception if String alignment is mismatched from template file.
*/
String render()(in string name, in Context context)
{
auto sink = appender!String();
render(name, context, sink);
return sink.data;
}
/**
* OutputRange version of $(D render).
*/
void render(Sink)(in string name, in Context context, ref Sink sink)
if(isOutputRange!(Sink, String))
{
/*
* Helper for file reading
*
* Throws:
* object.Exception if alignment is mismatched.
*/
@trusted
static String readFile(string file)
{
// cast checks character encoding alignment.
return cast(String)read(file);
}
string file;
if (option_.findPath) {
file = option_.findPath(name);
} else {
file = buildPath(option_.path, name ~ "." ~ option_.ext);
}
Node[] nodes;
final switch (option_.level) {
case CacheLevel.no:
nodes = compile(readFile(file));
break;
case CacheLevel.check:
auto t = timeLastModified(file);
auto p = file in caches_;
if (!p || t > p.modified)
caches_[file] = Cache(compile(readFile(file)), t);
nodes = caches_[file].compiled;
break;
case CacheLevel.once:
if (file !in caches_)
caches_[file] = Cache(compile(readFile(file)), SysTime.min);
nodes = caches_[file].compiled;
break;
}
renderImpl(nodes, context, sink);
}
/**
* string version of $(D render).
*/
String renderString()(in String src, in Context context)
{
auto sink = appender!String();
renderString(src, context, sink);
return sink.data;
}
/**
* string/OutputRange version of $(D render).
*/
void renderString(Sink)(in String src, in Context context, ref Sink sink)
if(isOutputRange!(Sink, String))
{
renderImpl(compile(src), context, sink);
}
private:
/*
* Implemention of render function.
*/
void renderImpl(Sink)(in Node[] nodes, in Context context, ref Sink sink)
if(isOutputRange!(Sink, String))
{
// helper for HTML escape(original function from std.xml.encode)
static void encode(in String text, ref Sink sink)
{
size_t index;
foreach (i, c; text) {
String temp;
switch (c) {
case '&': temp = "&"; break;
case '"': temp = """; break;
case '<': temp = "<"; break;
case '>': temp = ">"; break;
default: continue;
}
sink.put(text[index .. i]);
sink.put(temp);
index = i + 1;
}
sink.put(text[index .. $]);
}
foreach (ref node; nodes) {
final switch (node.type) {
case NodeType.text:
sink.put(node.text);
break;
case NodeType.var:
auto value = context.fetch(node.key, option_.handler);
if (value)
{
if(node.flag)
sink.put(value);
else
encode(value, sink);
}
break;
case NodeType.section:
auto section = context.fetchSection(node.key);
final switch (section.type) {
case Context.SectionType.nil:
if (node.flag)
renderImpl(node.childs, context, sink);
break;
case Context.SectionType.use:
if (!node.flag)
renderImpl(node.childs, context, sink);
break;
case Context.SectionType.var:
auto var = section.var;
auto sub = new Context(context);
foreach (k, v; var)
sub[k] = v;
renderImpl(node.childs, sub, sink);
break;
case Context.SectionType.func:
auto func = section.func;
renderImpl(compile(func(node.source)), context, sink);
break;
case Context.SectionType.list:
auto list = section.list;
if (!node.flag) {
foreach (sub; list)
renderImpl(node.childs, sub, sink);
}
break;
}
break;
case NodeType.partial:
render(to!string(node.key.front), context, sink);
break;
}
}
}
unittest
{
MustacheEngine!(String) m;
auto render = (String str, Context c) => m.renderString(str, c);
{ // var
auto context = new Context;
context["name"] = "Ritsu & Mio";
assert(render("Hello {{name}}", context) == "Hello Ritsu & Mio");
assert(render("Hello {{&name}}", context) == "Hello Ritsu & Mio");
assert(render("Hello {{{name}}}", context) == "Hello Ritsu & Mio");
}
{ // var with handler
auto context = new Context;
context["name"] = "Ritsu & Mio";
m.handler = delegate String(String s) { assert(s=="unknown"); return "FIXME"; };
assert(render("Hello {{unknown}}", context) == "Hello FIXME");
m.handler = delegate String(String s) { assert(s=="unknown"); throw new MustacheException("Unknow"); };
try {
assert(render("Hello {{&unknown}}", context) == "Hello Ritsu & Mio");
assert(false);
} catch (const MustacheException e) {}
m.handler = null;
}
{ // list section
auto context = new Context;
foreach (name; ["resque", "hub", "rip"]) {
auto sub = context.addSubContext("repo");
sub["name"] = name;
}
assert(render("{{#repo}}\n {{name}} \n{{/repo}}", context) ==
" resque \n hub \n rip \n");
}
{ // var section
auto context = new Context;
String[String] aa = ["name" : "Ritsu"];
context["person?"] = aa;
assert(render("{{#person?}} Hi {{name}}!\n{{/person?}}", context) ==
" Hi Ritsu!\n");
}
{ // inverted section
{
String temp = "{{#repo}}\n{{name}} \n{{/repo}}\n{{^repo}}\nNo repos :(\n{{/repo}}\n";
auto context = new Context;
assert(render(temp, context) == "\nNo repos :(\n");
String[String] aa;
context["person?"] = aa;
assert(render(temp, context) == "\nNo repos :(\n");
}
{
auto temp = "{{^section}}This shouldn't be seen.{{/section}}";
auto context = new Context;
context.addSubContext("section")["foo"] = "bar";
assert(render(temp, context).empty);
}
}
{ // comment
auto context = new Context;
assert(render("Today{{! ignore me }}. ", context) == "Today. ");
}
{ // partial
std.file.write("user.mustache", to!String("{{name}} "));
scope(exit) std.file.remove("user.mustache");
auto context = new Context;
foreach (name; ["Ritsu", "Mio"]) {
auto sub = context.addSubContext("names");
sub["name"] = name;
}
assert(render("Names \n{{#names}}\n {{> user}}\n{{/names}}\n", context) ==
"Names \n Ritsu \n Mio \n");
}
{ // dotted names
auto context = new Context;
context
.addSubContext("a")
.addSubContext("b")
.addSubContext("c")
.addSubContext("person")["name"] = "Ritsu";
context
.addSubContext("b")
.addSubContext("c")
.addSubContext("person")["name"] = "Wrong";
assert(render("Hello {{a.b.c.person.name}}", context) == "Hello Ritsu");
assert(render("Hello {{#a}}{{b.c.person.name}}{{/a}}", context) == "Hello Ritsu");
assert(render("Hello {{# a . b }}{{c.person.name}}{{/a.b}}", context) == "Hello Ritsu");
}
{ // dotted names - context precedence
auto context = new Context;
context.addSubContext("a").addSubContext("b")["X"] = "Y";
context.addSubContext("b")["c"] = "ERROR";
assert(render("-{{#a}}{{b.c}}{{/a}}-", context) == "--");
}
{ // dotted names - broken chains
auto context = new Context;
context.addSubContext("a")["X"] = "Y";
assert(render("-{{a.b.c}}-", context) == "--");
}
{ // dotted names - broken chain resolution
auto context = new Context;
context.addSubContext("a").addSubContext("b")["X"] = "Y";
context.addSubContext("c")["name"] = "ERROR";
assert(render("-{{a.b.c.name}}-", context) == "--");
}
}
/*
* Compiles $(D_PARAM src) into Intermediate Representation.
*/
static Node[] compile(String src)
{
bool beforeNewline = true;
// strip previous whitespace
bool fixWS(ref Node node)
{
// TODO: refactor and optimize with enum
if (node.type == NodeType.text) {
if (beforeNewline) {
if (all!(std.ascii.isWhite)(node.text)) {
node.text = "";
return true;
}
}
auto i = node.text.lastIndexOf('\n');
if (i != -1) {
if (all!(std.ascii.isWhite)(node.text[i + 1..$])) {
node.text = node.text[0..i + 1];
return true;
}
}
}
return false;
}
String sTag = "{{";
String eTag = "}}";
void setDelimiter(String src)
{
auto i = src.indexOf(" ");
if (i == -1)
throw new MustacheException("Delimiter tag needs whitespace");
sTag = src[0..i];
eTag = src[i + 1..$].stripLeft();
}
size_t getEnd(String src)
{
auto end = src.indexOf(eTag);
if (end == -1)
throw new MustacheException("Mustache tag is not closed");
return end;
}
// State capturing for section
struct Memo
{
String[] key;
Node[] nodes;
String source;
bool opEquals()(auto ref const Memo m) inout
{
// Don't compare source because the internal
// whitespace might be different
return key == m.key && nodes == m.nodes;
}
}
Node[] result;
Memo[] stack; // for nested section
bool singleLineSection;
while (true) {
if (singleLineSection) {
src = chompPrefix(src, "\n");
singleLineSection = false;
}
auto hit = src.indexOf(sTag);
if (hit == -1) { // rest template does not have tags
if (src.length > 0)
result ~= Node(src);
break;
} else {
if (hit > 0)
result ~= Node(src[0..hit]);
src = src[hit + sTag.length..$];
}
size_t end;
immutable type = src[0];
switch (type) {
case '#': case '^':
src = src[1..$];
auto key = parseKey(src, eTag, end);
if (result.length == 0) { // for start of template
singleLineSection = true;
} else if (result.length > 0) {
if (src[end + eTag.length] == '\n') {
singleLineSection = fixWS(result[$ - 1]);
beforeNewline = false;
}
}
result ~= Node(NodeType.section, key, type == '^');
stack ~= Memo(key, result, src[end + eTag.length..$]);
result = null;
break;
case '/':
src = src[1..$];
auto key = parseKey(src, eTag, end);
if (stack.empty)
throw new MustacheException(to!string(key) ~ " is unopened");
auto memo = stack.back; stack.popBack(); stack.assumeSafeAppend();
if (key != memo.key)
throw new MustacheException(to!string(key) ~ " is different from expected " ~ to!string(memo.key));
if (src.length == (end + eTag.length)) // for end of template
fixWS(result[$ - 1]);
if ((src.length > (end + eTag.length)) && (src[end + eTag.length] == '\n')) {
singleLineSection = fixWS(result[$ - 1]);
beforeNewline = false;
}
auto temp = result;
result = memo.nodes;
result[$ - 1].childs = temp;
result[$ - 1].source = memo.source[0..src.ptr - memo.source.ptr - 1 - eTag.length];
break;
case '>':
// TODO: If option argument exists, this function can read and compile partial file.
end = getEnd(src);
result ~= Node(NodeType.partial, [src[1..end].strip()]);
break;
case '=':
end = getEnd(src);
setDelimiter(src[1..end - 1]);
break;
case '!':
end = getEnd(src);
break;
case '{':
src = src[1..$];
auto key = parseKey(src, "}", end);
end += 1;
if (end >= src.length || !src[end..$].startsWith(eTag))
throw new MustacheException("Unescaped tag is not closed");
result ~= Node(NodeType.var, key, true);
break;
case '&':
src = src[1..$];
auto key = parseKey(src, eTag, end);
result ~= Node(NodeType.var, key, true);
break;
default:
auto key = parseKey(src, eTag, end);
result ~= Node(NodeType.var, key);
break;
}
src = src[end + eTag.length..$];
}
return result;
}
unittest
{
{ // text and unescape
auto nodes = compile("Hello {{{name}}}");
assert(nodes[0].type == NodeType.text);
assert(nodes[0].text == "Hello ");
assert(nodes[1].type == NodeType.var);
assert(nodes[1].key == ["name"]);
assert(nodes[1].flag == true);
}
{ // section and escape
auto nodes = compile("{{#in_ca}}\nWell, ${{taxed_value}}, after taxes.\n{{/in_ca}}\n");
assert(nodes[0].type == NodeType.section);
assert(nodes[0].key == ["in_ca"]);
assert(nodes[0].flag == false);
assert(nodes[0].source == "\nWell, ${{taxed_value}}, after taxes.\n");
auto childs = nodes[0].childs;
assert(childs[0].type == NodeType.text);
assert(childs[0].text == "Well, $");
assert(childs[1].type == NodeType.var);
assert(childs[1].key == ["taxed_value"]);
assert(childs[1].flag == false);
assert(childs[2].type == NodeType.text);
assert(childs[2].text == ", after taxes.\n");
}
{ // inverted section
auto nodes = compile("{{^repo}}\n No repos :(\n{{/repo}}\n");
assert(nodes[0].type == NodeType.section);
assert(nodes[0].key == ["repo"]);
assert(nodes[0].flag == true);
auto childs = nodes[0].childs;
assert(childs[0].type == NodeType.text);
assert(childs[0].text == " No repos :(\n");
}
{ // partial and set delimiter
auto nodes = compile("{{=<% %>=}}<%> erb_style %>");
assert(nodes[0].type == NodeType.partial);
assert(nodes[0].key == ["erb_style"]);
}
}
private static String[] parseKey(String src, String eTag, out size_t end)
{
String[] key;
size_t index = 0;
size_t keySegmentStart = 0;
// Index from before eating whitespace, so stripRight
// doesn't need to be called on each segment of the key.
size_t beforeEatWSIndex = 0;
void advance(size_t length)
{
if (index + length >= src.length)
throw new MustacheException("Mustache tag is not closed");
index += length;
beforeEatWSIndex = index;
}
void eatWhitespace()
{
beforeEatWSIndex = index;
index = src.length - src[index..$].stripLeft().length;
}
void acceptKeySegment()
{
if (keySegmentStart >= beforeEatWSIndex)
throw new MustacheException("Missing tag name");
key ~= src[keySegmentStart .. beforeEatWSIndex];
}
eatWhitespace();
keySegmentStart = index;
enum String dot = ".";
while (true) {
if (src[index..$].startsWith(eTag)) {
acceptKeySegment();
break;
} else if (src[index..$].startsWith(dot)) {
acceptKeySegment();
advance(dot.length);
eatWhitespace();
keySegmentStart = index;
} else {
advance(1);
eatWhitespace();
}
}
end = index;
return key;
}
unittest
{
{ // single char, single segment, no whitespace
size_t end;
String src = "a}}";
auto key = parseKey(src, "}}", end);
assert(key.length == 1);
assert(key[0] == "a");
assert(src[end..$] == "}}");
}
{ // multiple chars, single segment, no whitespace
size_t end;
String src = "Mio}}";
auto key = parseKey(src, "}}", end);
assert(key.length == 1);
assert(key[0] == "Mio");
assert(src[end..$] == "}}");
}
{ // single char, multiple segments, no whitespace
size_t end;
String src = "a.b.c}}";
auto key = parseKey(src, "}}", end);
assert(key.length == 3);
assert(key[0] == "a");
assert(key[1] == "b");
assert(key[2] == "c");
assert(src[end..$] == "}}");
}
{ // multiple chars, multiple segments, no whitespace
size_t end;
String src = "Mio.Ritsu.Yui}}";
auto key = parseKey(src, "}}", end);
assert(key.length == 3);
assert(key[0] == "Mio");
assert(key[1] == "Ritsu");
assert(key[2] == "Yui");
assert(src[end..$] == "}}");
}
{ // whitespace
size_t end;
String src = " Mio . Ritsu }}";
auto key = parseKey(src, "}}", end);
assert(key.length == 2);
assert(key[0] == "Mio");
assert(key[1] == "Ritsu");
assert(src[end..$] == "}}");
}
{ // single char custom end delimiter
size_t end;
String src = "Ritsu-";
auto key = parseKey(src, "-", end);
assert(key.length == 1);
assert(key[0] == "Ritsu");
assert(src[end..$] == "-");
}
{ // extra chars at end
size_t end;
String src = "Ritsu}}abc";
auto key = parseKey(src, "}}", end);
assert(key.length == 1);
assert(key[0] == "Ritsu");
assert(src[end..$] == "}}abc");
}
{ // error: no end delimiter
size_t end;
String src = "a.b.c";
try {
auto key = parseKey(src, "}}", end);
assert(false);
} catch (const MustacheException e) { }
}
{ // error: missing tag name
size_t end;
String src = " }}";
try {
auto key = parseKey(src, "}}", end);
assert(false);
} catch (const MustacheException e) { }
}
{ // error: missing ending tag name
size_t end;
String src = "Mio.}}";
try {
auto key = parseKey(src, "}}", end);
assert(false);
} catch (const MustacheException e) { }
}
{ // error: missing middle tag name
size_t end;
String src = "Mio. .Ritsu}}";
try {
auto key = parseKey(src, "}}", end);
assert(false);
} catch (const MustacheException e) { }
}
}
@trusted
static String keyToString(in String[] key)
{
if (key.length == 0)
return null;
if (key.length == 1)
return key[0];
Appender!String buf;
foreach (index, segment; key) {
if (index != 0)
buf.put('.');
buf.put(segment);
}
return buf.data;
}
/*
* Mustache's node types
*/
static enum NodeType
{
text, /// outside tag
var, /// {{}} or {{{}}} or {{&}}
section, /// {{#}} or {{^}}
partial /// {{>}}
}
/*
* Intermediate Representation of Mustache
*/
static struct Node
{
NodeType type;
union
{
String text;
struct
{
String[] key;
bool flag; // true is inverted or unescape
Node[] childs; // for list section
String source; // for lambda section
}
}
@trusted nothrow
{
/**
* Constructs with arguments.
*
* Params:
* t = raw text
*/
this(String t)
{
type = NodeType.text;
text = t;
}
/**
* ditto
*
* Params:
* t = Mustache's node type
* k = key string of tag
* f = invert? or escape?
*/
this(NodeType t, String[] k, bool f = false)
{
type = t;
key = k;
flag = f;
}
}
/**
* Represents the internal status as a string.
*
* Returns:
* stringized node representation.
*/
string toString() const
{
string result;
final switch (type) {
case NodeType.text:
result = "[T : \"" ~ to!string(text) ~ "\"]";
break;
case NodeType.var:
result = "[" ~ (flag ? "E" : "V") ~ " : \"" ~ keyToString(key) ~ "\"]";
break;
case NodeType.section:
result = "[" ~ (flag ? "I" : "S") ~ " : \"" ~ keyToString(key) ~ "\", [ ";
foreach (ref node; childs)
result ~= node.toString() ~ " ";
result ~= "], \"" ~ to!string(source) ~ "\"]";
break;
case NodeType.partial:
result = "[P : \"" ~ keyToString(key) ~ "\"]";
break;
}
return result;
}
}
unittest
{
Node section;
Node[] nodes, childs;
nodes ~= Node("Hi ");
nodes ~= Node(NodeType.var, ["name"]);
nodes ~= Node(NodeType.partial, ["redbull"]);
{
childs ~= Node("Ritsu is ");
childs ~= Node(NodeType.var, ["attr"], true);
section = Node(NodeType.section, ["ritsu"], false);
section.childs = childs;
nodes ~= section;
}
assert(to!string(nodes) == `[[T : "Hi "], [V : "name"], [P : "redbull"], ` ~
`[S : "ritsu", [ [T : "Ritsu is "] [E : "attr"] ], ""]]`);
}
}
unittest
{
alias MustacheEngine!(string) Mustache;
std.file.write("unittest.mustache", "Level: {{lvl}}");
scope(exit) std.file.remove("unittest.mustache");
Mustache mustache;
auto context = new Mustache.Context;
{ // no
mustache.level = Mustache.CacheLevel.no;
context["lvl"] = "no";
assert(mustache.render("unittest", context) == "Level: no");
assert(mustache.caches_.length == 0);
}
{ // check
mustache.level = Mustache.CacheLevel.check;
context["lvl"] = "check";
assert(mustache.render("unittest", context) == "Level: check");
assert(mustache.caches_.length > 0);
core.thread.Thread.sleep(dur!"seconds"(1));
std.file.write("unittest.mustache", "Modified");
assert(mustache.render("unittest", context) == "Modified");
}
mustache.caches_.remove("./unittest.mustache"); // remove previous cache
{ // once
mustache.level = Mustache.CacheLevel.once;
context["lvl"] = "once";
assert(mustache.render("unittest", context) == "Modified");
assert(mustache.caches_.length > 0);
core.thread.Thread.sleep(dur!"seconds"(1));
std.file.write("unittest.mustache", "Level: {{lvl}}");
assert(mustache.render("unittest", context) == "Modified");
}
}
unittest
{
alias Mustache = MustacheEngine!(string);
std.file.write("unittest.mustache", "{{>name}}");
scope(exit) std.file.remove("unittest.mustache");
std.file.write("other.mustache", "Ok");
scope(exit) std.file.remove("other.mustache");
Mustache mustache;
auto context = new Mustache.Context;
mustache.findPath((path) {
if (path == "name") {
return "other." ~ mustache.ext;
} else {
return path ~ "." ~ mustache.ext;
}
});
assert(mustache.render("unittest", context) == "Ok");
}
mustache-d-0.1.3/win.mak 0000664 0000000 0000000 00000000526 13040475462 0015044 0 ustar 00root root 0000000 0000000 DMD = dmd
LIB = mustache.lib
DFLAGS = -O -release -inline -nofloat -w -d -Isrc
UDFLAGS = -w -g -debug -unittest
SRCS = src\mustache.d
# DDoc
DOCS = mustache.html
DDOCFLAGS = -Dd. -c -o- std.ddoc -Isrc
target: $(LIB)
$(LIB):
$(DMD) $(DFLAGS) -lib -of$(LIB) $(SRCS)
doc:
$(DMD) $(DDOCFLAGS) $(SRCS)
clean:
rm $(DOCS) $(LIB)