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