├── .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 | [![Build Status](https://travis-ci.org/repeatedly/mustache-d.png)](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 |
12 | {{#projects}} 13 | 14 |

{{name}}

15 |

{{description}}

16 |
17 | {{/projects}} 18 |
19 | 20 |
21 | 22 | ↩ 23 | 24 |
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 |
56 | 70 | 74 |
75 | 76 | 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 |
 //{{name}} to "Chris"
305 |  context["name"] = "Chirs"
306 | 
307 | 308 | Lists section("addSubContext" name is drived from ctemplate's API): 309 |
 //{{#repo}}
310 |  //<b>{{name}}</b>
311 |  //{{/repo}}
312 |  //  to
313 |  //<b>resque</b>
314 |  //<b>hub</b>
315 |  //<b>rip</b>
316 |  foreach (name; ["resque", "hub", "rip"]) {
317 |      auto sub = context.addSubContext("repo");
318 |      sub["name"] = name;
319 |  }
320 | 
321 | 322 | Variable section: 323 |
 //{{#person?}}Hi {{name}}{{/person?}} to "Hi Jon"
324 |  context["person?"] = ["name" : "Jon"];
325 | 
326 | 327 | Lambdas section: 328 |
 //{{#wrapped}}awesome{{/wrapped}} to "<b>awesome</b>"
329 |  context["Wrapped"] = (string str) { return "<b>" ~ str ~ "</b>"; };
330 | 
331 | 332 | Inverted section: 333 |
 //{{#repo}}<b>{{name}}</b>{{/repo}}
334 |  //{{^repo}}No repos :({{/repo}}
335 |  //  to
336 |  //No repos :(
337 |  context["foo"] = "bar";  // not set to "repo"
338 | 
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: 346 | 347 |
String keykey string to search

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: 363 | 364 | 365 | 366 |
valuesome type value to assign
keykey string to assign

367 | 368 |
369 |
void useSection(in String key); 370 |
371 |
Enable key's section. 372 |

373 | Parameters: 374 | 375 |
String keykey string to enable

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: 386 | 387 | 388 | 389 |
String keykey string to add
size_t sizereserve size for avoiding reallocation

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: 427 | 428 | 429 | 430 |
string nametemplate name without extenstion
Context contextMustache context for rendering

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 | 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 | --------------------------------------------------------------------------------