├── .gitignore
├── .travis.yml
├── README.markdown
├── dub.json
├── example
├── basic.d
├── basic.mustache
├── projects.d
├── projects.mustache
└── whitespace_spec.d
├── meson.build
├── mustache.html
├── posix.mak
├── src
└── mustache.d
└── win.mak
/.gitignore:
--------------------------------------------------------------------------------
1 | *.[oa]
2 | *.so
3 | *.lib
4 | *.dll
5 | *.exe
6 |
7 | .dub/
8 | __test__*__
9 | dub.selections.json
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: d
2 |
3 | d:
4 | - dmd
5 | - gdc
6 | - ldc
7 |
8 | branches:
9 | only:
10 | - master
11 |
12 | notifications:
13 | email: true
14 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/repeatedly/mustache-d)
2 |
3 | # Mustache for D
4 |
5 | Mustache is a push-strategy (a.k.a logic-less) template engine.
6 |
7 | # Features
8 |
9 | * Variables
10 |
11 | * Sections
12 |
13 | * Lists
14 |
15 | * Non-False Values
16 |
17 | * Lambdas(half implementation)
18 |
19 | * Inverted
20 |
21 | * Comments
22 |
23 | * Partials
24 |
25 | # Usage
26 |
27 | See example directory and DDoc comments.
28 |
29 | ## Mustache.Option
30 |
31 | * ext(string)
32 |
33 | File extenstion of Mustache template. Default is "mustache".
34 |
35 | * path(string)
36 |
37 | root path to read Mustache template. Default is "."(current directory).
38 |
39 | * findPath(string delegate(string))
40 |
41 | callback to dynamically find the path do a Mustache template. Default is none. Mutually exclusive with the `path` option.
42 |
43 | * level(CacheLevel)
44 |
45 | Cache level for Mustache's in-memory cache. Default is "check". See DDoc.
46 |
47 | * handler(String delegate())
48 |
49 | Callback delegate for unknown name. handler is called if Context can't find name. Image code is below.
50 |
51 | if (followable context is nothing)
52 | return handler is null ? null : handler();
53 |
54 | # TODO
55 |
56 | Working on CTFE.
57 |
58 | # Link
59 |
60 | * [{{ mustache }}](http://mustache.github.com/)
61 |
62 | * [mustache(5) -- Logic-less templates.](http://mustache.github.com/mustache.5.html)
63 |
64 | man page
65 |
66 | # Copyright
67 |
68 | Copyright (c) 2011 Masahiro Nakagawa
69 |
70 | Distributed under the Boost Software License, Version 1.0.
71 |
--------------------------------------------------------------------------------
/dub.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mustache-d",
3 | "description": "Mustache template engine for D.",
4 | "authors": ["Masahiro Nakagawa"],
5 | "homepage": "https://github.com/repeatedly/mustache-d",
6 | "license": "Boost Software License, Version 1.0",
7 | "copyright": "Copyright (c) 2011-. Masahiro Nakagawa",
8 | "buildRequirements": ["silenceDeprecations"],
9 | "importPaths": ["src"],
10 | "targetType": "library"
11 | }
12 |
--------------------------------------------------------------------------------
/example/basic.d:
--------------------------------------------------------------------------------
1 | import mustache;
2 | import std.stdio;
3 |
4 | alias MustacheEngine!(string) Mustache;
5 |
6 | void main()
7 | {
8 | Mustache mustache;
9 | auto context = new Mustache.Context;
10 |
11 | context["name"] = "Chris";
12 | context["value"] = 10000;
13 | context["taxed_value"] = 10000 - (10000 * 0.4);
14 | context.useSection("in_ca");
15 |
16 | stdout.rawWrite(mustache.render("example/basic", context));
17 | }
18 |
--------------------------------------------------------------------------------
/example/basic.mustache:
--------------------------------------------------------------------------------
1 | Hello {{name}}
2 | You have just won ${{value}}!
3 | {{#in_ca}}
4 | Well, ${{taxed_value}}, after taxes.
5 | {{/in_ca}}
6 |
--------------------------------------------------------------------------------
/example/projects.d:
--------------------------------------------------------------------------------
1 | // This example from https://github.com/defunkt/mustache/blob/master/examples/projects.mustache
2 |
3 | import mustache;
4 | import std.stdio;
5 |
6 | struct Project
7 | {
8 | string name;
9 | string url;
10 | string description;
11 | }
12 |
13 | static Project[] projects = [
14 | Project("dmd", "https://github.com/dlang/dmd", "dmd D Programming Language compiler"),
15 | Project("druntime", "https://github.com/dlang/druntime", "Low level runtime library for the D programming language"),
16 | Project("phobos", "https://github.com/dlang/phobos", "The standard library of the D programming language")
17 | ];
18 |
19 | void main()
20 | {
21 | alias MustacheEngine!(string) Mustache;
22 |
23 | Mustache mustache;
24 | auto context = new Mustache.Context;
25 |
26 | context["width"] = 4968;
27 | foreach (ref project; projects) {
28 | auto sub = context.addSubContext("projects");
29 | sub["name"] = project.name;
30 | sub["url"] = project.url;
31 | sub["description"] = project.description;
32 | }
33 |
34 | mustache.path = "example";
35 | mustache.level = Mustache.CacheLevel.no;
36 | stdout.rawWrite(mustache.render("projects", context));
37 | }
38 |
--------------------------------------------------------------------------------
/example/projects.mustache:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | projects
5 |
6 |
7 |
8 |
9 | projects
10 |
11 |
19 |
20 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/whitespace_spec.d:
--------------------------------------------------------------------------------
1 | import mustache;
2 | import std.stdio;
3 |
4 | alias MustacheEngine!(string) Mustache;
5 |
6 | void main()
7 | {
8 | Mustache mustache;
9 | auto context = new Mustache.Context;
10 | context.useSection("boolean");
11 |
12 | // from https://github.com/mustache/spec/blob/master/specs/sections.yml
13 | assert(mustache.renderString(" | {{#boolean}}\t|\t{{/boolean}} | \n", context) == " | \t|\t | \n");
14 | assert(mustache.renderString(" | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n", context) == " | \n | \n");
15 | assert(mustache.renderString(" {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n", context) == " YES\n GOOD\n");
16 | assert(mustache.renderString("#{{#boolean}}\n/\n {{/boolean}}", context) == "#\n/\n");
17 | assert(mustache.renderString(" {{#boolean}}\n#{{/boolean}}\n/", context) == "#\n/");
18 |
19 | auto expected = `This Is
20 |
21 | A Line`;
22 | auto t = `This Is
23 | {{#boolean}}
24 |
25 | {{/boolean}}
26 | A Line`;
27 | assert(mustache.renderString(t, context) == expected);
28 |
29 | auto t2 = `This Is
30 | {{#boolean}}
31 |
32 | {{/boolean}}
33 | A Line`;
34 | assert(mustache.renderString(t, context) == expected);
35 |
36 | // TODO: \r\n support
37 |
38 | issue2();
39 | issue9();
40 | }
41 |
42 | void issue2()
43 | {
44 | Mustache mustache;
45 | auto context = new Mustache.Context;
46 | context["module_name"] = "mustache";
47 | context.useSection("static_imports");
48 |
49 | auto text = `module {{module_name}};
50 |
51 | {{#static_imports}}
52 | /*
53 | * Auto-generated static imports
54 | */
55 | {{/static_imports}}`;
56 |
57 | assert(mustache.renderString(text, context) == `module mustache;
58 |
59 | /*
60 | * Auto-generated static imports
61 | */
62 | `);
63 | }
64 |
65 | void issue9()
66 | {
67 | Mustache mustache;
68 | auto context = new Mustache.Context;
69 | context.useSection("section");
70 |
71 | auto text = `FOO
72 |
73 | {{#section}}BAR{{/section}}`;
74 |
75 | assert(mustache.renderString(text, context) == `FOO
76 |
77 | BAR`);
78 | }
79 |
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | project('mustache-d', 'd')
2 |
3 | project_version = '0.1.1'
4 | project_soversion = '0'
5 |
6 | src_dir = include_directories('src/')
7 | pkgc = import('pkgconfig')
8 |
9 | mustache_src = [
10 | 'src/mustache.d'
11 | ]
12 | install_headers(mustache_src, subdir: 'd/mustache-d')
13 |
14 | mustache_lib = static_library('mustache-d',
15 | [mustache_src],
16 | include_directories: [src_dir],
17 | install: true,
18 | version: project_version,
19 | soversion: project_soversion
20 | )
21 | pkgc.generate(name: 'mustache-d',
22 | libraries: mustache_lib,
23 | subdirs: 'd/mustache-d',
24 | version: project_version,
25 | description: 'Mustache template engine for D.'
26 | )
27 |
--------------------------------------------------------------------------------
/mustache.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
10 |
11 |
12 |
13 | mustache - D Programming Language - Digital Mars
14 |
15 |
16 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
75 |
76 |
77 |
78 |
97 |
98 |
99 |
101 |
102 |
178 |
179 |
182 |
183 |
200 |
201 |
202 |
203 |
mustache
204 |
205 |
206 | Mustache template engine for D
207 |
208 | Implemented according to
mustach(5) .
209 |
210 |
211 |
License: Boost License 1.0 .
212 |
213 |
Authors: Masahiro Nakagawa
214 |
215 |
class MustacheException : object.Exception;
216 |
217 | Exception for Mustache
218 |
219 |
220 | struct MustacheEngine (String = string) if (isSomeString!(String));
221 |
222 | Core implementation of Mustache
223 |
224 | String parameter means a string type to render.
225 |
226 |
227 | Example:
228 | alias MustacheEngine !(string) Mustache;
229 |
230 | Mustache mustache;
231 | auto context = new Mustache.Context;
232 |
233 | context["name" ] = "Chris" ;
234 | context["value" ] = 10000;
235 | context["taxed_value" ] = 10000 - (10000 * 0.4);
236 | context.useSection("in_ca" );
237 |
238 | write(mustache.render("sample" , context));
239 |
240 | sample.mustache:
241 | Hello {{name}}
242 | You have just won ${{value}}!
243 | {{#in_ca}}
244 | Well, ${{taxed_value}}, after taxes.
245 | {{/in_ca}}
246 |
247 |
248 | Output:
249 | Hello Chris
250 | You have just won 0000!
251 | Well, 000, after taxes.
252 |
253 |
254 |
255 | enum CacheLevel ;
256 |
257 | Cache level for compile result
258 |
259 | no
260 | No caching
261 |
262 |
263 | check
264 | Caches compiled result and checks the freshness of template
265 |
266 |
267 | once
268 | Caches compiled result but not check the freshness of template
269 |
270 |
271 |
272 |
273 | struct Option ;
274 |
275 | Options for rendering
276 |
277 | string ext ;
278 |
279 | template file extenstion
280 |
281 |
282 | string path ;
283 |
284 | root path for template file searching
285 |
286 |
287 | CacheLevel level ;
288 |
289 | See CacheLevel
290 |
291 |
292 | Handler handler ;
293 |
294 | Callback handler for unknown name
295 |
296 |
297 |
298 |
299 | class Context ;
300 |
301 | Mustache context for setting values
302 |
303 | Variable:
304 | context["name" ] = "Chirs"
306 |
307 |
308 | Lists section("addSubContext" name is drived from ctemplate's API):
309 | foreach (name; ["resque" , "hub" , "rip" ]) {
317 | auto sub = context.addSubContext("repo" );
318 | sub["name" ] = name;
319 | }
320 |
321 |
322 | Variable section:
323 | context["person?" ] = ["name" : "Jon" ];
325 |
326 |
327 | Lambdas section:
328 | context["Wrapped" ] = (string str) { return "<b>" ~ str ~ "</b>" ; };
330 |
331 |
332 | Inverted section:
333 | context["foo" ] = "bar" ;
339 |
340 |
341 | const nothrow String opIndex (in String key );
342 |
343 | Gets key 's value. This method does not search Section.
344 |
345 | Parameters: String key
346 | key string to search
347 |
348 | Returns: a key associated value.
349 |
350 |
351 | Throws: a RangeError if key does not exist.
352 |
353 |
354 | void opIndexAssign (T)(T value , in String key );
355 |
356 | Assigns value (automatically convert to String) to key field.
357 |
358 | If you try to assign associative array or delegate,
359 | This method assigns value as Section.
360 |
361 |
362 | Parameters: value
363 | some type value to assign
364 | key
365 | key string to assign
366 |
367 |
368 |
369 | void useSection (in String key );
370 |
371 | Enable key 's section.
372 |
373 | Parameters: String key
374 | key string to enable
375 |
376 | NOTE:
377 | I don't like this method, but D's typing can't well-handle Ruby's typing.
378 |
379 |
380 | Context addSubContext (in String key , lazy size_t size = 1);
381 |
382 | Adds new context to key 's section. This method overwrites with
383 | list type if you already assigned other type to key 's section.
384 |
385 | Parameters: String key
386 | key string to add
387 | size_t size
388 | reserve size for avoiding reallocation
389 |
390 | Returns: new Context object that added to key section list.
391 |
392 |
393 |
394 |
395 | const const(string) ext ();
396 | void ext (string ext );
397 |
398 | Property for template extenstion
399 |
400 |
401 | const const(string) path ();
402 | void path (string path );
403 |
404 | Property for template searche path
405 |
406 |
407 | const const(CacheLevel) level ();
408 | void level (CacheLevel level );
409 |
410 | Property for cache level
411 |
412 |
413 | const const(Handler) handler ();
414 | void handler (Handler handler );
415 |
416 | Property for callback handler
417 |
418 |
419 | String render (in string name , in Context context );
420 |
421 | Renders name template with context .
422 |
423 | This method stores compile result in memory if you set check or once CacheLevel.
424 |
425 |
426 | Parameters: string name
427 | template name without extenstion
428 | Context context
429 | Mustache context for rendering
430 |
431 | Returns: rendered result.
432 |
433 |
434 | Throws: object.Exception if String alignment is mismatched from template file.
435 |
436 |
437 | String renderString (in String src , in Context context );
438 |
439 | string version of render .
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
458 |
460 |
461 |
462 |
463 |
464 | Copyright Masahiro Nakagawa 2011-.
465 | |
466 | Page generated by
Ddoc .
467 |
468 |
469 |
470 |
471 |
--------------------------------------------------------------------------------
/posix.mak:
--------------------------------------------------------------------------------
1 | # build mode: 32bit or 64bit
2 |
3 | MODEL ?= $(shell getconf LONG_BIT)
4 | DMD ?= dmd
5 |
6 | LIB = libmustache.a
7 | DFLAGS = -Isrc -m$(MODEL) -w -d #-property
8 |
9 | ifeq ($(BUILD),debug)
10 | DFLAGS += -g -debug
11 | else
12 | DFLAGS += -O -release -nofloat -inline -noboundscheck
13 | endif
14 |
15 | NAMES = mustache
16 | FILES = $(addsuffix .d, $(NAMES))
17 | SRCS = $(addprefix src/, $(FILES))
18 |
19 | # DDoc
20 | DOCS = $(addsuffix .html, $(NAMES))
21 | DDOCFLAGS = -Dd. -c -o- std.ddoc -Isrc
22 |
23 | target: $(LIB)
24 |
25 | $(LIB):
26 | $(DMD) $(DFLAGS) -lib -of$(LIB) $(SRCS)
27 |
28 | doc:
29 | $(DMD) $(DDOCFLAGS) $(SRCS)
30 |
31 | clean:
32 | rm $(DOCS) $(LIB)
33 |
34 | MAIN_FILE = "empty_mustache_unittest.d"
35 |
36 | unittest:
37 | echo 'import mustache; void main(){}' > $(MAIN_FILE)
38 | $(DMD) $(DFLAGS) -unittest -of$(LIB) $(SRCS) -run $(MAIN_FILE)
39 | rm $(MAIN_FILE)
40 |
--------------------------------------------------------------------------------
/src/mustache.d:
--------------------------------------------------------------------------------
1 | /**
2 | * Mustache template engine for D
3 | *
4 | * Implemented according to mustach(5) .
5 | *
6 | * Copyright: Copyright Masahiro Nakagawa 2011-.
7 | * License: Boost License 1.0 .
8 | * Authors: Masahiro Nakagawa
9 | */
10 | module mustache;
11 |
12 | import std.algorithm : all;
13 | import std.array; // empty, back, popBack, appender
14 | import std.conv; // to
15 | import std.datetime; // SysTime (I think std.file should import std.datetime as public)
16 | import std.file; // read, timeLastModified
17 | import std.path; // buildPath
18 | import std.range; // isOutputRange
19 | import std.string; // strip, chomp, stripLeft
20 | import std.traits; // isSomeString, isAssociativeArray
21 |
22 | static import std.ascii; // isWhite;
23 |
24 | version(unittest) import core.thread;
25 |
26 |
27 | /**
28 | * Exception for Mustache
29 | */
30 | class MustacheException : Exception
31 | {
32 | this(string messaage)
33 | {
34 | super(messaage);
35 | }
36 | }
37 |
38 |
39 | /**
40 | * Core implementation of Mustache
41 | *
42 | * $(D_PARAM String) parameter means a string type to render.
43 | *
44 | * Example:
45 | * -----
46 | * alias MustacheEngine!(string) Mustache;
47 | *
48 | * Mustache mustache;
49 | * auto context = new Mustache.Context;
50 | *
51 | * context["name"] = "Chris";
52 | * context["value"] = 10000;
53 | * context["taxed_value"] = 10000 - (10000 * 0.4);
54 | * context.useSection("in_ca");
55 | *
56 | * write(mustache.render("sample", context));
57 | * -----
58 | * sample.mustache:
59 | * -----
60 | * Hello {{name}}
61 | * You have just won ${{value}}!
62 | * {{#in_ca}}
63 | * Well, ${{taxed_value}}, after taxes.
64 | * {{/in_ca}}
65 | * -----
66 | * Output:
67 | * -----
68 | * Hello Chris
69 | * You have just won $10000!
70 | * Well, $6000, after taxes.
71 | * -----
72 | */
73 | struct MustacheEngine(String = string) if (isSomeString!(String))
74 | {
75 | static assert(!is(String == wstring), "wstring is unsupported. It's a buggy!");
76 |
77 |
78 | public:
79 | alias String delegate(String) Handler;
80 | alias string delegate(string) FindPath;
81 |
82 |
83 | /**
84 | * Cache level for compile result
85 | */
86 | static enum CacheLevel
87 | {
88 | no, /// No caching
89 | check, /// Caches compiled result and checks the freshness of template
90 | once /// Caches compiled result but not check the freshness of template
91 | }
92 |
93 |
94 | /**
95 | * Options for rendering
96 | */
97 | static struct Option
98 | {
99 | string ext = "mustache"; /// template file extenstion
100 | string path = "."; /// root path for template file searching
101 | FindPath findPath; /// dynamically finds the path for a name
102 | CacheLevel level = CacheLevel.check; /// See CacheLevel
103 | Handler handler; /// Callback handler for unknown name
104 | }
105 |
106 |
107 | /**
108 | * Mustache context for setting values
109 | *
110 | * Variable:
111 | * -----
112 | * //{{name}} to "Chris"
113 | * context["name"] = "Chirs"
114 | * -----
115 | *
116 | * Lists section("addSubContext" name is drived from ctemplate's API):
117 | * -----
118 | * //{{#repo}}
119 | * //{{name}}
120 | * //{{/repo}}
121 | * // to
122 | * //resque
123 | * //hub
124 | * //rip
125 | * foreach (name; ["resque", "hub", "rip"]) {
126 | * auto sub = context.addSubContext("repo");
127 | * sub["name"] = name;
128 | * }
129 | * -----
130 | *
131 | * Variable section:
132 | * -----
133 | * //{{#person?}}Hi {{name}}{{/person?}} to "Hi Jon"
134 | * context["person?"] = ["name" : "Jon"];
135 | * -----
136 | *
137 | * Lambdas section:
138 | * -----
139 | * //{{#wrapped}}awesome{{/wrapped}} to "awesome "
140 | * context["Wrapped"] = (string str) { return "" ~ str ~ " "; };
141 | * -----
142 | *
143 | * Inverted section:
144 | * -----
145 | * //{{#repo}}{{name}} {{/repo}}
146 | * //{{^repo}}No repos :({{/repo}}
147 | * // to
148 | * //No repos :(
149 | * context["foo"] = "bar"; // not set to "repo"
150 | * -----
151 | */
152 | static final class Context
153 | {
154 | private:
155 | enum SectionType
156 | {
157 | nil, use, var, func, list
158 | }
159 |
160 | struct Section
161 | {
162 | SectionType type;
163 |
164 | union
165 | {
166 | String[String] var;
167 | String delegate(String) func; // func type is String delegate(String) delegate()?
168 | Context[] list;
169 | }
170 |
171 | @trusted nothrow
172 | {
173 | this(bool u)
174 | {
175 | type = SectionType.use;
176 | }
177 |
178 | this(String[String] v)
179 | {
180 | type = SectionType.var;
181 | var = v;
182 | }
183 |
184 | this(String delegate(String) f)
185 | {
186 | type = SectionType.func;
187 | func = f;
188 | }
189 |
190 | this(Context c)
191 | {
192 | type = SectionType.list;
193 | list = [c];
194 | }
195 |
196 | this(Context[] c)
197 | {
198 | type = SectionType.list;
199 | list = c;
200 | }
201 | }
202 |
203 | /* nothrow : AA's length is not nothrow */
204 | @trusted @property
205 | bool empty() const
206 | {
207 | final switch (type) {
208 | case SectionType.nil:
209 | return true;
210 | case SectionType.use:
211 | return false;
212 | case SectionType.var:
213 | return !var.length; // Why?
214 | case SectionType.func:
215 | return func is null;
216 | case SectionType.list:
217 | return !list.length;
218 | }
219 | }
220 |
221 | /* Convenience function */
222 | @safe @property
223 | static Section nil() nothrow
224 | {
225 | Section result;
226 | result.type = SectionType.nil;
227 | return result;
228 | }
229 | }
230 |
231 | const Context parent;
232 | String[String] variables;
233 | Section[String] sections;
234 |
235 |
236 | public:
237 | @safe
238 | this(const Context context = null) nothrow
239 | {
240 | parent = context;
241 | }
242 |
243 | /**
244 | * Gets $(D_PARAM key)'s value. This method does not search Section.
245 | *
246 | * Params:
247 | * key = key string to search
248 | *
249 | * Returns:
250 | * a $(D_PARAM key) associated value.
251 | *
252 | * Throws:
253 | * a RangeError if $(D_PARAM key) does not exist.
254 | */
255 | @safe
256 | String opIndex(in String key) const nothrow
257 | {
258 | return variables[key];
259 | }
260 |
261 | /**
262 | * Assigns $(D_PARAM value)(automatically convert to String) to $(D_PARAM key) field.
263 | *
264 | * If you try to assign associative array or delegate,
265 | * This method assigns $(D_PARAM value) as Section.
266 | *
267 | * Arrays of Contexts are accepted, too.
268 | *
269 | * Params:
270 | * value = some type value to assign
271 | * key = key string to assign
272 | */
273 | @trusted
274 | void opIndexAssign(T)(T value, in String key)
275 | {
276 | static if (isAssociativeArray!(T))
277 | {
278 | static if (is(T V : V[K], K : String))
279 | {
280 | String[String] aa;
281 |
282 | static if (is(V == String))
283 | aa = value;
284 | else
285 | foreach (k, v; value) aa[k] = to!String(v);
286 |
287 | sections[key] = Section(aa);
288 | }
289 | else static assert(false, "Non-supported Associative Array type");
290 | }
291 | else static if (isCallable!T)
292 | {
293 | import std.functional : toDelegate;
294 |
295 | auto v = toDelegate(value);
296 | static if (is(typeof(v) D == S delegate(S), S : String))
297 | sections[key] = Section(v);
298 | else static assert(false, "Non-supported delegate type");
299 | }
300 | else static if (isArray!T && !isSomeString!T)
301 | {
302 | static if (is(T : Context[]))
303 | sections[key] = Section(value);
304 | else static assert(false, "Non-supported array type");
305 | }
306 | else
307 | {
308 | variables[key] = to!String(value);
309 | }
310 | }
311 |
312 | /**
313 | * Enable $(D_PARAM key)'s section.
314 | *
315 | * Params:
316 | * key = key string to enable
317 | *
318 | * NOTE:
319 | * I don't like this method, but D's typing can't well-handle Ruby's typing.
320 | */
321 | @safe
322 | void useSection(in String key)
323 | {
324 | sections[key] = Section(true);
325 | }
326 |
327 | /**
328 | * Adds new context to $(D_PARAM key)'s section. This method overwrites with
329 | * list type if you already assigned other type to $(D_PARAM key)'s section.
330 | *
331 | * Params:
332 | * key = key string to add
333 | * size = reserve size for avoiding reallocation
334 | *
335 | * Returns:
336 | * new Context object that added to $(D_PARAM key) section list.
337 | */
338 | @trusted
339 | Context addSubContext(in String key, lazy size_t size = 1)
340 | {
341 | auto c = new Context(this);
342 | auto p = key in sections;
343 | if (!p || p.type != SectionType.list) {
344 | sections[key] = Section(c);
345 | sections[key].list.reserve(size);
346 | } else {
347 | sections[key].list ~= c;
348 | }
349 |
350 | return c;
351 | }
352 |
353 |
354 | private:
355 | /*
356 | * Fetches $(D_PARAM)'s value. This method follows parent context.
357 | *
358 | * Params:
359 | * key = key string to fetch
360 | *
361 | * Returns:
362 | * a $(D_PARAM key) associated value. null if key does not exist.
363 | */
364 | @trusted
365 | String fetch(in String[] key, lazy Handler handler = null) const
366 | {
367 | assert(key.length > 0);
368 |
369 | if (key.length == 1) {
370 | auto result = key[0] in variables;
371 |
372 | if (result !is null)
373 | return *result;
374 |
375 | if (parent !is null)
376 | return parent.fetch(key, handler);
377 | } else {
378 | auto contexts = fetchList(key[0..$-1]);
379 | foreach (c; contexts) {
380 | auto result = key[$-1] in c.variables;
381 |
382 | if (result !is null)
383 | return *result;
384 | }
385 | }
386 |
387 | return handler is null ? null : handler()(keyToString(key));
388 | }
389 |
390 | @trusted
391 | const(Section) fetchSection()(in String[] key) const /* nothrow */
392 | {
393 | assert(key.length > 0);
394 |
395 | // Ascend context tree to find the key's beginning
396 | auto currentSection = key[0] in sections;
397 | if (currentSection is null) {
398 | if (parent is null)
399 | return Section.nil;
400 |
401 | return parent.fetchSection(key);
402 | }
403 |
404 | // Decend context tree to match the rest of the key
405 | size_t keyIndex = 0;
406 | while (currentSection) {
407 | // Matched the entire key?
408 | if (keyIndex == key.length-1)
409 | return currentSection.empty ? Section.nil : *currentSection;
410 |
411 | if (currentSection.type != SectionType.list)
412 | return Section.nil; // Can't decend any further
413 |
414 | // Find next part of key
415 | keyIndex++;
416 | foreach (c; currentSection.list)
417 | {
418 | currentSection = key[keyIndex] in c.sections;
419 | if (currentSection)
420 | break;
421 | }
422 | }
423 |
424 | return Section.nil;
425 | }
426 |
427 | @trusted
428 | const(Result) fetchSection(Result, SectionType type, string name)(in String[] key) const /* nothrow */
429 | {
430 | auto result = fetchSection(key);
431 | if (result.type == type)
432 | return result.empty ? null : mixin("result." ~ to!string(type));
433 |
434 | return null;
435 | }
436 |
437 | alias fetchSection!(String[String], SectionType.var, "Var") fetchVar;
438 | alias fetchSection!(Context[], SectionType.list, "List") fetchList;
439 | alias fetchSection!(String delegate(String), SectionType.func, "Func") fetchFunc;
440 | }
441 |
442 | unittest
443 | {
444 | Context context = new Context();
445 |
446 | context["name"] = "Red Bull";
447 | assert(context["name"] == "Red Bull");
448 | context["price"] = 275;
449 | assert(context["price"] == "275");
450 |
451 | { // list
452 | foreach (i; 100..105) {
453 | auto sub = context.addSubContext("sub");
454 | sub["num"] = i;
455 |
456 | foreach (b; [true, false]) {
457 | auto subsub = sub.addSubContext("subsub");
458 | subsub["To be or not to be"] = b;
459 | }
460 | }
461 |
462 | foreach (i, sub; context.fetchList(["sub"])) {
463 | assert(sub.fetch(["name"]) == "Red Bull");
464 | assert(sub["num"] == to!String(i + 100));
465 |
466 | foreach (j, subsub; sub.fetchList(["subsub"])) {
467 | assert(subsub.fetch(["price"]) == to!String(275));
468 | assert(subsub["To be or not to be"] == to!String(j == 0));
469 | }
470 | }
471 | }
472 | { // variable
473 | String[String] aa = ["name" : "Ritsu"];
474 |
475 | context["Value"] = aa;
476 | assert(context.fetchVar(["Value"]) == cast(const)aa);
477 | }
478 | { // func
479 | auto func = function (String str) { return "" ~ str ~ " "; };
480 |
481 | context["Wrapped"] = func;
482 | assert(context.fetchFunc(["Wrapped"])("Ritsu") == func("Ritsu"));
483 | }
484 | { // handler
485 | Handler fixme = delegate String(String s) { assert(s=="unknown"); return "FIXME"; };
486 | Handler error = delegate String(String s) { assert(s=="unknown"); throw new MustacheException("Unknow"); };
487 |
488 | assert(context.fetch(["unknown"]) == "");
489 | assert(context.fetch(["unknown"], fixme) == "FIXME");
490 | try {
491 | assert(context.fetch(["unknown"], error) == "");
492 | assert(false);
493 | } catch (MustacheException e) { }
494 | }
495 | { // subcontext
496 | auto sub = new Context();
497 | sub["num"] = 42;
498 | context["a"] = [sub];
499 |
500 | auto list = context.fetchList(["a"]);
501 | assert(list.length == 1);
502 | foreach (i, s; list)
503 | assert(s["num"] == to!String(42));
504 | }
505 | }
506 |
507 |
508 | private:
509 | // Internal cache
510 | struct Cache
511 | {
512 | Node[] compiled;
513 | SysTime modified;
514 | }
515 |
516 | Option option_;
517 | Cache[string] caches_;
518 |
519 |
520 | public:
521 | @safe
522 | this(Option option) nothrow
523 | {
524 | option_ = option;
525 | }
526 |
527 | @property @safe nothrow
528 | {
529 | /**
530 | * Property for template extenstion
531 | */
532 | const(string) ext() const
533 | {
534 | return option_.ext;
535 | }
536 |
537 | /// ditto
538 | void ext(string ext)
539 | {
540 | option_.ext = ext;
541 | }
542 |
543 | /**
544 | * Property for template searche path
545 | */
546 | const(string) path() const
547 | {
548 | return option_.path;
549 | }
550 |
551 | /// ditto
552 | void path(string path)
553 | {
554 | option_.path = path;
555 | }
556 |
557 | /**
558 | * Property for callback to dynamically search path.
559 | * The result of the delegate should return the full path for
560 | * the given name.
561 | */
562 | FindPath findPath() const
563 | {
564 | return option_.findPath;
565 | }
566 |
567 | /// ditto
568 | void findPath(FindPath findPath)
569 | {
570 | option_.findPath = findPath;
571 | }
572 |
573 | /**
574 | * Property for cache level
575 | */
576 | const(CacheLevel) level() const
577 | {
578 | return option_.level;
579 | }
580 |
581 | /// ditto
582 | void level(CacheLevel level)
583 | {
584 | option_.level = level;
585 | }
586 |
587 | /**
588 | * Property for callback handler
589 | */
590 | const(Handler) handler() const
591 | {
592 | return option_.handler;
593 | }
594 |
595 | /// ditto
596 | void handler(Handler handler)
597 | {
598 | option_.handler = handler;
599 | }
600 | }
601 |
602 | /**
603 | * Clears the intenal cache.
604 | * Useful for forcing reloads when using CacheLevel.once.
605 | */
606 | @safe
607 | void clearCache()
608 | {
609 | caches_ = null;
610 | }
611 |
612 | /**
613 | * Renders $(D_PARAM name) template with $(D_PARAM context).
614 | *
615 | * This method stores compile result in memory if you set check or once CacheLevel.
616 | *
617 | * Params:
618 | * name = template name without extenstion
619 | * context = Mustache context for rendering
620 | *
621 | * Returns:
622 | * rendered result.
623 | *
624 | * Throws:
625 | * object.Exception if String alignment is mismatched from template file.
626 | */
627 | String render()(in string name, in Context context)
628 | {
629 | auto sink = appender!String();
630 | render(name, context, sink);
631 | return sink.data;
632 | }
633 |
634 | /**
635 | * OutputRange version of $(D render).
636 | */
637 | void render(Sink)(in string name, in Context context, ref Sink sink)
638 | if(isOutputRange!(Sink, String))
639 | {
640 | /*
641 | * Helper for file reading
642 | *
643 | * Throws:
644 | * object.Exception if alignment is mismatched.
645 | */
646 | @trusted
647 | static String readFile(string file)
648 | {
649 | // cast checks character encoding alignment.
650 | return cast(String)read(file);
651 | }
652 |
653 | string file;
654 | if (option_.findPath) {
655 | file = option_.findPath(name);
656 | } else {
657 | file = buildPath(option_.path, name ~ "." ~ option_.ext);
658 | }
659 | Node[] nodes;
660 |
661 | final switch (option_.level) {
662 | case CacheLevel.no:
663 | nodes = compile(readFile(file));
664 | break;
665 | case CacheLevel.check:
666 | auto t = timeLastModified(file);
667 | auto p = file in caches_;
668 | if (!p || t > p.modified)
669 | caches_[file] = Cache(compile(readFile(file)), t);
670 | nodes = caches_[file].compiled;
671 | break;
672 | case CacheLevel.once:
673 | if (file !in caches_)
674 | caches_[file] = Cache(compile(readFile(file)), SysTime.min);
675 | nodes = caches_[file].compiled;
676 | break;
677 | }
678 |
679 | renderImpl(nodes, context, sink);
680 | }
681 |
682 | /**
683 | * string version of $(D render).
684 | */
685 | String renderString()(in String src, in Context context)
686 | {
687 | auto sink = appender!String();
688 | renderString(src, context, sink);
689 | return sink.data;
690 | }
691 |
692 | /**
693 | * string/OutputRange version of $(D render).
694 | */
695 | void renderString(Sink)(in String src, in Context context, ref Sink sink)
696 | if(isOutputRange!(Sink, String))
697 | {
698 | renderImpl(compile(src), context, sink);
699 | }
700 |
701 |
702 | private:
703 | /*
704 | * Implemention of render function.
705 | */
706 | void renderImpl(Sink)(in Node[] nodes, in Context context, ref Sink sink)
707 | if(isOutputRange!(Sink, String))
708 | {
709 | // helper for HTML escape(original function from std.xml.encode)
710 | static void encode(in String text, ref Sink sink)
711 | {
712 | size_t index;
713 |
714 | foreach (i, c; text) {
715 | String temp;
716 |
717 | switch (c) {
718 | case '&': temp = "&"; break;
719 | case '"': temp = """; break;
720 | case '<': temp = "<"; break;
721 | case '>': temp = ">"; break;
722 | default: continue;
723 | }
724 |
725 | sink.put(text[index .. i]);
726 | sink.put(temp);
727 | index = i + 1;
728 | }
729 |
730 | sink.put(text[index .. $]);
731 | }
732 |
733 | foreach (ref node; nodes) {
734 | final switch (node.type) {
735 | case NodeType.text:
736 | sink.put(node.text);
737 | break;
738 | case NodeType.var:
739 | auto value = context.fetch(node.key, option_.handler);
740 | if (value)
741 | {
742 | if(node.flag)
743 | sink.put(value);
744 | else
745 | encode(value, sink);
746 | }
747 | break;
748 | case NodeType.section:
749 | auto section = context.fetchSection(node.key);
750 | final switch (section.type) {
751 | case Context.SectionType.nil:
752 | if (node.flag)
753 | renderImpl(node.childs, context, sink);
754 | break;
755 | case Context.SectionType.use:
756 | if (!node.flag)
757 | renderImpl(node.childs, context, sink);
758 | break;
759 | case Context.SectionType.var:
760 | auto var = section.var;
761 | auto sub = new Context(context);
762 | foreach (k, v; var)
763 | sub[k] = v;
764 | renderImpl(node.childs, sub, sink);
765 | break;
766 | case Context.SectionType.func:
767 | auto func = section.func;
768 | renderImpl(compile(func(node.source)), context, sink);
769 | break;
770 | case Context.SectionType.list:
771 | auto list = section.list;
772 | if (!node.flag) {
773 | foreach (sub; list)
774 | renderImpl(node.childs, sub, sink);
775 | }
776 | break;
777 | }
778 | break;
779 | case NodeType.partial:
780 | render(to!string(node.key.front), context, sink);
781 | break;
782 | }
783 | }
784 | }
785 |
786 |
787 | unittest
788 | {
789 | MustacheEngine!(String) m;
790 | auto render = (String str, Context c) => m.renderString(str, c);
791 |
792 | { // var
793 | auto context = new Context;
794 | context["name"] = "Ritsu & Mio";
795 |
796 | assert(render("Hello {{name}}", context) == "Hello Ritsu & Mio");
797 | assert(render("Hello {{&name}}", context) == "Hello Ritsu & Mio");
798 | assert(render("Hello {{{name}}}", context) == "Hello Ritsu & Mio");
799 | }
800 | { // var with handler
801 | auto context = new Context;
802 | context["name"] = "Ritsu & Mio";
803 |
804 | m.handler = delegate String(String s) { assert(s=="unknown"); return "FIXME"; };
805 | assert(render("Hello {{unknown}}", context) == "Hello FIXME");
806 |
807 | m.handler = delegate String(String s) { assert(s=="unknown"); throw new MustacheException("Unknow"); };
808 | try {
809 | assert(render("Hello {{&unknown}}", context) == "Hello Ritsu & Mio");
810 | assert(false);
811 | } catch (MustacheException e) {}
812 |
813 | m.handler = null;
814 | }
815 | { // list section
816 | auto context = new Context;
817 | foreach (name; ["resque", "hub", "rip"]) {
818 | auto sub = context.addSubContext("repo");
819 | sub["name"] = name;
820 | }
821 |
822 | assert(render("{{#repo}}\n {{name}} \n{{/repo}}", context) ==
823 | " resque \n hub \n rip \n");
824 | }
825 | { // var section
826 | auto context = new Context;
827 | String[String] aa = ["name" : "Ritsu"];
828 | context["person?"] = aa;
829 |
830 | assert(render("{{#person?}} Hi {{name}}!\n{{/person?}}", context) ==
831 | " Hi Ritsu!\n");
832 | }
833 | { // inverted section
834 | {
835 | String temp = "{{#repo}}\n{{name}} \n{{/repo}}\n{{^repo}}\nNo repos :(\n{{/repo}}\n";
836 | auto context = new Context;
837 | assert(render(temp, context) == "\nNo repos :(\n");
838 |
839 | String[String] aa;
840 | context["person?"] = aa;
841 | assert(render(temp, context) == "\nNo repos :(\n");
842 | }
843 | {
844 | auto temp = "{{^section}}This shouldn't be seen.{{/section}}";
845 | auto context = new Context;
846 | context.addSubContext("section")["foo"] = "bar";
847 | assert(render(temp, context).empty);
848 | }
849 | }
850 | { // comment
851 | auto context = new Context;
852 | assert(render("Today{{! ignore me }}. ", context) == "Today. ");
853 | }
854 | { // partial
855 | std.file.write("user.mustache", to!String("{{name}} "));
856 | scope(exit) std.file.remove("user.mustache");
857 |
858 | auto context = new Context;
859 | foreach (name; ["Ritsu", "Mio"]) {
860 | auto sub = context.addSubContext("names");
861 | sub["name"] = name;
862 | }
863 |
864 | assert(render("Names \n{{#names}}\n {{> user}}\n{{/names}}\n", context) ==
865 | "Names \n Ritsu \n Mio \n");
866 | }
867 | { // dotted names
868 | auto context = new Context;
869 | context
870 | .addSubContext("a")
871 | .addSubContext("b")
872 | .addSubContext("c")
873 | .addSubContext("person")["name"] = "Ritsu";
874 | context
875 | .addSubContext("b")
876 | .addSubContext("c")
877 | .addSubContext("person")["name"] = "Wrong";
878 |
879 | assert(render("Hello {{a.b.c.person.name}}", context) == "Hello Ritsu");
880 | assert(render("Hello {{#a}}{{b.c.person.name}}{{/a}}", context) == "Hello Ritsu");
881 | assert(render("Hello {{# a . b }}{{c.person.name}}{{/a.b}}", context) == "Hello Ritsu");
882 | }
883 | { // dotted names - context precedence
884 | auto context = new Context;
885 | context.addSubContext("a").addSubContext("b")["X"] = "Y";
886 | context.addSubContext("b")["c"] = "ERROR";
887 |
888 | assert(render("-{{#a}}{{b.c}}{{/a}}-", context) == "--");
889 | }
890 | { // dotted names - broken chains
891 | auto context = new Context;
892 | context.addSubContext("a")["X"] = "Y";
893 | assert(render("-{{a.b.c}}-", context) == "--");
894 | }
895 | { // dotted names - broken chain resolution
896 | auto context = new Context;
897 | context.addSubContext("a").addSubContext("b")["X"] = "Y";
898 | context.addSubContext("c")["name"] = "ERROR";
899 |
900 | assert(render("-{{a.b.c.name}}-", context) == "--");
901 | }
902 | }
903 |
904 | /*
905 | * Compiles $(D_PARAM src) into Intermediate Representation.
906 | */
907 | static Node[] compile(String src)
908 | {
909 | bool beforeNewline = true;
910 |
911 | // strip previous whitespace
912 | bool fixWS(ref Node node)
913 | {
914 | // TODO: refactor and optimize with enum
915 | if (node.type == NodeType.text) {
916 | if (beforeNewline) {
917 | if (all!(std.ascii.isWhite)(node.text)) {
918 | node.text = "";
919 | return true;
920 | }
921 | }
922 |
923 | auto i = node.text.lastIndexOf('\n');
924 | if (i != -1) {
925 | if (all!(std.ascii.isWhite)(node.text[i + 1..$])) {
926 | node.text = node.text[0..i + 1];
927 | return true;
928 | }
929 | }
930 | }
931 |
932 | return false;
933 | }
934 |
935 | String sTag = "{{";
936 | String eTag = "}}";
937 |
938 | void setDelimiter(String src)
939 | {
940 | auto i = src.indexOf(" ");
941 | if (i == -1)
942 | throw new MustacheException("Delimiter tag needs whitespace");
943 |
944 | sTag = src[0..i];
945 | eTag = src[i + 1..$].stripLeft();
946 | }
947 |
948 | size_t getEnd(String src)
949 | {
950 | auto end = src.indexOf(eTag);
951 | if (end == -1)
952 | throw new MustacheException("Mustache tag is not closed");
953 |
954 | return end;
955 | }
956 |
957 | // State capturing for section
958 | struct Memo
959 | {
960 | String[] key;
961 | Node[] nodes;
962 | String source;
963 |
964 | bool opEquals()(auto ref const Memo m) inout
965 | {
966 | // Don't compare source because the internal
967 | // whitespace might be different
968 | return key == m.key && nodes == m.nodes;
969 | }
970 | }
971 |
972 | Node[] result;
973 | Memo[] stack; // for nested section
974 | bool singleLineSection;
975 |
976 | while (true) {
977 | if (singleLineSection) {
978 | src = chompPrefix(src, "\n");
979 | singleLineSection = false;
980 | }
981 |
982 | auto hit = src.indexOf(sTag);
983 | if (hit == -1) { // rest template does not have tags
984 | if (src.length > 0)
985 | result ~= Node(src);
986 | break;
987 | } else {
988 | if (hit > 0)
989 | result ~= Node(src[0..hit]);
990 | src = src[hit + sTag.length..$];
991 | }
992 |
993 | size_t end;
994 |
995 | immutable type = src[0];
996 | switch (type) {
997 | case '#': case '^':
998 | src = src[1..$];
999 | auto key = parseKey(src, eTag, end);
1000 |
1001 | if (result.length == 0) { // for start of template
1002 | singleLineSection = true;
1003 | } else if (result.length > 0) {
1004 | if (src[end + eTag.length] == '\n') {
1005 | singleLineSection = fixWS(result[$ - 1]);
1006 | beforeNewline = false;
1007 | }
1008 | }
1009 |
1010 | result ~= Node(NodeType.section, key, type == '^');
1011 | stack ~= Memo(key, result, src[end + eTag.length..$]);
1012 | result = null;
1013 | break;
1014 | case '/':
1015 | src = src[1..$];
1016 | auto key = parseKey(src, eTag, end);
1017 | if (stack.empty)
1018 | throw new MustacheException(to!string(key) ~ " is unopened");
1019 | auto memo = stack.back; stack.popBack(); stack.assumeSafeAppend();
1020 | if (key != memo.key)
1021 | throw new MustacheException(to!string(key) ~ " is different from expected " ~ to!string(memo.key));
1022 |
1023 | if (src.length == (end + eTag.length)) // for end of template
1024 | fixWS(result[$ - 1]);
1025 | if ((src.length > (end + eTag.length)) && (src[end + eTag.length] == '\n')) {
1026 | singleLineSection = fixWS(result[$ - 1]);
1027 | beforeNewline = false;
1028 | }
1029 |
1030 | auto temp = result;
1031 | result = memo.nodes;
1032 | result[$ - 1].childs = temp;
1033 | result[$ - 1].source = memo.source[0..src.ptr - memo.source.ptr - 1 - eTag.length];
1034 | break;
1035 | case '>':
1036 | // TODO: If option argument exists, this function can read and compile partial file.
1037 | end = getEnd(src);
1038 | result ~= Node(NodeType.partial, [src[1..end].strip()]);
1039 | break;
1040 | case '=':
1041 | end = getEnd(src);
1042 | setDelimiter(src[1..end - 1]);
1043 | break;
1044 | case '!':
1045 | end = getEnd(src);
1046 | break;
1047 | case '{':
1048 | src = src[1..$];
1049 | auto key = parseKey(src, "}", end);
1050 |
1051 | end += 1;
1052 | if (end >= src.length || !src[end..$].startsWith(eTag))
1053 | throw new MustacheException("Unescaped tag is not closed");
1054 |
1055 | result ~= Node(NodeType.var, key, true);
1056 | break;
1057 | case '&':
1058 | src = src[1..$];
1059 | auto key = parseKey(src, eTag, end);
1060 | result ~= Node(NodeType.var, key, true);
1061 | break;
1062 | default:
1063 | auto key = parseKey(src, eTag, end);
1064 | result ~= Node(NodeType.var, key);
1065 | break;
1066 | }
1067 |
1068 | src = src[end + eTag.length..$];
1069 | }
1070 |
1071 | return result;
1072 | }
1073 |
1074 | unittest
1075 | {
1076 | { // text and unescape
1077 | auto nodes = compile("Hello {{{name}}}");
1078 | assert(nodes[0].type == NodeType.text);
1079 | assert(nodes[0].text == "Hello ");
1080 | assert(nodes[1].type == NodeType.var);
1081 | assert(nodes[1].key == ["name"]);
1082 | assert(nodes[1].flag == true);
1083 | }
1084 | { // section and escape
1085 | auto nodes = compile("{{#in_ca}}\nWell, ${{taxed_value}}, after taxes.\n{{/in_ca}}\n");
1086 | assert(nodes[0].type == NodeType.section);
1087 | assert(nodes[0].key == ["in_ca"]);
1088 | assert(nodes[0].flag == false);
1089 | assert(nodes[0].source == "\nWell, ${{taxed_value}}, after taxes.\n");
1090 |
1091 | auto childs = nodes[0].childs;
1092 | assert(childs[0].type == NodeType.text);
1093 | assert(childs[0].text == "Well, $");
1094 | assert(childs[1].type == NodeType.var);
1095 | assert(childs[1].key == ["taxed_value"]);
1096 | assert(childs[1].flag == false);
1097 | assert(childs[2].type == NodeType.text);
1098 | assert(childs[2].text == ", after taxes.\n");
1099 | }
1100 | { // inverted section
1101 | auto nodes = compile("{{^repo}}\n No repos :(\n{{/repo}}\n");
1102 | assert(nodes[0].type == NodeType.section);
1103 | assert(nodes[0].key == ["repo"]);
1104 | assert(nodes[0].flag == true);
1105 |
1106 | auto childs = nodes[0].childs;
1107 | assert(childs[0].type == NodeType.text);
1108 | assert(childs[0].text == " No repos :(\n");
1109 | }
1110 | { // partial and set delimiter
1111 | auto nodes = compile("{{=<% %>=}}<%> erb_style %>");
1112 | assert(nodes[0].type == NodeType.partial);
1113 | assert(nodes[0].key == ["erb_style"]);
1114 | }
1115 | }
1116 |
1117 | private static String[] parseKey(String src, String eTag, out size_t end)
1118 | {
1119 | String[] key;
1120 | size_t index = 0;
1121 | size_t keySegmentStart = 0;
1122 | // Index from before eating whitespace, so stripRight
1123 | // doesn't need to be called on each segment of the key.
1124 | size_t beforeEatWSIndex = 0;
1125 |
1126 | void advance(size_t length)
1127 | {
1128 | if (index + length >= src.length)
1129 | throw new MustacheException("Mustache tag is not closed");
1130 |
1131 | index += length;
1132 | beforeEatWSIndex = index;
1133 | }
1134 |
1135 | void eatWhitespace()
1136 | {
1137 | beforeEatWSIndex = index;
1138 | index = src.length - src[index..$].stripLeft().length;
1139 | }
1140 |
1141 | void acceptKeySegment()
1142 | {
1143 | if (keySegmentStart >= beforeEatWSIndex)
1144 | throw new MustacheException("Missing tag name");
1145 |
1146 | key ~= src[keySegmentStart .. beforeEatWSIndex];
1147 | }
1148 |
1149 | eatWhitespace();
1150 | keySegmentStart = index;
1151 |
1152 | enum String dot = ".";
1153 | while (true) {
1154 | if (src[index..$].startsWith(eTag)) {
1155 | acceptKeySegment();
1156 | break;
1157 | } else if (src[index..$].startsWith(dot)) {
1158 | acceptKeySegment();
1159 | advance(dot.length);
1160 | eatWhitespace();
1161 | keySegmentStart = index;
1162 | } else {
1163 | advance(1);
1164 | eatWhitespace();
1165 | }
1166 | }
1167 |
1168 | end = index;
1169 | return key;
1170 | }
1171 |
1172 | unittest
1173 | {
1174 | { // single char, single segment, no whitespace
1175 | size_t end;
1176 | String src = "a}}";
1177 | auto key = parseKey(src, "}}", end);
1178 | assert(key.length == 1);
1179 | assert(key[0] == "a");
1180 | assert(src[end..$] == "}}");
1181 | }
1182 | { // multiple chars, single segment, no whitespace
1183 | size_t end;
1184 | String src = "Mio}}";
1185 | auto key = parseKey(src, "}}", end);
1186 | assert(key.length == 1);
1187 | assert(key[0] == "Mio");
1188 | assert(src[end..$] == "}}");
1189 | }
1190 | { // single char, multiple segments, no whitespace
1191 | size_t end;
1192 | String src = "a.b.c}}";
1193 | auto key = parseKey(src, "}}", end);
1194 | assert(key.length == 3);
1195 | assert(key[0] == "a");
1196 | assert(key[1] == "b");
1197 | assert(key[2] == "c");
1198 | assert(src[end..$] == "}}");
1199 | }
1200 | { // multiple chars, multiple segments, no whitespace
1201 | size_t end;
1202 | String src = "Mio.Ritsu.Yui}}";
1203 | auto key = parseKey(src, "}}", end);
1204 | assert(key.length == 3);
1205 | assert(key[0] == "Mio");
1206 | assert(key[1] == "Ritsu");
1207 | assert(key[2] == "Yui");
1208 | assert(src[end..$] == "}}");
1209 | }
1210 | { // whitespace
1211 | size_t end;
1212 | String src = " Mio . Ritsu }}";
1213 | auto key = parseKey(src, "}}", end);
1214 | assert(key.length == 2);
1215 | assert(key[0] == "Mio");
1216 | assert(key[1] == "Ritsu");
1217 | assert(src[end..$] == "}}");
1218 | }
1219 | { // single char custom end delimiter
1220 | size_t end;
1221 | String src = "Ritsu-";
1222 | auto key = parseKey(src, "-", end);
1223 | assert(key.length == 1);
1224 | assert(key[0] == "Ritsu");
1225 | assert(src[end..$] == "-");
1226 | }
1227 | { // extra chars at end
1228 | size_t end;
1229 | String src = "Ritsu}}abc";
1230 | auto key = parseKey(src, "}}", end);
1231 | assert(key.length == 1);
1232 | assert(key[0] == "Ritsu");
1233 | assert(src[end..$] == "}}abc");
1234 | }
1235 | { // error: no end delimiter
1236 | size_t end;
1237 | String src = "a.b.c";
1238 | try {
1239 | auto key = parseKey(src, "}}", end);
1240 | assert(false);
1241 | } catch (MustacheException e) { }
1242 | }
1243 | { // error: missing tag name
1244 | size_t end;
1245 | String src = " }}";
1246 | try {
1247 | auto key = parseKey(src, "}}", end);
1248 | assert(false);
1249 | } catch (MustacheException e) { }
1250 | }
1251 | { // error: missing ending tag name
1252 | size_t end;
1253 | String src = "Mio.}}";
1254 | try {
1255 | auto key = parseKey(src, "}}", end);
1256 | assert(false);
1257 | } catch (MustacheException e) { }
1258 | }
1259 | { // error: missing middle tag name
1260 | size_t end;
1261 | String src = "Mio. .Ritsu}}";
1262 | try {
1263 | auto key = parseKey(src, "}}", end);
1264 | assert(false);
1265 | } catch (MustacheException e) { }
1266 | }
1267 | }
1268 |
1269 | @trusted
1270 | static String keyToString(in String[] key)
1271 | {
1272 | if (key.length == 0)
1273 | return null;
1274 |
1275 | if (key.length == 1)
1276 | return key[0];
1277 |
1278 | Appender!String buf;
1279 | foreach (index, segment; key) {
1280 | if (index != 0)
1281 | buf.put('.');
1282 |
1283 | buf.put(segment);
1284 | }
1285 |
1286 | return buf.data;
1287 | }
1288 |
1289 | /*
1290 | * Mustache's node types
1291 | */
1292 | static enum NodeType
1293 | {
1294 | text, /// outside tag
1295 | var, /// {{}} or {{{}}} or {{&}}
1296 | section, /// {{#}} or {{^}}
1297 | partial /// {{>}}
1298 | }
1299 |
1300 |
1301 | /*
1302 | * Intermediate Representation of Mustache
1303 | */
1304 | static struct Node
1305 | {
1306 | NodeType type;
1307 |
1308 | union
1309 | {
1310 | String text;
1311 |
1312 | struct
1313 | {
1314 | String[] key;
1315 | bool flag; // true is inverted or unescape
1316 | Node[] childs; // for list section
1317 | String source; // for lambda section
1318 | }
1319 | }
1320 |
1321 | @trusted nothrow
1322 | {
1323 | /**
1324 | * Constructs with arguments.
1325 | *
1326 | * Params:
1327 | * t = raw text
1328 | */
1329 | this(String t)
1330 | {
1331 | type = NodeType.text;
1332 | text = t;
1333 | }
1334 |
1335 | /**
1336 | * ditto
1337 | *
1338 | * Params:
1339 | * t = Mustache's node type
1340 | * k = key string of tag
1341 | * f = invert? or escape?
1342 | */
1343 | this(NodeType t, String[] k, bool f = false)
1344 | {
1345 | type = t;
1346 | key = k;
1347 | flag = f;
1348 | }
1349 | }
1350 |
1351 | /**
1352 | * Represents the internal status as a string.
1353 | *
1354 | * Returns:
1355 | * stringized node representation.
1356 | */
1357 | string toString() const
1358 | {
1359 | string result;
1360 |
1361 | final switch (type) {
1362 | case NodeType.text:
1363 | result = "[T : \"" ~ to!string(text) ~ "\"]";
1364 | break;
1365 | case NodeType.var:
1366 | result = "[" ~ (flag ? "E" : "V") ~ " : \"" ~ keyToString(key) ~ "\"]";
1367 | break;
1368 | case NodeType.section:
1369 | result = "[" ~ (flag ? "I" : "S") ~ " : \"" ~ keyToString(key) ~ "\", [ ";
1370 | foreach (ref node; childs)
1371 | result ~= node.toString() ~ " ";
1372 | result ~= "], \"" ~ to!string(source) ~ "\"]";
1373 | break;
1374 | case NodeType.partial:
1375 | result = "[P : \"" ~ keyToString(key) ~ "\"]";
1376 | break;
1377 | }
1378 |
1379 | return result;
1380 | }
1381 | }
1382 |
1383 | unittest
1384 | {
1385 | Node section;
1386 | Node[] nodes, childs;
1387 |
1388 | nodes ~= Node("Hi ");
1389 | nodes ~= Node(NodeType.var, ["name"]);
1390 | nodes ~= Node(NodeType.partial, ["redbull"]);
1391 | {
1392 | childs ~= Node("Ritsu is ");
1393 | childs ~= Node(NodeType.var, ["attr"], true);
1394 | section = Node(NodeType.section, ["ritsu"], false);
1395 | section.childs = childs;
1396 | nodes ~= section;
1397 | }
1398 |
1399 | assert(to!string(nodes) == `[[T : "Hi "], [V : "name"], [P : "redbull"], ` ~
1400 | `[S : "ritsu", [ [T : "Ritsu is "] [E : "attr"] ], ""]]`);
1401 | }
1402 | }
1403 |
1404 | unittest
1405 | {
1406 | alias MustacheEngine!(string) Mustache;
1407 |
1408 | std.file.write("unittest.mustache", "Level: {{lvl}}");
1409 | scope(exit) std.file.remove("unittest.mustache");
1410 |
1411 | Mustache mustache;
1412 | auto context = new Mustache.Context;
1413 |
1414 | { // no
1415 | mustache.level = Mustache.CacheLevel.no;
1416 | context["lvl"] = "no";
1417 | assert(mustache.render("unittest", context) == "Level: no");
1418 | assert(mustache.caches_.length == 0);
1419 | }
1420 | { // check
1421 | mustache.level = Mustache.CacheLevel.check;
1422 | context["lvl"] = "check";
1423 | assert(mustache.render("unittest", context) == "Level: check");
1424 | assert(mustache.caches_.length > 0);
1425 |
1426 | core.thread.Thread.sleep(dur!"seconds"(1));
1427 | std.file.write("unittest.mustache", "Modified");
1428 | assert(mustache.render("unittest", context) == "Modified");
1429 | }
1430 | mustache.caches_.remove("./unittest.mustache"); // remove previous cache
1431 | { // once
1432 | mustache.level = Mustache.CacheLevel.once;
1433 | context["lvl"] = "once";
1434 | assert(mustache.render("unittest", context) == "Modified");
1435 | assert(mustache.caches_.length > 0);
1436 |
1437 | core.thread.Thread.sleep(dur!"seconds"(1));
1438 | std.file.write("unittest.mustache", "Level: {{lvl}}");
1439 | assert(mustache.render("unittest", context) == "Modified");
1440 | }
1441 | }
1442 |
1443 | unittest
1444 | {
1445 | alias Mustache = MustacheEngine!(string);
1446 |
1447 | std.file.write("unittest.mustache", "{{>name}}");
1448 | scope(exit) std.file.remove("unittest.mustache");
1449 | std.file.write("other.mustache", "Ok");
1450 | scope(exit) std.file.remove("other.mustache");
1451 |
1452 | Mustache mustache;
1453 | auto context = new Mustache.Context;
1454 | mustache.findPath((path) {
1455 | if (path == "name") {
1456 | return "other." ~ mustache.ext;
1457 | } else {
1458 | return path ~ "." ~ mustache.ext;
1459 | }
1460 | });
1461 |
1462 | assert(mustache.render("unittest", context) == "Ok");
1463 | }
1464 |
--------------------------------------------------------------------------------
/win.mak:
--------------------------------------------------------------------------------
1 | DMD = dmd
2 | LIB = mustache.lib
3 | DFLAGS = -O -release -inline -nofloat -w -d -Isrc
4 | UDFLAGS = -w -g -debug -unittest
5 | SRCS = src\mustache.d
6 |
7 | # DDoc
8 | DOCS = mustache.html
9 | DDOCFLAGS = -Dd. -c -o- std.ddoc -Isrc
10 |
11 | target: $(LIB)
12 |
13 | $(LIB):
14 | $(DMD) $(DFLAGS) -lib -of$(LIB) $(SRCS)
15 |
16 | doc:
17 | $(DMD) $(DDOCFLAGS) $(SRCS)
18 |
19 | clean:
20 | rm $(DOCS) $(LIB)
21 |
--------------------------------------------------------------------------------