pax_global_header00006660000000000000000000000064136236126070014520gustar00rootroot0000000000000052 comment=d32aaeb27b1a48241367e4039529efa2ce0a79a3 mustache-d-0.1.4/000077500000000000000000000000001362361260700135545ustar00rootroot00000000000000mustache-d-0.1.4/.gitignore000066400000000000000000000001051362361260700155400ustar00rootroot00000000000000*.[oa] *.so *.lib *.dll *.exe .dub/ __test__*__ dub.selections.json mustache-d-0.1.4/.travis.yml000066400000000000000000000001461362361260700156660ustar00rootroot00000000000000language: d d: - dmd - gdc - ldc branches: only: - master notifications: email: true mustache-d-0.1.4/README.markdown000066400000000000000000000025051362361260700162570ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/repeatedly/mustache-d.png)](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.4/dub.json000066400000000000000000000006461362361260700152270ustar00rootroot00000000000000{ "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.4/example/000077500000000000000000000000001362361260700152075ustar00rootroot00000000000000mustache-d-0.1.4/example/basic.d000066400000000000000000000005621362361260700164400ustar00rootroot00000000000000import 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.4/example/basic.mustache000066400000000000000000000001501362361260700200170ustar00rootroot00000000000000Hello {{name}} You have just won ${{value}}! {{#in_ca}} Well, ${{taxed_value}}, after taxes. {{/in_ca}} mustache-d-0.1.4/example/projects.d000066400000000000000000000021051362361260700172030ustar00rootroot00000000000000// 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.4/example/projects.mustache000066400000000000000000000006741362361260700206020ustar00rootroot00000000000000 projects

projects

{{#projects}}

{{name}}

{{description}}

{{/projects}}
mustache-d-0.1.4/example/whitespace_spec.d000066400000000000000000000034021362361260700205210ustar00rootroot00000000000000import 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.4/meson.build000066400000000000000000000012331362361260700157150ustar00rootroot00000000000000project('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.4/mustache.html000066400000000000000000000551061362361260700162620ustar00rootroot00000000000000 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:
 //{{name}} to "Chris"
 context["name"] = "Chirs"
Lists section("addSubContext" name is drived from ctemplate's API):
 //{{#repo}}
 //<b>{{name}}</b>
 //{{/repo}}
 //  to
 //<b>resque</b>
 //<b>hub</b>
 //<b>rip</b>
 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 "<b>awesome</b>"
 context["Wrapped"] = (string str) { return "<b>" ~ str ~ "</b>"; };
Inverted section:
 //{{#repo}}<b>{{name}}</b>{{/repo}}
 //{{^repo}}No repos :({{/repo}}
 //  to
 //No repos :(
 context["foo"] = "bar";  // not set to "repo"

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.





mustache-d-0.1.4/posix.mak000066400000000000000000000013501362361260700154070ustar00rootroot00000000000000# 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.4/src/000077500000000000000000000000001362361260700143435ustar00rootroot00000000000000mustache-d-0.1.4/src/mustache.d000066400000000000000000001266301362361260700163310ustar00rootroot00000000000000/** * 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 (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 (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 (MustacheException e) { } } { // error: missing tag name size_t end; String src = " }}"; try { auto key = parseKey(src, "}}", end); assert(false); } catch (MustacheException e) { } } { // error: missing ending tag name size_t end; String src = "Mio.}}"; try { auto key = parseKey(src, "}}", end); assert(false); } catch (MustacheException e) { } } { // error: missing middle tag name size_t end; String src = "Mio. .Ritsu}}"; try { auto key = parseKey(src, "}}", end); assert(false); } catch (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.4/win.mak000066400000000000000000000005261362361260700150460ustar00rootroot00000000000000DMD = 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)