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="&amp;&quot;"/>`);
1123 	assert(compile!("- auto param = \"t=1&u=1\";\na(href=\"/?#{param}&v=1\") foo")
1124 			== `<a href="/?t=1&amp;u=1&amp;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 */