1 /** HTML output generator implementation. 2 */ 3 module diet.html; 4 5 import diet.defs; 6 import diet.dom; 7 import diet.internal.html; 8 import diet.internal..string; 9 import diet.input; 10 import diet.parser; 11 import diet.traits; 12 13 14 /** Compiles a Diet template file that is available as a string import. 15 16 The final HTML will be written to the given `_diet_output` output range. 17 18 Params: 19 filename = Name of the main Diet template file. 20 ALIASES = A list of variables to make available inside of the template, 21 as well as traits structs annotated with the `@dietTraits` 22 attribute. 23 dst = The output range to write the generated HTML to. 24 25 Traits: 26 In addition to the default Diet traits, adding an enum field 27 `htmlOutputStyle` of type `HTMLOutputStyle` to a traits 28 struct can be used to control the style of the generated 29 HTML. 30 31 See_Also: `compileHTMLDietString`, `compileHTMLDietStrings` 32 */ 33 template compileHTMLDietFile(string filename, ALIASES...) 34 { 35 import diet.internal..string : stripUTF8BOM; 36 private static immutable contents = stripUTF8BOM(import(filename)); 37 alias compileHTMLDietFile = compileHTMLDietFileString!(filename, contents, ALIASES); 38 } 39 40 41 /** Compiles a Diet template given as a string, with support for includes and extensions. 42 43 This function behaves the same as `compileHTMLDietFile`, except that the 44 contents of the file are 45 46 The final HTML will be written to the given `_diet_output` output range. 47 48 Params: 49 filename = The name to associate with `contents` 50 contents = The contents of the Diet template 51 ALIASES = A list of variables to make available inside of the template, 52 as well as traits structs annotated with the `@dietTraits` 53 attribute. 54 dst = The output range to write the generated HTML to. 55 56 See_Also: `compileHTMLDietFile`, `compileHTMLDietString`, `compileHTMLDietStrings` 57 */ 58 template compileHTMLDietFileString(string filename, alias contents, ALIASES...) 59 { 60 import std.conv : to; 61 enum _diet_files = collectFiles!(filename, contents); 62 63 version (DietUseCache) enum _diet_use_cache = true; 64 else enum _diet_use_cache = false; 65 66 67 ulong computeTemplateHash() 68 { 69 ulong ret = 0; 70 void hash(string s) 71 { 72 foreach (char c; s) { 73 ret *= 9198984547192449281; 74 ret += c * 7576889555963512219; 75 } 76 } 77 foreach (ref f; _diet_files) { 78 hash(f.name); 79 hash(f.contents); 80 } 81 return ret; 82 } 83 84 enum _diet_hash = computeTemplateHash(); 85 enum _diet_cache_file_name = "_cached_"~filename~"_"~_diet_hash.to!string~".d"; 86 87 static if (_diet_use_cache && is(typeof(import(_diet_cache_file_name)))) { 88 pragma(msg, "Using cached Diet HTML template "~filename~"..."); 89 enum _dietParser = import(_diet_cache_file_name); 90 } else { 91 alias TRAITS = DietTraits!ALIASES; 92 pragma(msg, "Compiling Diet HTML template "~filename~"..."); 93 private Document _diet_nodes() { return applyTraits!TRAITS(parseDiet!(translate!TRAITS)(_diet_files)); } 94 enum _dietParser = getHTMLMixin(_diet_nodes(), dietOutputRangeName, getHTMLOutputStyle!TRAITS); 95 96 static if (_diet_use_cache) { 97 shared static this() 98 { 99 import std.file : exists, write; 100 if (!exists("views/"~_diet_cache_file_name)) 101 write("views/"~_diet_cache_file_name, _dietParser); 102 } 103 } 104 } 105 106 // uses the correct range name and removes 'dst' from the scope 107 private void exec(R)(ref R _diet_output) 108 { 109 mixin(localAliasesMixin!(0, ALIASES)); 110 //pragma(msg, _dietParser); 111 mixin(_dietParser); 112 } 113 114 void compileHTMLDietFileString(R)(ref R dst) 115 { 116 exec(dst); 117 } 118 } 119 120 121 /** Compiles a Diet template given as a string. 122 123 The final HTML will be written to the given `_diet_output` output range. 124 125 Params: 126 contents = The contents of the Diet template 127 ALIASES = A list of variables to make available inside of the template, 128 as well as traits structs annotated with the `@dietTraits` 129 attribute. 130 dst = The output range to write the generated HTML to. 131 132 See_Also: `compileHTMLDietFileString`, `compileHTMLDietStrings` 133 */ 134 template compileHTMLDietString(string contents, ALIASES...) 135 { 136 void compileHTMLDietString(R)(ref R dst) 137 { 138 compileHTMLDietStrings!(Group!(contents, "diet-string"), ALIASES)(dst); 139 } 140 } 141 142 143 /** Compiles a set of Diet template files. 144 145 The final HTML will be written to the given `_diet_output` output range. 146 147 Params: 148 FILES_GROUP = A `diet.input.Group` containing an alternating list of 149 file names and file contents. 150 ALIASES = A list of variables to make available inside of the template, 151 as well as traits structs annotated with the `@dietTraits` 152 attribute. 153 dst = The output range to write the generated HTML to. 154 155 See_Also: `compileHTMLDietString`, `compileHTMLDietStrings` 156 */ 157 template compileHTMLDietStrings(alias FILES_GROUP, ALIASES...) 158 { 159 alias TRAITS = DietTraits!ALIASES; 160 private static Document _diet_nodes() { return applyTraits!TRAITS(parseDiet!(translate!TRAITS)(filesFromGroup!FILES_GROUP)); } 161 162 // uses the correct range name and removes 'dst' from the scope 163 private void exec(R)(ref R _diet_output) 164 { 165 mixin(localAliasesMixin!(0, ALIASES)); 166 //pragma(msg, getHTMLMixin(_diet_nodes())); 167 mixin(getHTMLMixin(_diet_nodes(), dietOutputRangeName, getHTMLOutputStyle!TRAITS)); 168 } 169 170 void compileHTMLDietStrings(R)(ref R dst) 171 { 172 exec(dst); 173 } 174 } 175 176 /** Returns a mixin string that generates HTML for the given DOM tree. 177 178 Params: 179 nodes = The root nodes of the DOM tree 180 range_name = Optional custom name to use for the output range, defaults 181 to `_diet_output`. 182 183 Returns: 184 A string of D statements suitable to be mixed in inside of a function. 185 */ 186 string getHTMLMixin(in Document doc, string range_name = dietOutputRangeName, HTMLOutputStyle style = HTMLOutputStyle.compact) 187 { 188 CTX ctx; 189 ctx.pretty = style == HTMLOutputStyle.pretty; 190 ctx.rangeName = range_name; 191 string ret = "import diet.internal.html : htmlEscape, htmlAttribEscape;\n"; 192 ret ~= "import std.format : formattedWrite;\n"; 193 foreach (i, n; doc.nodes) 194 ret ~= ctx.getHTMLMixin(n, false); 195 ret ~= ctx.flushRawText(); 196 return ret; 197 } 198 199 unittest { 200 import diet.parser; 201 void test(string src)(string expected) { 202 import std.array : appender; 203 static const n = parseDiet(src); 204 auto _diet_output = appender!string(); 205 //pragma(msg, getHTMLMixin(n)); 206 mixin(getHTMLMixin(n)); 207 assert(_diet_output.data == expected, _diet_output.data); 208 } 209 210 test!"doctype html\nfoo(test=true)"("<!DOCTYPE html><foo test></foo>"); 211 test!"doctype html X\nfoo(test=true)"("<!DOCTYPE html X><foo test=\"test\"></foo>"); 212 test!"doctype X\nfoo(test=true)"("<!DOCTYPE X><foo test=\"test\"/>"); 213 test!"foo(test=2+3)"("<foo test=\"5\"></foo>"); 214 test!"foo(test='#{2+3}')"("<foo test=\"5\"></foo>"); 215 test!"foo #{2+3}"("<foo>5</foo>"); 216 test!"foo= 2+3"("<foo>5</foo>"); 217 test!"- int x = 3;\nfoo=x"("<foo>3</foo>"); 218 test!"- foreach (i; 0 .. 2)\n\tfoo"("<foo></foo><foo></foo>"); 219 test!"div(*ngFor=\"\\#item of list\")"( 220 "<div *ngFor=\"#item of list\"></div>" 221 ); 222 test!".foo"("<div class=\"foo\"></div>"); 223 test!"#foo"("<div id=\"foo\"></div>"); 224 } 225 226 227 /** Determines how the generated HTML gets styled. 228 229 To use this, put an enum field named `htmlOutputStyle` into a diet traits 230 struct and pass that to the render function. 231 232 The default output style is `compact`. 233 */ 234 enum HTMLOutputStyle { 235 compact, /// Outputs no extraneous whitespace (including line breaks) around HTML tags 236 pretty, /// Inserts line breaks and indents lines according to their nesting level in the HTML structure 237 } 238 239 /// 240 unittest { 241 @dietTraits 242 struct Traits { 243 enum htmlOutputStyle = HTMLOutputStyle.pretty; 244 } 245 246 import std.array : appender; 247 auto dst = appender!string(); 248 dst.compileHTMLDietString!("html\n\tbody\n\t\tp Hello", Traits); 249 import std.conv : to; 250 assert(dst.data == "<html>\n\t<body>\n\t\t<p>Hello</p>\n\t</body>\n</html>", [dst.data].to!string); 251 } 252 253 private @property template getHTMLOutputStyle(TRAITS...) 254 { 255 static if (TRAITS.length) { 256 static if (is(typeof(TRAITS[0].htmlOutputStyle))) 257 enum getHTMLOutputStyle = TRAITS[0].htmlOutputStyle; 258 else enum getHTMLOutputStyle = getHTMLOutputStyle!(TRAITS[1 .. $]); 259 } else enum getHTMLOutputStyle = HTMLOutputStyle.compact; 260 } 261 262 private string getHTMLMixin(ref CTX ctx, in Node node, bool in_pre) 263 { 264 switch (node.name) { 265 default: return ctx.getElementMixin(node, in_pre); 266 case "doctype": return ctx.getDoctypeMixin(node); 267 case Node.SpecialName.code: return ctx.getCodeMixin(node, in_pre); 268 case Node.SpecialName.comment: return ctx.getCommentMixin(node); 269 case Node.SpecialName.hidden: return null; 270 case Node.SpecialName.text: 271 string ret; 272 foreach (i, c; node.contents) 273 ret ~= ctx.getNodeContentsMixin(c, in_pre); 274 if (in_pre) ctx.plainNewLine(); 275 else ctx.prettyNewLine(); 276 return ret; 277 } 278 } 279 280 private string getElementMixin(ref CTX ctx, in Node node, bool in_pre) 281 { 282 import std.algorithm : countUntil; 283 284 if (node.name == "pre") in_pre = true; 285 286 bool need_newline = ctx.needPrettyNewline(node.contents); 287 288 bool is_singular_tag; 289 // determine if we need a closing tag or have a singular tag 290 if (ctx.isHTML) { 291 switch (node.name) { 292 default: break; 293 case "area", "base", "basefont", "br", "col", "embed", "frame", "hr", "img", "input", 294 "keygen", "link", "meta", "param", "source", "track", "wbr": 295 is_singular_tag = true; 296 need_newline = true; 297 break; 298 } 299 } else if (!node.hasNonWhitespaceContent) is_singular_tag = true; 300 301 // write tag name 302 string tagname = node.name.length ? node.name : "div"; 303 string ret; 304 if (node.attribs & NodeAttribs.fitOutside || in_pre) 305 ctx.inhibitNewLine(); 306 else if (need_newline) 307 ctx.prettyNewLine(); 308 309 ret ~= ctx.rawText(node.loc, "<"~tagname); 310 311 bool had_class = false; 312 313 // write attributes 314 foreach (ai, att_; node.attributes) { 315 auto att = att_.dup; // this sucks... 316 317 // merge multiple class attributes into one 318 if (att.name == "class") { 319 if (had_class) continue; 320 had_class = true; 321 foreach (ca; node.attributes[ai+1 .. $]) { 322 if (ca.name != "class") continue; 323 if (!ca.contents.length || (ca.isText && !ca.expectText.length)) continue; 324 att.addText(" "); 325 att.addContents(ca.contents); 326 } 327 } 328 329 bool is_expr = att.contents.length == 1 && att.contents[0].kind == AttributeContent.Kind.interpolation; 330 331 if (is_expr) { 332 auto expr = att.contents[0].value; 333 334 if (expr == "true") { 335 if (ctx.isHTML5) ret ~= ctx.rawText(node.loc, " "~att.name); 336 else ret ~= ctx.rawText(node.loc, " "~att.name~"=\""~att.name~"\""); 337 continue; 338 } 339 340 ret ~= ctx.statement(node.loc, q{ 341 static if (is(typeof(() { return %s; }()) == bool) ) 342 }~'{', expr); 343 if (ctx.isHTML5) 344 ret ~= ctx.statement(node.loc, q{if (%s) %s.put(" %s");}, expr, ctx.rangeName, att.name); 345 else 346 ret ~= ctx.statement(node.loc, q{if (%s) %s.put(" %s=\"%s\"");}, expr, ctx.rangeName, att.name, att.name); 347 348 ret ~= ctx.statement(node.loc, "} else "~q{static if (is(typeof(%s) : const(char)[])) }~"{{", expr); 349 ret ~= ctx.statement(node.loc, q{ auto _diet_val = %s;}, expr); 350 ret ~= ctx.statement(node.loc, q{ if (_diet_val !is null) }~'{'); 351 ret ~= ctx.rawText(node.loc, " "~att.name~"=\""); 352 ret ~= ctx.statement(node.loc, q{ %s.filterHTMLAttribEscape(_diet_val);}, ctx.rangeName); 353 ret ~= ctx.rawText(node.loc, "\""); 354 ret ~= ctx.statement(node.loc, " }"); 355 ret ~= ctx.statement(node.loc, "}} else {"); 356 } 357 358 ret ~= ctx.rawText(node.loc, " "~att.name ~ "=\""); 359 360 foreach (i, v; att.contents) { 361 final switch (v.kind) with (AttributeContent.Kind) { 362 case text: 363 ret ~= ctx.rawText(node.loc, htmlAttribEscape(v.value)); 364 break; 365 case interpolation, rawInterpolation: 366 ret ~= ctx.statement(node.loc, q{%s.htmlAttribEscape(%s);}, ctx.rangeName, v.value); 367 break; 368 } 369 } 370 371 ret ~= ctx.rawText(node.loc, "\""); 372 373 if (is_expr) ret ~= ctx.statement(node.loc, "}"); 374 } 375 376 // determine if we need a closing tag or have a singular tag 377 if (is_singular_tag) { 378 enforcep(!node.hasNonWhitespaceContent, "Singular HTML element '"~node.name~"' may not have contents.", node.loc); 379 ret ~= ctx.rawText(node.loc, "/>"); 380 if (need_newline && !(node.attribs & NodeAttribs.fitOutside)) 381 ctx.prettyNewLine(); 382 return ret; 383 } 384 385 ret ~= ctx.rawText(node.loc, ">"); 386 387 // write contents 388 if (need_newline) { 389 ctx.depth++; 390 if (!(node.attribs & NodeAttribs.fitInside) && !in_pre) 391 ctx.prettyNewLine(); 392 } 393 394 foreach (i, c; node.contents) 395 ret ~= ctx.getNodeContentsMixin(c, in_pre); 396 397 if (need_newline && !in_pre) { 398 ctx.depth--; 399 if (!(node.attribs & NodeAttribs.fitInside) && !in_pre) 400 ctx.prettyNewLine(); 401 } else ctx.inhibitNewLine(); 402 403 // write end tag 404 ret ~= ctx.rawText(node.loc, "</"~tagname~">"); 405 406 if ((node.attribs & NodeAttribs.fitOutside) || in_pre) 407 ctx.inhibitNewLine(); 408 else if (need_newline) 409 ctx.prettyNewLine(); 410 411 return ret; 412 } 413 414 private string getNodeContentsMixin(ref CTX ctx, in NodeContent c, bool in_pre) 415 { 416 final switch (c.kind) with (NodeContent.Kind) { 417 case node: 418 return getHTMLMixin(ctx, c.node, in_pre); 419 case text: 420 return ctx.rawText(c.loc, c.value); 421 case interpolation: 422 return ctx.textStatement(c.loc, q{%s.htmlEscape(%s);}, ctx.rangeName, c.value); 423 case rawInterpolation: 424 return ctx.textStatement(c.loc, q{() @trusted { return (&%s); } ().formattedWrite("%%s", %s);}, ctx.rangeName, c.value); 425 } 426 } 427 428 private string getDoctypeMixin(ref CTX ctx, in Node node) 429 { 430 import std.algorithm.searching : startsWith; 431 import diet.internal..string; 432 433 if (node.name == "!!!") 434 ctx.statement(node.loc, q{pragma(msg, "Use of '!!!' is deprecated. Use 'doctype' instead.");}); 435 436 enforcep(node.contents.length == 1 && node.contents[0].kind == NodeContent.Kind.text, 437 "Only doctype specifiers allowed as content for doctype nodes.", node.loc); 438 439 auto args = ctstrip(node.contents[0].value); 440 441 ctx.isHTML5 = false; 442 443 string doctype_str = "!DOCTYPE html"; 444 switch (args) { 445 case "5": 446 case "": 447 case "html": 448 ctx.isHTML5 = true; 449 break; 450 case "xml": 451 doctype_str = `?xml version="1.0" encoding="utf-8" ?`; 452 ctx.isHTML = false; 453 break; 454 case "transitional": 455 doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ` 456 ~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd`; 457 break; 458 case "strict": 459 doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ` 460 ~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"`; 461 break; 462 case "frameset": 463 doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" ` 464 ~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"`; 465 break; 466 case "1.1": 467 doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" ` 468 ~ `"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"`; 469 break; 470 case "basic": 471 doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" ` 472 ~ `"http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd"`; 473 break; 474 case "mobile": 475 doctype_str = `!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" ` 476 ~ `"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd"`; 477 break; 478 default: 479 doctype_str = "!DOCTYPE " ~ args; 480 ctx.isHTML = args.startsWith("html "); 481 break; 482 } 483 484 return ctx.rawText(node.loc, "<"~doctype_str~">"); 485 } 486 487 private string getCodeMixin(ref CTX ctx, in ref Node node, bool in_pre) 488 { 489 enforcep(node.attributes.length == 0, "Code lines may not have attributes.", node.loc); 490 enforcep(node.attribs == NodeAttribs.none, "Code lines may not specify translation or text block suffixes.", node.loc); 491 if (node.contents.length == 0) return null; 492 493 string ret; 494 bool got_code = false; 495 foreach (i, c; node.contents) { 496 if (i == 0 && c.kind == NodeContent.Kind.text) { 497 ret ~= ctx.statement(node.loc, "%s {", c.value); 498 got_code = true; 499 } else { 500 assert(c.kind == NodeContent.Kind.node); 501 ret ~= ctx.getHTMLMixin(c.node, in_pre); 502 } 503 } 504 ret ~= ctx.statement(node.loc, "}"); 505 return ret; 506 } 507 508 private string getCommentMixin(ref CTX ctx, in ref Node node) 509 { 510 string ret = ctx.rawText(node.loc, "<!--"); 511 ctx.depth++; 512 foreach (i, c; node.contents) 513 ret ~= ctx.getNodeContentsMixin(c, false); 514 ctx.depth--; 515 ret ~= ctx.rawText(node.loc, "-->"); 516 return ret; 517 } 518 519 private struct CTX { 520 enum NewlineState { 521 none, 522 plain, 523 pretty, 524 inhibit 525 } 526 527 bool isHTML5, isHTML = true; 528 bool pretty; 529 int depth = 0; 530 string rangeName; 531 bool inRawText = false; 532 NewlineState newlineState = NewlineState.none; 533 bool anyText; 534 535 pure string statement(ARGS...)(Location loc, string fmt, ARGS args) 536 { 537 import std..string : format; 538 string ret = flushRawText(); 539 ret ~= ("#line %s \"%s\"\n"~fmt~"\n").format(loc.line+1, loc.file, args); 540 return ret; 541 } 542 543 pure string textStatement(ARGS...)(Location loc, string fmt, ARGS args) 544 { 545 string ret; 546 if (newlineState != NewlineState.none) ret ~= rawText(loc, null); 547 ret ~= statement(loc, fmt, args); 548 return ret; 549 } 550 551 pure string rawText(ARGS...)(Location loc, string text) 552 { 553 string ret; 554 if (!this.inRawText) { 555 ret = this.rangeName ~ ".put(\""; 556 this.inRawText = true; 557 } 558 ret ~= outputPendingNewline(); 559 ret ~= dstringEscape(text); 560 anyText = true; 561 return ret; 562 } 563 564 pure string flushRawText() 565 { 566 if (this.inRawText) { 567 this.inRawText = false; 568 return "\");\n"; 569 } 570 return null; 571 } 572 573 void plainNewLine() { if (newlineState != NewlineState.inhibit) newlineState = NewlineState.plain; } 574 void prettyNewLine() { if (newlineState != NewlineState.inhibit) newlineState = NewlineState.pretty; } 575 void inhibitNewLine() { newlineState = NewlineState.inhibit; } 576 577 bool needPrettyNewline(in NodeContent[] contents) { 578 import std.algorithm.searching : any; 579 return pretty && contents.any!(c => c.kind == NodeContent.Kind.node); 580 } 581 582 private pure string outputPendingNewline() 583 { 584 auto st = newlineState; 585 newlineState = NewlineState.none; 586 587 final switch (st) { 588 case NewlineState.none: return null; 589 case NewlineState.inhibit:return null; 590 case NewlineState.plain: return "\n"; 591 case NewlineState.pretty: 592 import std.array : replicate; 593 return anyText ? "\n"~"\t".replicate(depth) : null; 594 } 595 } 596 } 597 598 unittest { 599 static string compile(string diet, ALIASES...)() { 600 import std.array : appender; 601 import std..string : strip; 602 auto dst = appender!string; 603 compileHTMLDietString!(diet, ALIASES)(dst); 604 return strip(cast(string)(dst.data)); 605 } 606 607 assert(compile!(`!!! 5`) == `<!DOCTYPE html>`, `_`~compile!(`!!! 5`)~`_`); 608 assert(compile!(`!!! html`) == `<!DOCTYPE html>`); 609 assert(compile!(`doctype html`) == `<!DOCTYPE html>`); 610 assert(compile!(`doctype xml`) == `<?xml version="1.0" encoding="utf-8" ?>`); 611 assert(compile!(`p= 5`) == `<p>5</p>`); 612 assert(compile!(`script= 5`) == `<script>5</script>`); 613 assert(compile!(`style= 5`) == `<style>5</style>`); 614 //assert(compile!(`include #{"p Hello"}`) == "<p>Hello</p>"); 615 assert(compile!(`<p>Hello</p>`) == "<p>Hello</p>"); 616 assert(compile!(`// I show up`) == "<!-- I show up-->"); 617 assert(compile!(`//-I don't show up`) == ""); 618 assert(compile!(`//- I don't show up`) == ""); 619 620 // issue 372 621 assert(compile!(`div(class="")`) == `<div></div>`); 622 assert(compile!(`div.foo(class="")`) == `<div class="foo"></div>`); 623 assert(compile!(`div.foo(class="bar")`) == `<div class="foo bar"></div>`); 624 assert(compile!(`div(class="foo")`) == `<div class="foo"></div>`); 625 assert(compile!(`div#foo(class='')`) == `<div id="foo"></div>`); 626 627 // issue 19 628 assert(compile!(`input(checked=false)`) == `<input/>`); 629 assert(compile!(`input(checked=true)`) == `<input checked="checked"/>`); 630 assert(compile!(`input(checked=(true && false))`) == `<input/>`); 631 assert(compile!(`input(checked=(true || false))`) == `<input checked="checked"/>`); 632 633 assert(compile!(q{- import std.algorithm.searching : any; 634 input(checked=([false].any))}) == `<input/>`); 635 assert(compile!(q{- import std.algorithm.searching : any; 636 input(checked=([true].any))}) == `<input checked="checked"/>`); 637 638 assert(compile!(q{- bool foo() { return false; } 639 input(checked=foo)}) == `<input/>`); 640 assert(compile!(q{- bool foo() { return true; } 641 input(checked=foo)}) == `<input checked="checked"/>`); 642 643 // issue 520 644 assert(compile!("- auto cond = true;\ndiv(someattr=cond ? \"foo\" : null)") == "<div someattr=\"foo\"></div>"); 645 assert(compile!("- auto cond = false;\ndiv(someattr=cond ? \"foo\" : null)") == "<div></div>"); 646 assert(compile!("- auto cond = false;\ndiv(someattr=cond ? true : false)") == "<div></div>"); 647 assert(compile!("- auto cond = true;\ndiv(someattr=cond ? true : false)") == "<div someattr=\"someattr\"></div>"); 648 assert(compile!("doctype html\n- auto cond = true;\ndiv(someattr=cond ? true : false)") 649 == "<!DOCTYPE html><div someattr></div>"); 650 assert(compile!("doctype html\n- auto cond = false;\ndiv(someattr=cond ? true : false)") 651 == "<!DOCTYPE html><div></div>"); 652 653 // issue 510 654 assert(compile!("pre.test\n\tfoo") == "<pre class=\"test\"><foo></foo></pre>"); 655 assert(compile!("pre.test.\n\tfoo") == "<pre class=\"test\">foo</pre>"); 656 assert(compile!("pre.test. foo") == "<pre class=\"test\"></pre>"); 657 assert(compile!("pre().\n\tfoo") == "<pre>foo</pre>"); 658 assert(compile!("pre#foo.test(data-img=\"sth\",class=\"meh\"). something\n\tmeh") == 659 "<pre id=\"foo\" class=\"test meh\" data-img=\"sth\">meh</pre>"); 660 661 assert(compile!("input(autofocus)").length); 662 663 assert(compile!("- auto s = \"\";\ninput(type=\"text\",value=\"&\\\"#{s}\")") 664 == `<input type="text" value="&""/>`); 665 assert(compile!("- auto param = \"t=1&u=1\";\na(href=\"/?#{param}&v=1\") foo") 666 == `<a href="/?t=1&u=1&v=1">foo</a>`); 667 668 // issue #1021 669 assert(compile!("html( lang=\"en\" )") 670 == "<html lang=\"en\"></html>"); 671 672 // issue #1033 673 assert(compile!("input(placeholder=')')") 674 == "<input placeholder=\")\"/>"); 675 assert(compile!("input(placeholder='(')") 676 == "<input placeholder=\"(\"/>"); 677 } 678 679 unittest { // blocks and extensions 680 static string compilePair(string extension, string base, ALIASES...)() { 681 import std.array : appender; 682 import std..string : strip; 683 auto dst = appender!string; 684 compileHTMLDietStrings!(Group!(extension, "extension.dt", base, "base.dt"), ALIASES)(dst); 685 return strip(dst.data); 686 } 687 688 assert(compilePair!("extends base\nblock test\n\tp Hello", "body\n\tblock test") 689 == "<body><p>Hello</p></body>"); 690 assert(compilePair!("extends base\nblock test\n\tp Hello", "body\n\tblock test\n\t\tp Default") 691 == "<body><p>Hello</p></body>"); 692 assert(compilePair!("extends base", "body\n\tblock test\n\t\tp Default") 693 == "<body><p>Default</p></body>"); 694 assert(compilePair!("extends base\nprepend test\n\tp Hello", "body\n\tblock test\n\t\tp Default") 695 == "<body><p>Hello</p><p>Default</p></body>"); 696 } 697 698 /*@nogc*/ @safe unittest { // NOTE: formattedWrite is not @nogc 699 static struct R { 700 @nogc @safe nothrow: 701 void put(in char[]) {} 702 void put(char) {} 703 void put(dchar) {} 704 } 705 706 R r; 707 r.compileHTMLDietString!( 708 `doctype html 709 html 710 - foreach (i; 0 .. 10) 711 title= i 712 title t #{12} !{13} 713 `); 714 } 715 716 unittest { // issue 4 - nested text in code 717 static string compile(string diet, ALIASES...)() { 718 import std.array : appender; 719 import std..string : strip; 720 auto dst = appender!string; 721 compileHTMLDietString!(diet, ALIASES)(dst); 722 return strip(cast(string)(dst.data)); 723 } 724 assert(compile!"- if (true)\n\t| int bar;" == "int bar;"); 725 } 726 727 unittest { // class instance variables 728 import std.array : appender; 729 import std..string : strip; 730 731 static class C { 732 int x = 42; 733 734 string test() 735 { 736 auto dst = appender!string; 737 dst.compileHTMLDietString!("| #{x}", x); 738 return dst.data; 739 } 740 } 741 742 auto c = new C; 743 assert(c.test().strip == "42"); 744 } 745 746 unittest { // raw interpolation for non-copyable range 747 struct R { @disable this(this); void put(dchar) {} void put(in char[]) {} } 748 R r; 749 r.compileHTMLDietString!("a !{2}"); 750 } 751 752 unittest { 753 assert(utCompile!(".foo(class=true?\"bar\":\"baz\")") == "<div class=\"foo bar\"></div>"); 754 } 755 756 version (unittest) { 757 private string utCompile(string diet, ALIASES...)() { 758 import std.array : appender; 759 import std..string : strip; 760 auto dst = appender!string; 761 compileHTMLDietString!(diet, ALIASES)(dst); 762 return strip(cast(string)(dst.data)); 763 } 764 } 765 766 unittest { // blank lines in text blocks 767 assert(utCompile!("pre.\n\tfoo\n\n\tbar") == "<pre>foo\n\nbar</pre>"); 768 } 769 770 unittest { // singular tags should be each on their own line 771 enum src = "p foo\nlink\nlink"; 772 enum dst = "<p>foo</p>\n<link/>\n<link/>"; 773 @dietTraits struct T { enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.pretty; } 774 assert(utCompile!(src, T) == dst); 775 } 776 777 unittest { // ignore whitespace content for singular tags 778 assert(utCompile!("link ") == "<link/>"); 779 assert(utCompile!("link \n\t ") == "<link/>"); 780 } 781 782 unittest { 783 @dietTraits struct T { enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.pretty; } 784 import std.conv : to; 785 // no extraneous newlines before text lines 786 assert(utCompile!("foo\n\tbar text1\n\t| text2", T) == "<foo>\n\t<bar>text1</bar>text2\n</foo>"); 787 assert(utCompile!("foo\n\tbar: baz\n\t| text2", T) == "<foo>\n\t<bar>\n\t\t<baz></baz>\n\t</bar>\n\ttext2\n</foo>"); 788 // fit inside/outside + pretty printing - issue #27 789 assert(utCompile!("| foo\na<> bar\n| baz", T) == "foo<a>bar</a>baz"); 790 assert(utCompile!("foo\n\ta< bar", T) == "<foo>\n\t<a>bar</a>\n</foo>"); 791 assert(utCompile!("foo\n\ta> bar", T) == "<foo><a>bar</a></foo>"); 792 assert(utCompile!("a\nfoo<\n\ta bar\nb", T) == "<a></a>\n<foo><a>bar</a></foo>\n<b></b>"); 793 assert(utCompile!("a\nfoo>\n\ta bar\nb", T) == "<a></a><foo>\n\t<a>bar</a>\n</foo><b></b>"); 794 // hard newlines in pre blocks 795 assert(utCompile!("pre\n\t| foo\n\t| bar", T) == "<pre>foo\nbar</pre>"); 796 assert(utCompile!("pre\n\tcode\n\t\t| foo\n\t\t| bar", T) == "<pre><code>foo\nbar</code></pre>"); 797 // always hard breaks for text blocks 798 assert(utCompile!("pre.\n\tfoo\n\tbar", T) == "<pre>foo\nbar</pre>"); 799 assert(utCompile!("foo.\n\tfoo\n\tbar", T) == "<foo>foo\nbar</foo>"); 800 } 801 802 unittest { // issue #45 - no singular tags for XML 803 assert(!__traits(compiles, utCompile!("doctype html\nlink foo"))); 804 assert(!__traits(compiles, utCompile!("doctype html FOO\nlink foo"))); 805 assert(utCompile!("doctype xml\nlink foo") == `<?xml version="1.0" encoding="utf-8" ?><link>foo</link>`); 806 assert(utCompile!("doctype foo\nlink foo") == `<!DOCTYPE foo><link>foo</link>`); 807 } 808 809 unittest { // output empty tags as singular for XML output 810 assert(utCompile!("doctype html\nfoo") == `<!DOCTYPE html><foo></foo>`); 811 assert(utCompile!("doctype xml\nfoo") == `<?xml version="1.0" encoding="utf-8" ?><foo/>`); 812 }