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 import memutils.vector; 13 import memutils.scoped; 14 15 nothrow: 16 private template _dietFileData(string filename) 17 { 18 import diet.internal.string : stripUTF8BOM; 19 private static immutable contents = stripUTF8BOM(import(filename)); 20 } 21 22 /** Compiles a Diet template file that is available as a string import. 23 24 The resulting HTML is written to the output range given as a runtime 25 parameter. 26 27 Params: 28 filename = Name of the main Diet template file. 29 ALIASES = A list of variables to make available inside of the template, 30 as well as traits structs annotated with the `@dietTraits` 31 attribute. 32 33 Traits: 34 In addition to the default Diet traits, adding an enum field 35 `htmlOutputStyle` of type `HTMLOutputStyle` to a traits 36 struct can be used to control the style of the generated 37 HTML. 38 39 See_Also: `compileHTMLDietString`, `compileHTMLDietStrings` 40 41 Example: 42 --- 43 import std.array : appender; 44 45 auto text = appender!string; 46 text.compileHTMLDietFile!("invitation-email.diet", name, address); 47 sendMail(address, text.data); 48 --- 49 */ 50 template compileHTMLDietFile(string filename, ALIASES...) 51 { 52 alias compileHTMLDietFile = compileHTMLDietFileString!(filename, _dietFileData!filename.contents, ALIASES); 53 } 54 55 version(DietUseLive) 56 { 57 // out here, because the FileInfo struct isn't different based on the TRAITS. 58 private struct FileInfo 59 { 60 import std.datetime : SysTime; 61 SysTime modTime; 62 string[] dependencies; 63 string[] htmlstrings; 64 } 65 66 private string[] _getHTMLStrings(TRAITS...)(string filename, string expectedCode) @safe 67 { 68 import std.range : chain; 69 import std.file; 70 import std.array; 71 import std.algorithm; 72 import std.string : lineSplitter; 73 static FileInfo[string] cache; // one per set of TRAITS. 74 // assume files live in views/filename 75 if(auto fi = filename in cache) 76 { 77 // have to check all the files, not just the main one 78 bool newer = false; 79 foreach(dep; fi.dependencies) 80 { 81 auto curMod = chain("views/", dep).timeLastModified; 82 if(curMod > fi.modTime) 83 { 84 newer = true; 85 break; 86 } 87 } 88 // already checked, return the strings 89 if(!newer) 90 return fi.htmlstrings; 91 } 92 93 auto inputs = rtGetInputs(filename, "views/"); 94 95 // need to process the file again 96 auto doc = applyTraits!TRAITS(parseDiet!(translate!TRAITS)(inputs)); 97 auto code = getHTMLLiveMixin(doc); 98 // remove all the "#line" directives and compare the code. If it doesn't 99 // match, then the code changes might affect the output, and a recompile is 100 // necessary. 101 if(!code.lineSplitter.filter!(l => !l.startsWith("#line")).equal(expectedCode.lineSplitter.filter!(l => !l.startsWith("#line")))) 102 { 103 throw new DietParserException("Recompile necessary! view file " ~ filename ~ " or dependency has changed its code"); 104 } 105 106 auto curMod = chain("views/", inputs[0].name).timeLastModified; 107 foreach(x; inputs[1 .. $]) 108 { 109 // find latest time modified 110 curMod = max(curMod, chain("views/", x.name).timeLastModified); 111 } 112 auto newFI = FileInfo(curMod, inputs.map!(fi => fi.name).array, getHTMLRawTextOnly(doc, dietOutputRangeName, getHTMLOutputStyle!TRAITS).splitter('\0').array); 113 cache[filename] = newFI; 114 return newFI.htmlstrings; 115 } 116 } 117 118 119 // provide a place to cache compilation of a file. No reason to rebuild every 120 // time a file is used. 121 private template realCompileHTMLDietFileString(string filename, alias contents, TRAITS...) 122 { 123 private static immutable _diet_files = collectFiles!(filename, contents); 124 125 version (DietUseCache) 126 { 127 enum _diet_use_cache = true; 128 ulong computeTemplateHash() 129 { 130 ulong ret = 0; 131 void hash(string s) 132 { 133 foreach (char c; s) { 134 ret *= 9198984547192449281; 135 ret += c * 7576889555963512219; 136 } 137 } 138 foreach (ref f; _diet_files) { 139 hash(f.name[]); 140 hash(f.contents[]); 141 } 142 return ret; 143 } 144 145 enum _diet_hash = computeTemplateHash(); 146 enum _diet_cache_file_name = filename~"_cached_"~_diet_hash.to!string~".d"; 147 } 148 else 149 { 150 enum _diet_use_cache = false; 151 enum _diet_cache_file_name = "***INVALID***"; // not used anyway 152 } 153 154 155 156 static if (_diet_use_cache && is(typeof(import(_diet_cache_file_name)))) { 157 pragma(msg, "Using cached Diet HTML template "~filename~"..."); 158 enum _dietParser = import(_diet_cache_file_name); 159 } else { 160 pragma(msg, "Compiling Diet HTML template ..."); 161 pragma(msg, "Compiling Diet HTML template "~filename~"..."); 162 private Document _diet_nodes() { return applyTraits!TRAITS(parseDiet!(translate!TRAITS)(_diet_files)); } 163 version(DietUseLive) 164 { 165 enum _dietParser = getHTMLLiveMixin(_diet_nodes(), dietOutputRangeName); 166 } 167 else 168 { 169 enum _dietParser = getHTMLMixin(_diet_nodes(), dietOutputRangeName, getHTMLOutputStyle!TRAITS); 170 } 171 172 static if (_diet_use_cache) { 173 shared static this() 174 { 175 import std.file : exists, write; 176 if (!exists("views/"~_diet_cache_file_name)) 177 write("views/"~_diet_cache_file_name, _dietParser); 178 } 179 } 180 } 181 } 182 /** Compiles a Diet template given as a string, with support for includes and extensions. 183 184 This function behaves the same as `compileHTMLDietFile`, except that the 185 contents of the file are 186 187 The final HTML will be written to the given `_diet_output` output range. 188 189 Params: 190 filename = The name to associate with `contents` 191 contents = The contents of the Diet template 192 ALIASES = A list of variables to make available inside of the template, 193 as well as traits structs annotated with the `@dietTraits` 194 attribute. 195 196 See_Also: `compileHTMLDietFile`, `compileHTMLDietString`, `compileHTMLDietStrings` 197 */ 198 template compileHTMLDietFileString(string filename, alias contents, ALIASES...) 199 { 200 // This import should be REMOVED for 2.0.0, as it was unintentionally 201 // exposed for use inside the mixin. See issue #81 202 203 alias TRAITS = DietTraits!ALIASES; 204 205 alias _dietParser = realCompileHTMLDietFileString!(filename, contents, TRAITS)._dietParser; 206 207 version(DietUseLive) 208 { 209 // uses the correct range name and removes 'dst' from the scope 210 private void exec(R)(ref R _diet_output, string[] _diet_html_strings) 211 { 212 mixin(localAliasesMixin!(0, ALIASES)); 213 mixin(_dietParser); 214 } 215 216 /** 217 * See `.compileHTMLDietFileString` 218 * 219 * Params: 220 * dst = The output range to write the generated HTML to. 221 */ 222 void compileHTMLDietFileString(R)(ref R dst) 223 { 224 // first, load the data 225 exec(dst, _getHTMLStrings!TRAITS(filename, _dietParser)); 226 } 227 } 228 else 229 { 230 debug { 231 // uses the correct range name and removes 'dst' from the scope 232 private string exec(R)(ref R _diet_output) 233 { 234 mixin(localAliasesMixin!(0, ALIASES)); 235 mixin(_dietParser); 236 return _dietParser; 237 } 238 239 /** 240 * See `.compileHTMLDietFileString` 241 * 242 * Params: 243 * dst = The output range to write the generated HTML to. 244 */ 245 string compileHTMLDietFileString(R)(ref R dst) 246 { 247 return exec(dst); 248 } 249 250 } else { 251 252 // uses the correct range name and removes 'dst' from the scope 253 private void exec(R)(ref R _diet_output) 254 { 255 mixin(localAliasesMixin!(0, ALIASES)); 256 mixin(_dietParser); 257 } 258 259 /** 260 * See `.compileHTMLDietFileString` 261 * 262 * Params: 263 * dst = The output range to write the generated HTML to. 264 */ 265 void compileHTMLDietFileString(R)(ref R dst) 266 { 267 exec(dst); 268 } 269 } 270 } 271 } 272 273 274 /** Compiles a Diet template given as a string. 275 276 The final HTML will be written to the given `_diet_output` output range. 277 278 Params: 279 contents = The contents of the Diet template 280 ALIASES = A list of variables to make available inside of the template, 281 as well as traits structs annotated with the `@dietTraits` 282 attribute. 283 dst = The output range to write the generated HTML to. 284 285 See_Also: `compileHTMLDietFileString`, `compileHTMLDietStrings` 286 */ 287 template compileHTMLDietString(string contents, ALIASES...) 288 { 289 debug { 290 string compileHTMLDietString(R)(ref R dst) 291 { 292 return compileHTMLDietStrings!(Group!(contents, "diet-string"), ALIASES)(dst); 293 } 294 } else { 295 void compileHTMLDietString(R)(ref R dst) 296 { 297 compileHTMLDietStrings!(Group!(contents, "diet-string"), ALIASES)(dst); 298 } 299 } 300 } 301 302 303 /** Compiles a set of Diet template files. 304 305 The final HTML will be written to the given `_diet_output` output range. 306 307 Params: 308 FILES_GROUP = A `diet.input.Group` containing an alternating list of 309 file names and file contents. 310 ALIASES = A list of variables to make available inside of the template, 311 as well as traits structs annotated with the `@dietTraits` 312 attribute. 313 dst = The output range to write the generated HTML to. 314 315 See_Also: `compileHTMLDietString`, `compileHTMLDietStrings` 316 */ 317 template compileHTMLDietStrings(alias FILES_GROUP, ALIASES...) 318 { 319 alias TRAITS = DietTraits!ALIASES; 320 private static Document _diet_nodes() { return applyTraits!TRAITS(parseDiet!(translate!TRAITS)(filesFromGroup!FILES_GROUP)); } 321 322 debug { 323 // uses the correct range name and removes 'dst' from the scope 324 private string exec(R)(ref R _diet_output) 325 { 326 mixin(localAliasesMixin!(0, ALIASES)); 327 mixin(getHTMLMixin(_diet_nodes(), dietOutputRangeName, getHTMLOutputStyle!TRAITS)); 328 329 return getHTMLMixin(_diet_nodes(), dietOutputRangeName, getHTMLOutputStyle!TRAITS); 330 } 331 332 string compileHTMLDietStrings(R)(ref R dst) 333 { 334 return exec(dst); 335 } 336 337 } else { 338 339 // uses the correct range name and removes 'dst' from the scope 340 private void exec(R)(ref R _diet_output) 341 { 342 mixin(localAliasesMixin!(0, ALIASES)); 343 mixin(getHTMLMixin(_diet_nodes(), dietOutputRangeName, getHTMLOutputStyle!TRAITS)); 344 } 345 346 void compileHTMLDietStrings(R)(ref R dst) 347 { 348 exec(dst); 349 } 350 } 351 } 352 353 // encapsulate this externally for maintenance and for testing. 354 private enum _diet_imports = "import diet.internal.html : htmlEscape, htmlAttribEscape, filterHTMLAttribEscape;\n" 355 ~ "import diet.defs;\n"; 356 357 /** Returns a mixin string that generates HTML for the given DOM tree. 358 359 Params: 360 doc = The root nodes of the DOM tree. 361 range_name = Optional custom name to use for the output range, defaults 362 to `_diet_output`. 363 style = Output style to use. 364 365 Returns: 366 A string of D statements suitable to be mixed in inside of a function. 367 */ 368 string getHTMLMixin(in Document doc, string range_name = dietOutputRangeName, HTMLOutputStyle style = HTMLOutputStyle.compact) 369 { 370 CTX ctx; 371 ctx.pretty = style == HTMLOutputStyle.pretty; 372 ctx.rangeName[] = range_name; 373 Vector!char ret = _diet_imports; 374 foreach (i, n; doc.nodes[]) { 375 //auto scoped = ScopedPool(); 376 ret ~= ctx.getHTMLMixin(n, false); 377 } 378 ret ~= ctx.flushRawText(); 379 return ret[].copy(); 380 } 381 382 /** This is like getHTMLMixin, but returns only the NON-code portions of the diet 383 template. The usage is for the DietLiveMode, which can update the HTML 384 portions of the diet template at runtime without requiring a recompile. 385 386 387 Params: 388 doc = The root nodes of the DOM tree. 389 range_name = Optional custom name to use for the output range, defaults 390 to `_diet_output`. 391 style = Output style to use. 392 393 Returns: 394 The return value is a concatenated string with each string of raw 395 HTML text separated by a null character. To extract the strings to send 396 into the live renderer, split the string based on a null character. 397 */ 398 string getHTMLRawTextOnly(in Document doc, string range_name = dietOutputRangeName, HTMLOutputStyle style = HTMLOutputStyle.compact) @safe 399 { 400 CTX ctx; 401 ctx.pretty = style == HTMLOutputStyle.pretty; 402 ctx.mode = CTX.OutputMode.rawTextOnly; 403 ctx.rangeName[] = range_name; 404 // definitely don't want the top imports here 405 Vector!char ret; 406 foreach(i, n; doc.nodes[]) 407 ret ~= ctx.getHTMLMixin(n, false); 408 ret ~= ctx.flushRawText(); 409 return ret[].copy(); 410 } 411 412 /** 413 This returns a "live" version of the mixin. The live version generates the code skeleton and then accepts a list of HTML strings that go between the code to output. This way, you can read the diet template at runtime, and if any non-code changes are made, you can avoid recompilation. 414 */ 415 string getHTMLLiveMixin(in Document doc, string range_name = dietOutputRangeName, string htmlPiecesMapName = "_diet_html_strings") @safe 416 { 417 CTX ctx; 418 ctx.mode = CTX.OutputMode.live; 419 ctx.rangeName[] = range_name; 420 ctx.piecesMapName[] = htmlPiecesMapName; 421 Vector!char ret = _diet_imports; 422 foreach(i, n; doc.nodes[]) 423 ret ~= ctx.getHTMLMixin(n, false); 424 // output a final html in case there were any items at the end 425 ret ~= ctx.statement!""(Location(Array!char("_livediet"), 0)); 426 return ret[].copy(); 427 } 428 /* 429 unittest { 430 import diet.parser; 431 void test(string src)(string expected) { 432 import std.array : appender, array; 433 import std.algorithm : splitter; 434 static const n = parseDiet(src); 435 { 436 auto _diet_output = appender!string(); 437 mixin(getHTMLMixin(n)); 438 assert(_diet_output.data == expected, _diet_output.data); 439 } 440 441 // test live mode. 442 { 443 // generate the strings 444 auto _diet_output = appender!string(); 445 auto _diet_html_strings = getHTMLRawTextOnly(n).splitter('\0').array; 446 mixin(getHTMLLiveMixin(n)); 447 assert(_diet_output.data == expected, _diet_output.data); 448 } 449 } 450 451 test!"doctype html\nfoo(test=true)"("<!DOCTYPE html><foo test></foo>"); 452 test!"doctype html X\nfoo(test=true)"("<!DOCTYPE html X><foo test=\"test\"></foo>"); 453 test!"doctype X\nfoo(test=true)"("<!DOCTYPE X><foo test=\"test\"/>"); 454 test!"foo(test=2+3)"("<foo test=\"5\"></foo>"); 455 test!"foo(test='#{2+3}')"("<foo test=\"5\"></foo>"); 456 test!"foo #{2+3}"("<foo>5</foo>"); 457 test!"foo= 2+3"("<foo>5</foo>"); 458 test!"- int x = 3;\nfoo=x"("<foo>3</foo>"); 459 test!"- foreach (i; 0 .. 2)\n\tfoo"("<foo></foo><foo></foo>"); 460 test!"div(*ngFor=\"\\#item of list\")"( 461 "<div *ngFor=\"#item of list\"></div>" 462 ); 463 test!".foo"("<div class=\"foo\"></div>"); 464 test!"#foo"("<div id=\"foo\"></div>"); 465 } 466 467 // test live mode works with HTML changes 468 unittest { 469 void test(string before, string after)(string expectedBefore, string expectedAfter) { 470 import std.array : appender, array; 471 import std.algorithm : splitter, equal, filter, startsWith; 472 import std.string : lineSplitter; 473 static const bef = parseDiet(before); 474 static const aft = parseDiet(after); 475 476 enum _codeBefore = getHTMLLiveMixin(bef); 477 enum _codeAfter = getHTMLLiveMixin(aft); 478 479 // ensure both items produce the same code 480 assert( _codeBefore.lineSplitter.filter!(l => !l.startsWith("#line")) 481 .equal(_codeAfter.lineSplitter.filter!(l => !l.startsWith("#line")))); 482 483 484 // test both sets of code with both strings 485 auto _diet_html_strings = getHTMLRawTextOnly(bef).splitter('\0').array; 486 { 487 auto _diet_output = appender!string(); 488 mixin(_codeBefore); 489 assert(_diet_output.data == expectedBefore, _diet_output.data); 490 } 491 { 492 auto _diet_output = appender!string(); 493 mixin(_codeAfter); 494 assert(_diet_output.data == expectedBefore, _diet_output.data); 495 } 496 497 // second set of strings 498 _diet_html_strings = getHTMLRawTextOnly(aft).splitter('\0').array; 499 { 500 auto _diet_output = appender!string(); 501 mixin(_codeBefore); 502 assert(_diet_output.data == expectedAfter, _diet_output.data); 503 } 504 { 505 auto _diet_output = appender!string(); 506 mixin(_codeAfter); 507 assert(_diet_output.data == expectedAfter, _diet_output.data); 508 } 509 } 510 511 // test renaming things 512 test!("foo(test=2+3)", 513 "foobar(testbaz=2+3)") 514 ("<foo test=\"5\"></foo>", 515 "<foobar testbaz=\"5\"></foobar>"); 516 517 // test injecting extra html 518 test!("- if(true)\n - auto x = 5;\n foo #{x}", 519 "- if(true)\n a(href=\"injected!\") injected html!\n - auto x = 5;\n foo #{x}", 520 )("<foo>5</foo>", "<a href=\"injected!\">injected html!</a><foo>5</foo>"); 521 } 522 */ 523 524 /** Determines how the generated HTML gets styled. 525 526 To use this, put an enum field named `htmlOutputStyle` into a diet traits 527 struct and pass that to the render function. 528 529 The default output style is `compact`. 530 */ 531 enum HTMLOutputStyle { 532 compact, /// Outputs no extraneous whitespace (including line breaks) around HTML tags 533 pretty, /// Inserts line breaks and indents lines according to their nesting level in the HTML structure 534 } 535 536 /// 537 /* 538 unittest { 539 @dietTraits 540 struct Traits { 541 enum htmlOutputStyle = HTMLOutputStyle.pretty; 542 } 543 544 import std.array : appender; 545 auto dst = appender!string(); 546 dst.compileHTMLDietString!("html\n\tbody\n\t\tp Hello", Traits); 547 import std.conv : to; 548 assert(dst.data == "<html>\n\t<body>\n\t\t<p>Hello</p>\n\t</body>\n</html>", [dst.data].to!string); 549 } 550 */ 551 private @property template getHTMLOutputStyle(TRAITS...) 552 { 553 static if (TRAITS.length) { 554 static if (is(typeof(TRAITS[0].htmlOutputStyle))) 555 enum getHTMLOutputStyle = TRAITS[0].htmlOutputStyle; 556 else enum getHTMLOutputStyle = getHTMLOutputStyle!(TRAITS[1 .. $]); 557 } else enum getHTMLOutputStyle = HTMLOutputStyle.compact; 558 } 559 560 private string getHTMLMixin(ref CTX ctx, in Node node, bool in_pre) @safe 561 { 562 switch (node.name[]) { 563 default: return ctx.getElementMixin(node, in_pre); 564 case "doctype": return ctx.getDoctypeMixin(node); 565 case Node.SpecialName.code: return ctx.getCodeMixin(node, in_pre); 566 case Node.SpecialName.comment: return ctx.getCommentMixin(node); 567 case Node.SpecialName.hidden: return null; 568 case Node.SpecialName.text: 569 Vector!char ret; 570 foreach (i, c; node.contents[]) 571 ret ~= ctx.getNodeContentsMixin(c, in_pre); 572 if (in_pre) ctx.plainNewLine(); 573 else ctx.prettyNewLine(); 574 return ret[].copy(); 575 } 576 } 577 578 private string getElementMixin(ref CTX ctx, in Node node, bool in_pre) @safe 579 { 580 581 if (node.name[] == "pre") in_pre = true; 582 583 bool need_newline = ctx.needPrettyNewline(node.contents[]); 584 585 bool is_singular_tag; 586 // determine if we need a closing tag or have a singular tag 587 if (ctx.isHTML) { 588 switch (node.name[]) { 589 default: break; 590 case "area", "base", "basefont", "br", "col", "embed", "frame", "hr", "img", "input", 591 "keygen", "link", "meta", "param", "source", "track", "wbr": 592 is_singular_tag = true; 593 need_newline = true; 594 break; 595 } 596 } else if (!node.hasNonWhitespaceContent) is_singular_tag = true; 597 598 // write tag name 599 string tagname = node.name[].length ? node.name[] : "div"; 600 Vector!char ret; 601 if (node.attribs & NodeAttribs.fitOutside || in_pre) 602 ctx.inhibitNewLine(); 603 else if (need_newline) 604 ctx.prettyNewLine(); 605 { 606 auto open_tag = Vector!char(); 607 open_tag ~= "<"; 608 open_tag ~= tagname; 609 ret ~= ctx.rawText(node.loc, open_tag[]); 610 611 } 612 613 bool had_class = false; 614 615 // write attributes 616 foreach (ai, att_; node.attributes[]) { 617 auto att = att_.dup; // this sucks... 618 619 // merge multiple class attributes into one 620 if (att.name[] == "class") { 621 if (had_class) continue; 622 had_class = true; 623 foreach (ca; node.attributes[ai+1 .. $]) { 624 if (ca.name[] != "class") continue; 625 if (!ca.contents.length || (ca.isText && !ca.expectText.length)) continue; 626 att.addText(" "); 627 att.addContents(ca.contents[]); 628 } 629 } 630 631 bool is_expr = att.contents.length == 1 && att.contents[0].kind == AttributeContent.Kind.interpolation; 632 633 if (is_expr) { 634 auto expr = att.contents[0].value; 635 636 if (expr[] == "true") { 637 if (ctx.isHTML5) ret ~= ctx.rawText(node.loc, format!" %s"(att.name[])); 638 else ret ~= ctx.rawText(node.loc, format!" %s=\"%s\""(att.name[], att.name[])); 639 continue; 640 } 641 642 // note the attribute name is HTML, and not code, so live mode 643 // should reprocess that and use the string table. 644 ret ~= ctx.statement!("static if (is(typeof(() { return %s; }()) == bool) ){")(node.loc, expr[]); 645 ret ~= ctx.statementCont!"if (%s)"(node.loc, expr[]); 646 if (ctx.isHTML5) 647 ret ~= ctx.rawText(node.loc, format!" %s"(att.name[])); 648 else 649 ret ~= ctx.rawText(node.loc, format!" %s=\"%s\""(att.name[], att.name[])); 650 651 ret ~= ctx.statement!"} else static if (is(typeof(%s) : const(char)[])) {{"(node.loc, expr[]); 652 ret ~= ctx.statementCont!" auto _diet_val = %s;"(node.loc, expr[]); 653 ret ~= ctx.statementCont!" if (_diet_val !is null) {"(node.loc); 654 ret ~= ctx.rawText!" %s=\""(node.loc, att.name[]); 655 ret ~= ctx.statement!" %s.filterHTMLAttribEscape(_diet_val);"(node.loc, ctx.rangeName[]); 656 ret ~= ctx.rawText(node.loc, "\""); 657 ret ~= ctx.statement!" }"(node.loc); 658 ret ~= ctx.statementCont!"}} else {"(node.loc); 659 } 660 661 ret ~= ctx.rawText(node.loc, format!" %s=\""(att.name[])); 662 663 foreach (i, v; att.contents[]) { 664 final switch (v.kind) with (AttributeContent.Kind) { 665 case text: 666 ret ~= ctx.rawText(node.loc, htmlAttribEscape(v.value[])); 667 break; 668 case interpolation, rawInterpolation: 669 ret ~= ctx.statement!"%s.htmlAttribEscape(%s);"(node.loc, ctx.rangeName[], v.value[]); 670 break; 671 } 672 } 673 674 ret ~= ctx.rawText(node.loc, "\""); 675 676 if (is_expr) ret ~= ctx.statement!"}"(node.loc); 677 } 678 679 // determine if we need a closing tag or have a singular tag 680 if (is_singular_tag) { 681 enforcep(!node.hasNonWhitespaceContent, format!"Singular HTML element '%s' may not have contents."(node.name), node.loc); 682 ret ~= ctx.rawText(node.loc, "/>"); 683 if (need_newline && !(node.attribs & NodeAttribs.fitOutside)) 684 ctx.prettyNewLine(); 685 return ret[].copy(); 686 } 687 688 ret ~= ctx.rawText(node.loc, ">"); 689 690 // write contents 691 if (need_newline) { 692 ctx.depth++; 693 if (!(node.attribs & NodeAttribs.fitInside) && !in_pre) 694 ctx.prettyNewLine(); 695 } 696 697 foreach (i, c; node.contents[]) 698 ret ~= ctx.getNodeContentsMixin(c, in_pre); 699 700 if (need_newline && !in_pre) { 701 ctx.depth--; 702 if (!(node.attribs & NodeAttribs.fitInside) && !in_pre) 703 ctx.prettyNewLine(); 704 } else ctx.inhibitNewLine(); 705 706 // write end tag 707 ret ~= ctx.rawText(node.loc, format!"</%s>"(tagname)); 708 709 if ((node.attribs & NodeAttribs.fitOutside) || in_pre) 710 ctx.inhibitNewLine(); 711 else if (need_newline) 712 ctx.prettyNewLine(); 713 return ret[].copy(); 714 } 715 716 private string getNodeContentsMixin(ref CTX ctx, in NodeContent* c, bool in_pre) @safe 717 { 718 final switch (c.kind) with (NodeContent.Kind) { 719 case node: 720 return getHTMLMixin(ctx, c.node, in_pre); 721 case text: 722 return ctx.rawText(c.loc, c.value[]); 723 case interpolation: 724 return ctx.textStatement!"%s.htmlEscape(%s);"(c.loc, ctx.rangeName[], c.value[]); 725 case rawInterpolation: 726 return ctx.textStatement!"put(%s, format!\"%s\"(%s));"(c.loc, ctx.rangeName[], "%s", c.value[]); 727 } 728 } 729 730 private string getDoctypeMixin(ref CTX ctx, in Node node) @safe 731 { 732 import diet.internal.string; 733 734 if (node.name[] == "!!!") 735 ctx.statement!"pragma(msg, \"Use of '!!!' is deprecated. Use 'doctype' instead.\");"(node.loc); 736 737 enforcep(node.contents.length == 1 && node.contents[0].kind == NodeContent.Kind.text, 738 "Only doctype specifiers allowed as content for doctype nodes.", node.loc); 739 740 auto args = ctstrip(node.contents[0].value[]); 741 742 ctx.isHTML5 = false; 743 744 auto doctype_str = Vector!char("!DOCTYPE html"); 745 switch (args) { 746 case "5": 747 case "": 748 case "html": 749 ctx.isHTML5 = true; 750 break; 751 case "xml": 752 doctype_str[] = `?xml version="1.0" encoding="utf-8" ?`; 753 ctx.isHTML = false; 754 break; 755 case "transitional": 756 doctype_str[] = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ` 757 ~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"`; 758 break; 759 case "strict": 760 doctype_str[] = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ` 761 ~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"`; 762 break; 763 case "frameset": 764 doctype_str[] = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" ` 765 ~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"`; 766 break; 767 case "1.1": 768 doctype_str[] = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" ` 769 ~ `"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"`; 770 break; 771 case "basic": 772 doctype_str[] = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" ` 773 ~ `"http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd"`; 774 break; 775 case "mobile": 776 doctype_str[] = `!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" ` 777 ~ `"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd"`; 778 break; 779 default: 780 doctype_str[] = "!DOCTYPE "; 781 doctype_str ~= args; 782 ctx.isHTML = args.length >= 5 && args[0 .. 5] == "html "; 783 break; 784 } 785 786 return ctx.rawText(node.loc, format!"<%s>"(doctype_str[])); 787 } 788 789 private string getCodeMixin(ref CTX ctx, const ref Node node, bool in_pre) @safe 790 { 791 enforcep(node.attributes.length == 0, "Code lines may not have attributes.", node.loc); 792 enforcep(node.attribs == NodeAttribs.none, "Code lines may not specify translation or text block suffixes.", node.loc); 793 if (node.contents.length == 0) return null; 794 795 Vector!char ret; 796 bool have_contents = node.contents.length > 1; 797 foreach (i, c; node.contents[]) { 798 if (i == 0 && c.kind == NodeContent.Kind.text) { 799 if(have_contents) { 800 ret ~= ctx.statement!"%s\n{"(node.loc, c.value[]); 801 } 802 else { 803 ret ~= ctx.statement!"%s"(node.loc, c.value[]); 804 } 805 } else { 806 assert(c.kind == NodeContent.Kind.node); 807 ret ~= ctx.getHTMLMixin(c.node, in_pre); 808 } 809 } 810 if(have_contents) 811 ret ~= ctx.statement!"}"(node.loc); 812 return ret[].copy(); 813 } 814 815 private string getCommentMixin(ref CTX ctx, const ref Node node) @safe 816 { 817 Vector!char ret = ctx.rawText(node.loc, "<!--"); 818 ctx.depth++; 819 foreach (i, c; node.contents[]) 820 ret ~= ctx.getNodeContentsMixin(c, false); 821 ctx.depth--; 822 ret ~= ctx.rawText(node.loc, "-->"); 823 return ret[].copy(); 824 } 825 826 private struct CTX { 827 @safe: 828 nothrow: 829 830 enum NewlineState { 831 none, 832 plain, 833 pretty, 834 inhibit 835 } 836 837 bool isHTML5, isHTML = true; 838 bool pretty; 839 enum OutputMode { 840 normal, 841 live, 842 rawTextOnly 843 } 844 OutputMode mode; 845 int depth = 0; 846 Vector!char rangeName; 847 Vector!char piecesMapName; 848 Vector!char piecesMapOutputStr; 849 size_t currentStatement; 850 bool inRawText = false; 851 NewlineState newlineState = NewlineState.none; 852 bool anyText; 853 int suppressLive; 854 855 // trying to cut down on compile time memory, this should help by not formatting very similar lines. 856 @safe const(char)[] getHTMLPiece() 857 { 858 if(!piecesMapOutputStr.length) 859 { 860 piecesMapOutputStr[] = format!"put(%s, %s[0x00000000]);\n"(rangeName[], piecesMapName[]); 861 } 862 863 // The last characters of the string are "[0x00000000]);\n". We can 864 // replace the 0s with hex characters representing the bytes of the 865 // index. Since we are always increasing the index, there's no need to 866 // keep replacing 0s once the index is out of data 867 size_t idx = piecesMapOutputStr.length - 5; 868 size_t curIdx = currentStatement; 869 while(curIdx) 870 { 871 immutable n = curIdx & 0x0f; 872 if(n > 9) 873 piecesMapOutputStr[idx] = 'a' + n - 10; 874 else 875 piecesMapOutputStr[idx] = '0' + n; 876 --idx; 877 curIdx >>= 4; 878 } 879 return piecesMapOutputStr[].copy(); 880 } 881 882 // same as statement, but with guaranteed no raw text between the last 883 // statement and it. 884 string statementCont(string fmt, ARGS...)(in Location loc, ARGS args) 885 { 886 with(OutputMode) final switch(mode) 887 { 888 case live: 889 case normal: 890 auto ret = format!("#line %s \"%s\"\n"~fmt~"\n")(loc.line+1, loc.file[], args); 891 //assert(ret.length == 0, ret); 892 return ret; 893 case rawTextOnly: 894 // do not output anything here, no raw text is possible 895 return ""; 896 } 897 } 898 899 string statement(string fmt, ARGS...)(in Location loc, ARGS args) 900 { 901 Vector!char ret = flushRawText(); 902 903 // Notes on live mode here. This is about to output a statement in D 904 // code from the diet template. In live mode, this means we need to 905 // output any HTML text before outputting the D line. Because we don't 906 // know if someone might add HTML output where there currently isn't 907 // any, we always output another string from the table even though it 908 // might be empty. 909 // 910 // There are 2 cases where the code avoids doing this. The first is 911 // between an `if` an `else` statement. D does not allow this in the 912 // grammar (and it wouldn't make sense anyway). It is technically 913 // possible to add HTML in the diet file between these two, but it will 914 // not compile anyway. 915 // 916 // The second case is after a return statement. This one is tricky 917 // because we need to suppress it on the closing brace. In practice, 918 // the return statement will not have an HTML or any other statement 919 // printout (or it will fail to compile), so a flag is stored that 920 // indicates the next statement should suppress "possible" HTML output. 921 // 922 // At this time, the code just does a simple match to the keywords 923 // `return` or `else` as the first word of the line. This should be 924 // good enough, but may not be sufficient in all cases. 925 auto nextLine = format!(fmt~"\n")(args); 926 //assert(nextLine.length == 0, nextLine[]); 927 Vector!string firstNonSpace = ctsplit(nextLine[], ' '); 928 immutable isReturn = !firstNonSpace.empty && (firstNonSpace[][0] == "return" || firstNonSpace[][0] == "return;"); 929 immutable isElse = !firstNonSpace.empty && firstNonSpace[][0] == "else"; 930 with(OutputMode) final switch(mode) 931 { 932 case rawTextOnly: 933 // each statement is represented by a null character as a placeholder. 934 if(!isElse && !suppressLive) 935 ret ~= '\0'; 936 break; 937 case live: 938 // output all non-statement data until this point. 939 if(!isElse && !suppressLive) 940 { 941 ret ~= getHTMLPiece(); 942 } 943 // fall through 944 goto case normal; 945 case normal: 946 ret ~= format!("#line %s \"%s\"\n")(loc.line+1, loc.file[]); 947 ret ~= nextLine[]; 948 break; 949 } 950 if(!isElse) 951 { 952 if(suppressLive) 953 --suppressLive; 954 else 955 ++currentStatement; 956 } 957 if(isReturn) 958 { 959 // need to skip next HTML output 960 suppressLive = 1; 961 } 962 963 return ret[].copy(); 964 } 965 966 string textStatement(string fmt, ARGS...)(in Location loc, ARGS args) 967 { 968 Vector!char ret; 969 if (newlineState != NewlineState.none) ret ~= rawText(loc, null); 970 ret ~= statement!fmt(loc, args); 971 return ret[].copy(); 972 } 973 974 string rawText(ARGS...)(in Location loc, string text) 975 { 976 Vector!char ret; 977 if (!this.inRawText) { 978 with(OutputMode) final switch(mode) 979 { 980 case rawTextOnly: 981 case live: 982 // do nothing 983 break; 984 case normal: 985 ret ~= format!"put(%s, \""(this.rangeName[]); 986 break; 987 } 988 this.inRawText = true; 989 } 990 ret ~= outputPendingNewline(); 991 with(OutputMode) final switch(mode) 992 { 993 case live: 994 // do nothing 995 break; 996 case normal: 997 ret ~= dstringEscape(text); 998 break; 999 case rawTextOnly: 1000 // this is the raw string being output to the browser, indexed in 1001 // an array. Since it's not being mixed in, we do not need to 1002 // escape. 1003 ret ~= text; 1004 break; 1005 } 1006 anyText = true; 1007 return ret[].copy(); 1008 } 1009 1010 pure string flushRawText() 1011 { 1012 if (this.inRawText) { 1013 this.inRawText = false; 1014 if(mode == OutputMode.normal) 1015 return "\");\n"; 1016 } 1017 return null; 1018 } 1019 1020 void plainNewLine() { if (newlineState != NewlineState.inhibit) newlineState = NewlineState.plain; } 1021 void prettyNewLine() { if (newlineState != NewlineState.inhibit) newlineState = NewlineState.pretty; } 1022 void inhibitNewLine() { newlineState = NewlineState.inhibit; } 1023 1024 bool needPrettyNewline(in NodeContent*[] contents) { 1025 foreach(c; contents[]) { 1026 if (c.kind == NodeContent.Kind.node) { 1027 return pretty; 1028 } 1029 } 1030 return false; 1031 } 1032 1033 private string outputPendingNewline() 1034 { 1035 auto st = newlineState; 1036 newlineState = NewlineState.none; 1037 1038 if(mode == OutputMode.live) 1039 return null; 1040 1041 final switch (st) { 1042 case NewlineState.none: return null; 1043 case NewlineState.inhibit:return null; 1044 case NewlineState.plain: return "\n"; 1045 case NewlineState.pretty: 1046 Vector!char tabs; 1047 tabs ~= "\n"; 1048 for (int i; i < depth; i++) { 1049 tabs ~= "\t"; 1050 } 1051 return anyText ? tabs[] : null; 1052 } 1053 } 1054 } 1055 /* 1056 unittest { 1057 static string compile(string diet, ALIASES...)() { 1058 import std.array : appender; 1059 import std.string : strip; 1060 auto dst = appender!string; 1061 compileHTMLDietString!(diet, ALIASES)(dst); 1062 return strip(cast(string)(dst.data)); 1063 } 1064 1065 assert(compile!(`!!! 5`) == `<!DOCTYPE html>`, `_`~compile!(`!!! 5`)~`_`); 1066 assert(compile!(`!!! html`) == `<!DOCTYPE html>`); 1067 assert(compile!(`doctype html`) == `<!DOCTYPE html>`); 1068 assert(compile!(`doctype xml`) == `<?xml version="1.0" encoding="utf-8" ?>`); 1069 assert(compile!(`p= 5`) == `<p>5</p>`); 1070 assert(compile!(`script= 5`) == `<script>5</script>`); 1071 assert(compile!(`style= 5`) == `<style>5</style>`); 1072 //assert(compile!(`include #{"p Hello"}`) == "<p>Hello</p>"); 1073 assert(compile!(`<p>Hello</p>`) == "<p>Hello</p>"); 1074 assert(compile!(`// I show up`) == "<!-- I show up-->"); 1075 assert(compile!(`//-I don't show up`) == ""); 1076 assert(compile!(`//- I don't show up`) == ""); 1077 1078 // issue 372 1079 assert(compile!(`div(class="")`) == `<div></div>`); 1080 assert(compile!(`div.foo(class="")`) == `<div class="foo"></div>`); 1081 assert(compile!(`div.foo(class="bar")`) == `<div class="foo bar"></div>`); 1082 assert(compile!(`div(class="foo")`) == `<div class="foo"></div>`); 1083 assert(compile!(`div#foo(class='')`) == `<div id="foo"></div>`); 1084 1085 // issue 19 1086 assert(compile!(`input(checked=false)`) == `<input/>`); 1087 assert(compile!(`input(checked=true)`) == `<input checked="checked"/>`); 1088 assert(compile!(`input(checked=(true && false))`) == `<input/>`); 1089 assert(compile!(`input(checked=(true || false))`) == `<input checked="checked"/>`); 1090 1091 assert(compile!(q{- import std.algorithm.searching : any; 1092 input(checked=([false].any))}) == `<input/>`); 1093 assert(compile!(q{- import std.algorithm.searching : any; 1094 input(checked=([true].any))}) == `<input checked="checked"/>`); 1095 1096 assert(compile!(q{- bool foo() { return false; } 1097 input(checked=foo)}) == `<input/>`); 1098 assert(compile!(q{- bool foo() { return true; } 1099 input(checked=foo)}) == `<input checked="checked"/>`); 1100 1101 // issue 520 1102 assert(compile!("- auto cond = true;\ndiv(someattr=cond ? \"foo\" : null)") == "<div someattr=\"foo\"></div>"); 1103 assert(compile!("- auto cond = false;\ndiv(someattr=cond ? \"foo\" : null)") == "<div></div>"); 1104 assert(compile!("- auto cond = false;\ndiv(someattr=cond ? true : false)") == "<div></div>"); 1105 assert(compile!("- auto cond = true;\ndiv(someattr=cond ? true : false)") == "<div someattr=\"someattr\"></div>"); 1106 assert(compile!("doctype html\n- auto cond = true;\ndiv(someattr=cond ? true : false)") 1107 == "<!DOCTYPE html><div someattr></div>"); 1108 assert(compile!("doctype html\n- auto cond = false;\ndiv(someattr=cond ? true : false)") 1109 == "<!DOCTYPE html><div></div>"); 1110 1111 // issue 510 1112 assert(compile!("pre.test\n\tfoo") == "<pre class=\"test\"><foo></foo></pre>"); 1113 assert(compile!("pre.test.\n\tfoo") == "<pre class=\"test\">foo</pre>"); 1114 assert(compile!("pre.test. foo") == "<pre class=\"test\"></pre>"); 1115 assert(compile!("pre().\n\tfoo") == "<pre>foo</pre>"); 1116 assert(compile!("pre#foo.test(data-img=\"sth\",class=\"meh\"). something\n\tmeh") == 1117 "<pre id=\"foo\" class=\"test meh\" data-img=\"sth\">meh</pre>"); 1118 1119 assert(compile!("input(autofocus)").length); 1120 1121 assert(compile!("- auto s = \"\";\ninput(type=\"text\",value=\"&\\\"#{s}\")") 1122 == `<input type="text" value="&""/>`); 1123 assert(compile!("- auto param = \"t=1&u=1\";\na(href=\"/?#{param}&v=1\") foo") 1124 == `<a href="/?t=1&u=1&v=1">foo</a>`); 1125 1126 // issue #1021 1127 assert(compile!("html( lang=\"en\" )") 1128 == "<html lang=\"en\"></html>"); 1129 1130 // issue #1033 1131 assert(compile!("input(placeholder=')')") 1132 == "<input placeholder=\")\"/>"); 1133 assert(compile!("input(placeholder='(')") 1134 == "<input placeholder=\"(\"/>"); 1135 } 1136 1137 unittest { // blocks and extensions 1138 static string compilePair(string extension, string base, ALIASES...)() { 1139 import std.array : appender; 1140 import std.string : strip; 1141 auto dst = appender!string; 1142 compileHTMLDietStrings!(Group!(extension, "extension.dt", base, "base.dt"), ALIASES)(dst); 1143 return strip(dst.data); 1144 } 1145 1146 assert(compilePair!("extends base\nblock test\n\tp Hello", "body\n\tblock test") 1147 == "<body><p>Hello</p></body>"); 1148 assert(compilePair!("extends base\nblock test\n\tp Hello", "body\n\tblock test\n\t\tp Default") 1149 == "<body><p>Hello</p></body>"); 1150 assert(compilePair!("extends base", "body\n\tblock test\n\t\tp Default") 1151 == "<body><p>Default</p></body>"); 1152 assert(compilePair!("extends base\nprepend test\n\tp Hello", "body\n\tblock test\n\t\tp Default") 1153 == "<body><p>Hello</p><p>Default</p></body>"); 1154 } 1155 */ 1156 /*@nogc*/ @safe unittest { // NOTE: formattedWrite is not @nogc 1157 static struct R { 1158 @nogc @safe nothrow: 1159 void put(in char[]) {} 1160 void put(char) {} 1161 void put(dchar) {} 1162 } 1163 1164 R r; 1165 r.compileHTMLDietString!( 1166 `doctype html 1167 html 1168 - foreach (i; 0 .. 10) 1169 title= i 1170 title t #{12} !{13} 1171 `); 1172 } 1173 /* 1174 unittest { // issue 4 - nested text in code 1175 static string compile(string diet, ALIASES...)() { 1176 import std.array : appender; 1177 import std.string : strip; 1178 auto dst = appender!string; 1179 compileHTMLDietString!(diet, ALIASES)(dst); 1180 return strip(cast(string)(dst.data)); 1181 } 1182 assert(compile!"- if (true)\n\t| int bar;" == "int bar;"); 1183 } 1184 1185 unittest { // class instance variables 1186 import std.array : appender; 1187 import std.string : strip; 1188 1189 static class C { 1190 int x = 42; 1191 1192 string test() 1193 { 1194 auto dst = appender!string; 1195 dst.compileHTMLDietString!("| #{x}", x); 1196 return dst.data; 1197 } 1198 } 1199 1200 auto c = new C; 1201 assert(c.test().strip == "42"); 1202 } 1203 1204 unittest { // raw interpolation for non-copyable range 1205 struct R { @disable this(this); void put(dchar) {} void put(in char[]) {} } 1206 R r; 1207 r.compileHTMLDietString!("a !{2}"); 1208 } 1209 1210 unittest { 1211 assert(utCompile!(".foo(class=true?\"bar\":\"baz\")") == "<div class=\"foo bar\"></div>"); 1212 } 1213 version (unittest) { 1214 private string utCompile(string diet, ALIASES...)() { 1215 import std.array : appender; 1216 import std.string : strip; 1217 auto dst = appender!string; 1218 compileHTMLDietString!(diet, ALIASES)(dst); 1219 return strip(cast(string)(dst.data)); 1220 } 1221 } 1222 1223 unittest { // blank lines in text blocks 1224 assert(utCompile!("pre.\n\tfoo\n\n\tbar") == "<pre>foo\n\nbar</pre>"); 1225 } 1226 1227 unittest { // singular tags should be each on their own line 1228 enum src = "p foo\nlink\nlink"; 1229 enum dst = "<p>foo</p>\n<link/>\n<link/>"; 1230 @dietTraits struct T { enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.pretty; } 1231 assert(utCompile!(src, T) == dst); 1232 } 1233 1234 unittest { // ignore whitespace content for singular tags 1235 assert(utCompile!("link ") == "<link/>"); 1236 assert(utCompile!("link \n\t ") == "<link/>"); 1237 } 1238 1239 unittest { 1240 @dietTraits struct T { enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.pretty; } 1241 import std.conv : to; 1242 // no extraneous newlines before text lines 1243 assert(utCompile!("foo\n\tbar text1\n\t| text2", T) == "<foo>\n\t<bar>text1</bar>text2\n</foo>"); 1244 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>"); 1245 // fit inside/outside + pretty printing - issue #27 1246 assert(utCompile!("| foo\na<> bar\n| baz", T) == "foo<a>bar</a>baz"); 1247 assert(utCompile!("foo\n\ta< bar", T) == "<foo>\n\t<a>bar</a>\n</foo>"); 1248 assert(utCompile!("foo\n\ta> bar", T) == "<foo><a>bar</a></foo>"); 1249 assert(utCompile!("a\nfoo<\n\ta bar\nb", T) == "<a></a>\n<foo><a>bar</a></foo>\n<b></b>"); 1250 assert(utCompile!("a\nfoo>\n\ta bar\nb", T) == "<a></a><foo>\n\t<a>bar</a>\n</foo><b></b>"); 1251 // hard newlines in pre blocks 1252 assert(utCompile!("pre\n\t| foo\n\t| bar", T) == "<pre>foo\nbar</pre>"); 1253 assert(utCompile!("pre\n\tcode\n\t\t| foo\n\t\t| bar", T) == "<pre><code>foo\nbar</code></pre>"); 1254 // always hard breaks for text blocks 1255 assert(utCompile!("pre.\n\tfoo\n\tbar", T) == "<pre>foo\nbar</pre>"); 1256 assert(utCompile!("foo.\n\tfoo\n\tbar", T) == "<foo>foo\nbar</foo>"); 1257 } 1258 1259 unittest { // issue #45 - no singular tags for XML 1260 assert(!__traits(compiles, utCompile!("doctype html\nlink foo"))); 1261 assert(!__traits(compiles, utCompile!("doctype html FOO\nlink foo"))); 1262 assert(utCompile!("doctype xml\nlink foo") == `<?xml version="1.0" encoding="utf-8" ?><link>foo</link>`); 1263 assert(utCompile!("doctype foo\nlink foo") == `<!DOCTYPE foo><link>foo</link>`); 1264 } 1265 1266 unittest { // output empty tags as singular for XML output 1267 assert(utCompile!("doctype html\nfoo") == `<!DOCTYPE html><foo></foo>`); 1268 assert(utCompile!("doctype xml\nfoo") == `<?xml version="1.0" encoding="utf-8" ?><foo/>`); 1269 } 1270 1271 */