PageRenderTime 338ms CodeModel.GetById 80ms app.highlight 204ms RepoModel.GetById 38ms app.codeStats 0ms

/source/vibe/templ/diet.d

https://github.com/BoapNet/vibe.d
D | 1376 lines | 1101 code | 136 blank | 139 comment | 221 complexity | a62bb1b064b8ffb3ab02667738f9a684 MD5 | raw file
   1/**
   2	Implements a compile-time Diet template parser.
   3
   4	Diet templates are an more or less compatible incarnation of Jade templates but with
   5	embedded D source instead of JavaScript. The Diet syntax reference is found at
   6	$(LINK http://vibed.org/templates/diet).
   7
   8	Copyright: Š 2012-2014 RejectedSoftware e.K.
   9	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
  10	Authors: SĂśnke Ludwig
  11*/
  12module vibe.templ.diet;
  13
  14public import vibe.core.stream;
  15
  16import vibe.core.file;
  17import vibe.templ.parsertools;
  18import vibe.templ.utils;
  19import vibe.textfilter.html;
  20static import vibe.textfilter.markdown;
  21import vibe.utils.string;
  22
  23import core.vararg;
  24import std.ascii : isAlpha;
  25import std.array;
  26import std.conv;
  27import std.format;
  28import std.metastrings;
  29import std.typecons;
  30import std.typetuple;
  31import std.variant;
  32
  33/*
  34	TODO:
  35		support string interpolations in filter blocks
  36		to!string and htmlEscape should not be used in conjunction with ~ at run time. instead,
  37		use filterHtmlEncode().
  38*/
  39
  40
  41/**
  42	Parses the given diet template at compile time and writes the resulting
  43	HTML code into 'stream'.
  44
  45	Note that this function suffers from multiple compiler bugsin conjunction with local
  46	variables passed as alias template parameters up to DMD 2.063.2. DMD 2.064 supposedly
  47	has these fixed.
  48*/
  49void compileDietFile(string template_file, ALIASES...)(OutputStream stream__)
  50{
  51	compileDietFileIndent!(template_file, 0, ALIASES)(stream__);
  52}
  53/// ditto
  54void compileDietFileIndent(string template_file, size_t indent, ALIASES...)(OutputStream stream__)
  55{
  56	// some imports to make available by default inside templates
  57	import vibe.http.common;
  58	import vibe.utils.string;
  59
  60	pragma(msg, "Compiling diet template '"~template_file~"'...");
  61	static if (ALIASES.length > 0 && __VERSION__ < 2064) {
  62		pragma(msg, "Warning: using render!() or parseDietFile!() with aliases is unsafe,");
  63		pragma(msg, "         please consider using renderCompat!()/parseDietFileCompat!()");
  64		pragma(msg, "         on DMD versions prior to 2.064.");
  65	}
  66	//pragma(msg, localAliases!(0, ALIASES));
  67	mixin(localAliases!(0, ALIASES));
  68
  69	static if (is(typeof(diet_translate__))) alias TRANSLATE = TypeTuple!(diet_translate__);
  70	else alias TRANSLATE = TypeTuple!();
  71
  72	// Generate the D source code for the diet template
  73	//pragma(msg, dietParser!template_file(indent));
  74	static if (is(typeof(diet_translate__)))
  75		mixin(dietParser!(template_file, diet_translate__)(indent));
  76	else
  77		mixin(dietParser!template_file(indent));
  78}
  79
  80/// compatibility alias
  81alias compileDietFile parseDietFile;
  82
  83/**
  84	Compatibility version of parseDietFile().
  85
  86	This function should only be called indirectly through HTTPServerResponse.renderCompat().
  87
  88*/
  89void compileDietFileCompat(string template_file, TYPES_AND_NAMES...)(OutputStream stream__, ...)
  90{
  91	compileDietFileCompatV!(template_file, TYPES_AND_NAMES)(stream__, _argptr, _arguments);
  92}
  93/// ditto
  94void compileDietFileCompatV(string template_file, TYPES_AND_NAMES...)(OutputStream stream__, va_list _argptr, TypeInfo[] _arguments)
  95{
  96	// some imports to make available by default inside templates
  97	import vibe.http.common;
  98	import vibe.utils.string;
  99
 100	pragma(msg, "Compiling diet template '"~template_file~"' (compat)...");
 101	//pragma(msg, localAliasesCompat!(0, TYPES_AND_NAMES));
 102	mixin(localAliasesCompat!(0, TYPES_AND_NAMES));
 103
 104	static if (is(typeof(diet_translate__))) alias TRANSLATE = TypeTuple!(diet_translate__);
 105	else alias TRANSLATE = TypeTuple!();
 106
 107	// Generate the D source code for the diet template
 108	//pragma(msg, dietParser!template_file());
 109	mixin(dietParser!template_file(0));
 110}
 111
 112/// compatibility alias
 113alias compileDietFileCompat parseDietFileCompat;
 114
 115/**
 116	Generates a diet template compiler to use as a mixin.
 117
 118	This can be used as an alternative to compileDietFile or compileDietFileCompat. It allows
 119	the template to use all symbols visible in the enclosing scope. In situations where many
 120	variables from the calling function's scope are used within the template, it can reduce the
 121	amount of code required for invoking the template.
 122
 123	Note that even if this method of using diet templates can reduce the amount of source code. It
 124	is generally recommended to use compileDietFile(Compat) instead, as those
 125	facilitate a cleaner interface between D code and diet code by explicity documenting the
 126	symbols usable inside of the template and thus avoiding unwanted, hidden dependencies. A
 127	possible alternative for passing many variables is to pass a struct or class value to
 128	compileDietFile(Compat).
 129
 130	Examples:
 131	---
 132	void handleRequest(HTTPServerRequest req, HTTPServerResponse res)
 133	{
 134		int this_variable_is_automatically_visible_to_the_template;
 135		mixin(compileDietFileMixin!("index.dt", "res.bodyWriter"));
 136	}
 137	---
 138*/
 139template compileDietFileMixin(string template_file, string stream_variable, size_t base_indent = 0)
 140{
 141	enum compileDietFileMixin = "OutputStream stream__ = "~stream_variable~";\n" ~ dietParser!template_file(base_indent);
 142}
 143
 144
 145/**
 146	The same as compileDietFile, but taking a Diet source code string instead of a file name.
 147*/
 148void compileDietString(string diet_code, ALIASES...)(OutputStream stream__)
 149{
 150	// some imports to make available by default inside templates
 151	import vibe.http.common;
 152	import vibe.utils.string;
 153	import std.typetuple;
 154	
 155	//pragma(msg, localAliases!(0, ALIASES));
 156	mixin(localAliases!(0, ALIASES));
 157
 158	// Generate the D source code for the diet template
 159	//pragma(msg, dietParser!template_file());
 160	static if (is(typeof(diet_translate__))) alias TRANSLATE = TypeTuple!(diet_translate__);
 161	else alias TRANSLATE = TypeTuple!();
 162
 163	mixin(dietStringParser!(diet_code, "__diet_code__", TRANSLATE)(0));
 164}
 165
 166
 167/**
 168	Registers a new text filter for use in Diet templates.
 169
 170	The filter will be available using :filtername inside of the template. The following filters are
 171	predefined: css, javascript, markdown
 172*/
 173void registerDietTextFilter(string name, string function(string, size_t indent) filter)
 174{
 175	s_filters[name] = filter;
 176}
 177
 178
 179/**************************************************************************************************/
 180/* private functions                                                                              */
 181/**************************************************************************************************/
 182
 183private {
 184	enum string StreamVariableName = "stream__";
 185	string function(string, size_t indent)[string] s_filters;
 186}
 187
 188static this()
 189{
 190	registerDietTextFilter("css", &filterCSS);
 191	registerDietTextFilter("javascript", &filterJavaScript);
 192	registerDietTextFilter("markdown", &filterMarkdown);
 193	registerDietTextFilter("htmlescape", &filterHtmlEscape);
 194}
 195
 196
 197private string dietParser(string template_file, TRANSLATE...)(size_t base_indent)
 198{
 199	TemplateBlock[] files;
 200	readFileRec!(template_file)(files);
 201	auto compiler = DietCompiler!TRANSLATE(&files[0], &files, new BlockStore);
 202	return compiler.buildWriter(base_indent);
 203}
 204
 205private string dietStringParser(string diet_code, string name, TRANSLATE...)(size_t base_indent)
 206{
 207	enum LINES = removeEmptyLines(diet_code, name);
 208
 209	TemplateBlock ret;
 210	ret.name = name;
 211	ret.lines = LINES;
 212	ret.indentStyle = detectIndentStyle(ret.lines);
 213
 214	TemplateBlock[] files;
 215	files ~= ret;
 216	readFilesRec!(extractDependencies(LINES), name)(files);
 217
 218	auto compiler = DietCompiler!TRANSLATE(&files[0], &files, new BlockStore);
 219	return compiler.buildWriter(base_indent);
 220}
 221
 222
 223/******************************************************************************/
 224/* Reading of input files                                                     */
 225/******************************************************************************/
 226
 227private struct TemplateBlock {
 228	string name;
 229	int mode = 0; // -1: prepend, 0: replace, 1: append
 230	string indentStyle;
 231	Line[] lines;
 232}
 233
 234private class BlockStore {
 235	TemplateBlock[] blocks;
 236}
 237
 238
 239/// private
 240private void readFileRec(string FILE, ALREADY_READ...)(ref TemplateBlock[] dst)
 241{
 242	static if( !isPartOf!(FILE, ALREADY_READ)() ){
 243		enum LINES = removeEmptyLines(import(FILE), FILE);
 244
 245		TemplateBlock ret;
 246		ret.name = FILE;
 247		ret.lines = LINES;
 248		ret.indentStyle = detectIndentStyle(ret.lines);
 249
 250		enum DEPS = extractDependencies(LINES);
 251		dst ~= ret;
 252		readFilesRec!(DEPS, ALREADY_READ, FILE)(dst);
 253	}
 254}
 255
 256/// private
 257private void readFilesRec(alias FILES, ALREADY_READ...)(ref TemplateBlock[] dst)
 258{
 259	static if( FILES.length > 0 ){
 260		readFileRec!(FILES[0], ALREADY_READ)(dst);
 261		readFilesRec!(FILES[1 .. $], ALREADY_READ, FILES[0])(dst);
 262	}
 263}
 264
 265/// private
 266private bool isPartOf(string str, STRINGS...)()
 267{
 268	foreach( s; STRINGS )
 269		if( str == s )
 270			return true;
 271	return false;
 272}
 273
 274private string[] extractDependencies(in Line[] lines)
 275{
 276	string[] ret;
 277	foreach (ref ln; lines) {
 278		auto lnstr = ln.text.ctstrip();
 279		if (lnstr.startsWith("extends ")) ret ~= lnstr[8 .. $].ctstrip() ~ ".dt";
 280	}
 281	return ret;
 282}
 283
 284
 285/******************************************************************************/
 286/* The Diet compiler                                                          */
 287/******************************************************************************/
 288
 289private class OutputContext {
 290	enum State {
 291		Code,
 292		String
 293	}
 294	struct Node {
 295		string tag;
 296		bool inner;
 297		bool outer;
 298		alias tag this;
 299	}
 300
 301	State m_state = State.Code;
 302	Node[] m_nodeStack;
 303	string m_result;
 304	Line m_line = Line(null, -1, null);
 305	size_t m_baseIndent;
 306	bool m_isHTML5;
 307
 308	this(size_t base_indent = 0)
 309	{
 310		m_baseIndent = base_indent;
 311	}
 312
 313	void markInputLine(in ref Line line)
 314	{
 315		if( m_state == State.Code ){
 316			m_result ~= lineMarker(line);
 317		} else {
 318			m_line = Line(line.file, line.number, null);
 319		}
 320	}
 321
 322	@property size_t stackSize() const { return m_nodeStack.length; }
 323
 324	void pushNode(string str, bool inner = true, bool outer = true) { m_nodeStack ~= Node(str, inner, outer); }
 325	void pushDummyNode() { pushNode("-"); }
 326
 327	void popNodes(int next_indent_level, ref bool prepend_whitespaces)
 328	{
 329		// close all tags/blocks until we reach the level of the next line
 330		while( m_nodeStack.length > next_indent_level ){
 331			auto top = m_nodeStack[$-1];
 332			if( top[0] == '-' ){
 333				if( top.length > 1 ){
 334					writeCodeLine(top[1 .. $]);
 335				}
 336			} else if( top.length ){
 337				if( top.inner && prepend_whitespaces && top != "</pre>" ){
 338					writeString("\n");
 339					writeIndent(m_nodeStack.length-1);
 340				}
 341
 342				writeString(top);
 343				prepend_whitespaces = top.outer;
 344			}
 345			m_nodeStack.length--;
 346		}
 347	}
 348
 349	// TODO: avoid runtime allocations by replacing htmlEscape/_toString calls with
 350	//       filtering functions
 351	void writeRawString(string str) { enterState(State.String); m_result ~= str; }
 352	void writeString(string str) { writeRawString(dstringEscape(str)); }
 353	void writeStringHtmlEscaped(string str) { writeString(htmlEscape(str)); }
 354	void writeIndent(size_t stack_depth = size_t.max)
 355	{
 356		import std.algorithm : min;
 357		string str;
 358		foreach (i; 0 .. m_baseIndent) str ~= '\t';
 359		foreach (j; 0 .. min(m_nodeStack.length, stack_depth)) if (m_nodeStack[j][0] != '-') str ~= '\t';
 360		writeRawString(str);
 361	}
 362
 363	void writeStringExpr(string str) { writeCodeLine(StreamVariableName~".write("~str~");"); }
 364	void writeStringExprHtmlEscaped(string str) { writeStringExpr("htmlEscape("~str~")"); }
 365	void writeStringExprHtmlAttribEscaped(string str) { writeStringExpr("htmlAttribEscape("~str~")"); }
 366
 367	void writeExpr(string str) { writeStringExpr("_toString("~str~")"); }
 368	void writeExprHtmlEscaped(string str) { writeStringExprHtmlEscaped("_toString("~str~")"); }
 369	void writeExprHtmlAttribEscaped(string str) { writeStringExprHtmlAttribEscaped("_toString("~str~")"); }
 370
 371	void writeCodeLine(string stmt)
 372	{
 373		if( !enterState(State.Code) )
 374			m_result ~= lineMarker(m_line);
 375		m_result ~= stmt ~ "\n";
 376	}
 377
 378	private bool enterState(State state)
 379	{
 380		if( state == m_state ) return false;
 381
 382		if( state != m_state.Code ) enterState(State.Code);
 383
 384		final switch(state){
 385			case State.Code:
 386				if( m_state == State.String ) m_result ~= "\");\n";
 387				else m_result ~= ");\n";
 388				m_result ~= lineMarker(m_line);
 389				break;
 390			case State.String:
 391				m_result ~= StreamVariableName ~ ".write(\"";
 392				break;
 393		}
 394
 395		m_state = state;
 396		return true;
 397	}
 398}
 399
 400private struct DietCompiler(TRANSLATE...)
 401	if(TRANSLATE.length <= 1)
 402{
 403	private {
 404		size_t m_lineIndex = 0;
 405		TemplateBlock* m_block;
 406		TemplateBlock[]* m_files;
 407		BlockStore m_blocks;
 408	}
 409
 410	@property ref string indentStyle() { return m_block.indentStyle; }
 411	@property size_t lineCount() { return m_block.lines.length; }
 412	ref Line line(size_t ln) { return m_block.lines[ln]; }
 413	ref Line currLine() { return m_block.lines[m_lineIndex]; }
 414	ref string currLineText() { return m_block.lines[m_lineIndex].text; }
 415	Line[] lineRange(size_t from, size_t to) { return m_block.lines[from .. to]; }
 416
 417	@disable this();
 418
 419	this(TemplateBlock* block, TemplateBlock[]* files, BlockStore blocks)
 420	{
 421		m_block = block;
 422		m_files = files;
 423		m_blocks = blocks;
 424	}
 425
 426	string buildWriter(size_t base_indent)
 427	{
 428		auto output = new OutputContext(base_indent);
 429		buildWriter(output, 0);
 430		assert(output.m_nodeStack.length == 0, "Template writer did not consume all nodes!?");
 431		return output.m_result;
 432	}
 433
 434	void buildWriter(OutputContext output, int base_level)
 435	{
 436		assert(m_blocks !is null, "Trying to compile template with no blocks specified.");
 437
 438		while(true){
 439			if( lineCount == 0 ) return;
 440			auto firstline = line(m_lineIndex);
 441			auto firstlinetext = firstline.text;
 442
 443			if( firstlinetext.startsWith("extends ") ){
 444				string layout_file = firstlinetext[8 .. $].ctstrip() ~ ".dt";
 445				auto extfile = getFile(layout_file);
 446				m_lineIndex++;
 447
 448				// extract all blocks
 449				while( m_lineIndex < lineCount ){
 450					TemplateBlock subblock;
 451
 452					// read block header
 453					string blockheader = line(m_lineIndex).text;
 454					size_t spidx = 0;
 455					auto mode = skipIdent(line(m_lineIndex).text, spidx, "");
 456					assertp(spidx > 0, "Expected block/append/prepend.");
 457					subblock.name = blockheader[spidx .. $].ctstrip();
 458					if( mode == "block" ) subblock.mode = 0;
 459					else if( mode == "append" ) subblock.mode = 1;
 460					else if( mode == "prepend" ) subblock.mode = -1;
 461					else assertp(false, "Expected block/append/prepend.");
 462					m_lineIndex++;
 463
 464					// skip to next block
 465					auto block_start = m_lineIndex;
 466					while( m_lineIndex < lineCount ){
 467						auto lvl = indentLevel(line(m_lineIndex).text, indentStyle, false);
 468						if( lvl == 0 ) break;
 469						m_lineIndex++;
 470					}
 471
 472					// append block to compiler
 473					subblock.lines = lineRange(block_start, m_lineIndex);
 474					subblock.indentStyle = indentStyle;
 475					m_blocks.blocks ~= subblock;
 476
 477					//output.writeString("<!-- found block "~subblock.name~" in "~line(0).file ~ "-->\n");
 478				}
 479
 480				// change to layout file and start over
 481				m_block = extfile;
 482				m_lineIndex = 0;
 483			} else {
 484				auto start_indent_level = indentLevel(firstlinetext, indentStyle);
 485				//assertp(start_indent_level == 0, "Indentation must start at level zero.");
 486				buildBodyWriter(output, base_level, start_indent_level);
 487				break;
 488			}
 489		}
 490
 491		output.enterState(OutputContext.State.Code);
 492	}
 493
 494	private void buildBodyWriter(OutputContext output, int base_level, int start_indent_level)
 495	{
 496		assert(m_blocks !is null, "Trying to compile template body with no blocks specified.");
 497
 498		assertp(output.stackSize >= base_level);
 499
 500		int computeNextIndentLevel(){
 501			return (m_lineIndex+1 < lineCount ? indentLevel(line(m_lineIndex+1).text, indentStyle, false) - start_indent_level : 0) + base_level;
 502		}
 503
 504		bool prepend_whitespaces = true;
 505
 506		for( ; m_lineIndex < lineCount; m_lineIndex++ ){
 507			auto curline = line(m_lineIndex);
 508			output.markInputLine(curline);
 509			auto level = indentLevel(curline.text, indentStyle) - start_indent_level + base_level;
 510			assertp(level <= output.stackSize+1);
 511			auto ln = unindent(curline.text, indentStyle);
 512			assertp(ln.length > 0);
 513			int next_indent_level = computeNextIndentLevel();
 514
 515			assertp(output.stackSize >= level, cttostring(output.stackSize) ~ ">=" ~ cttostring(level));
 516			assertp(next_indent_level <= level+1, "The next line is indented by more than one level deeper. Please unindent accordingly.");
 517
 518			if( ln[0] == '-' ){ // embedded D code
 519				assertp(ln[$-1] != '{', "Use indentation to nest D statements instead of braces.");
 520				output.writeCodeLine(ln[1 .. $] ~ "{");
 521				output.pushNode("-}");
 522			} else if( ln[0] == '|' ){ // plain text node
 523				buildTextNodeWriter(output, ln[1 .. ln.length], level, prepend_whitespaces);
 524			} else if( ln[0] == ':' ){ // filter node (filtered raw text)
 525				// find all child lines
 526				size_t next_tag = m_lineIndex+1;
 527				while( next_tag < lineCount &&
 528					indentLevel(line(next_tag).text, indentStyle, false) - start_indent_level > level-base_level )
 529				{
 530					next_tag++;
 531				}
 532
 533				buildFilterNodeWriter(output, ln, curline.number, level + start_indent_level - base_level,
 534						lineRange(m_lineIndex+1, next_tag));
 535
 536				// skip to the next tag
 537				//output.pushDummyNode();
 538				m_lineIndex = next_tag-1;
 539				next_indent_level = computeNextIndentLevel();
 540			} else {
 541				size_t j = 0;
 542				auto tag = isAlpha(ln[0]) || ln[0] == '/' ? skipIdent(ln, j, "/:-_") : "div";
 543
 544				if (ln.startsWith("!!! ")) {
 545					//output.writeCodeLine(`pragma(msg, "\"!!!\" is deprecated, use \"doctype\" instead.");`);
 546					tag = "doctype";
 547					j += 4;
 548				}
 549
 550				switch(tag){
 551					default:
 552						if (buildHtmlNodeWriter(output, tag, ln[j .. $], level, next_indent_level > level, prepend_whitespaces)) {
 553							// tag had a '.' appended. treat child nodes as plain text
 554							size_t next_tag = m_lineIndex + 1;
 555							size_t unindent_count = level + start_indent_level - base_level + 1;
 556							size_t last_line_number = curline.number;
 557							while( next_tag < lineCount &&
 558							      indentLevel(line(next_tag).text, indentStyle, false) - start_indent_level > level-base_level )
 559							{
 560								// TODO: output all empty lines between this and the previous one
 561								foreach (i; last_line_number+1 .. line(next_tag).number) output.writeString("\n");
 562								last_line_number = line(next_tag).number;
 563								buildTextNodeWriter(output, unindent(line(next_tag++).text, indentStyle, unindent_count), level, prepend_whitespaces);
 564							}
 565							m_lineIndex = next_tag - 1;
 566							next_indent_level = computeNextIndentLevel();
 567						}
 568						break;
 569					case "doctype": // HTML Doctype header
 570						buildDoctypeNodeWriter(output, ln, j, level);
 571						break;
 572					case "//": // HTML comment
 573						skipWhitespace(ln, j);
 574						output.writeString("<!-- " ~ htmlEscape(ln[j .. $]));
 575						output.pushNode(" -->");
 576						break;
 577					case "//-": // non-output comment
 578						// find all child lines
 579						size_t next_tag = m_lineIndex+1;
 580						while( next_tag < lineCount &&
 581							indentLevel(line(next_tag).text, indentStyle, false) - start_indent_level > level-base_level )
 582						{
 583							next_tag++;
 584						}
 585
 586						// skip to the next tag
 587						m_lineIndex = next_tag-1;
 588						next_indent_level = computeNextIndentLevel();
 589						break;
 590					case "//if": // IE conditional comment
 591						skipWhitespace(ln, j);
 592						buildSpecialTag(output, "!--[if "~ln[j .. $]~"]", level);
 593						output.pushNode("<![endif]-->");
 594						break;
 595					case "block": // Block insertion place
 596						assertp(next_indent_level <= level, "Child elements for 'include' are not supported.");
 597						output.pushDummyNode();
 598						auto block = getBlock(ln[6 .. $].ctstrip());
 599						if( block ){
 600							output.writeString("<!-- using block " ~ ln[6 .. $] ~ " in " ~ curline.file ~ "-->");
 601							if( block.mode == 1 ){
 602								// output defaults
 603							}
 604							auto blockcompiler = new DietCompiler(block, m_files, m_blocks);
 605							/*blockcompiler.m_block = block;
 606							blockcompiler.m_blocks = m_blocks;*/
 607							blockcompiler.buildWriter(output, cast(int)output.m_nodeStack.length);
 608
 609							if( block.mode == -1 ){
 610								// output defaults
 611							}
 612						} else {
 613							// output defaults
 614							output.writeString("<!-- Default block " ~ ln[6 .. $] ~ " in " ~ curline.file ~ "-->");
 615						}
 616						break;
 617					case "include": // Diet file include
 618						assertp(next_indent_level <= level, "Child elements for 'include' are not supported.");
 619						auto content = ln[8 .. $].ctstrip();
 620						if (content.startsWith("#{")) {
 621							assertp(content.endsWith("}"), "Missing closing '}'.");
 622							output.writeCodeLine("mixin(dietStringParser!("~content[2 .. $-1]~", \""~replace(content, `"`, `'`)~"\", TRANSLATE)("~to!string(level)~"));");
 623						} else {
 624							output.writeCodeLine("mixin(dietParser!(\""~content~".dt\", TRANSLATE)("~to!string(level)~"));");
 625						}
 626						break;
 627					case "script":
 628					case "style":
 629						// determine if this is a plain css/JS tag (without a trailing .) and output a warning
 630						// for using deprecated behavior
 631						auto tagline = ln[j .. $];
 632						HTMLAttribute[] attribs;
 633						size_t tli;
 634						auto wst = parseHtmlTag(tagline, tli, attribs);
 635						tagline = tagline[0 .. tli];
 636						if (wst.block_tag) goto default;
 637						enum legacy_types = [`"text/css"`, `"text/javascript"`, `"application/javascript"`, `'text/css'`, `'text/javascript'`, `'application/javascript'`];
 638						bool is_legacy_type = true;
 639						foreach (i, ref a; attribs)
 640							if (a.key == "type") {
 641								is_legacy_type = false;
 642								foreach (t; legacy_types)
 643									if (a.value == t) {
 644										is_legacy_type = true;
 645										break;
 646									}
 647								break;
 648							}
 649						if (!is_legacy_type) goto default;
 650
 651						if (next_indent_level <= level) {
 652							buildHtmlNodeWriter(output, tag, ln[j .. $], level, false, prepend_whitespaces);
 653						} else {
 654							output.writeCodeLine(`pragma(msg, "`~dstringEscape(currLine.file)~`:`~currLine.number.to!string~
 655								`: Warning: Use an explicit text block '`~tag~dstringEscape(tagline)~
 656								`.' for embedded css/javascript - old behavior will be removed soon.");`);
 657
 658							// pass all child lines to buildRawTag and continue with the next sibling
 659							size_t next_tag = m_lineIndex+1;
 660							while( next_tag < lineCount &&
 661								indentLevel(line(next_tag).text, indentStyle, false) - start_indent_level > level-base_level )
 662							{
 663								next_tag++;
 664							}
 665							buildRawNodeWriter(output, tag, ln[j .. $], level, base_level,
 666								lineRange(m_lineIndex+1, next_tag));
 667							m_lineIndex = next_tag-1;
 668							next_indent_level = computeNextIndentLevel();
 669						}
 670						break;
 671					case "each":
 672					case "for":
 673					case "if":
 674					case "unless":
 675					case "mixin":
 676						assertp(false, "'"~tag~"' is not supported.");
 677						break;
 678				}
 679			}
 680			output.popNodes(next_indent_level, prepend_whitespaces);
 681		}
 682	}
 683
 684	private void buildTextNodeWriter(OutputContext output, in string textline, int level, ref bool prepend_whitespaces)
 685	{
 686		if(prepend_whitespaces) output.writeString("\n");
 687		if( textline.length >= 1 && textline[0] == '=' ){
 688			output.writeExprHtmlEscaped(textline[1 .. $]);
 689		} else if( textline.length >= 2 && textline[0 .. 2] == "!=" ){
 690			output.writeExpr(textline[2 .. $]);
 691		} else {
 692			buildInterpolatedString(output, textline);
 693		}
 694		output.pushDummyNode();
 695		prepend_whitespaces = true;
 696	}
 697
 698	private void buildDoctypeNodeWriter(OutputContext output, string ln, size_t j, int level)
 699	{
 700		skipWhitespace(ln, j);
 701		output.m_isHTML5 = false;
 702
 703		string doctype_str = "!DOCTYPE html";
 704		switch (ln[j .. $]) {
 705			case "5":
 706			case "":
 707			case "html":
 708				output.m_isHTML5 = true;
 709				break;
 710			case "xml":
 711				doctype_str = `?xml version="1.0" encoding="utf-8" ?`;
 712				break;
 713			case "transitional":
 714				doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" `
 715					~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd`;
 716				break;
 717			case "strict":
 718				doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" `
 719					~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"`;
 720				break;
 721			case "frameset":
 722				doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" `
 723					~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"`;
 724				break;
 725			case "1.1":
 726				doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" `
 727					~ `"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"`;
 728				break;
 729			case "basic":
 730				doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" `
 731					~ `"http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd"`;
 732				break;
 733			case "mobile":
 734				doctype_str = `!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" `
 735					~ `"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd"`;
 736				break;
 737			default:
 738				doctype_str = "!DOCTYPE " ~ ln[j .. $];
 739			break;
 740		}
 741		buildSpecialTag(output, doctype_str, level);
 742	}
 743
 744	private bool buildHtmlNodeWriter(OutputContext output, in ref string tag, in string line, int level, bool has_child_nodes, ref bool prepend_whitespaces)
 745	{
 746		// parse the HTML tag, leaving any trailing text as line[i .. $]
 747		size_t i;
 748		HTMLAttribute[] attribs;
 749		auto ws_type = parseHtmlTag(line, i, attribs);
 750
 751		// determine if we need a closing tag
 752		bool is_singular_tag = false;
 753		switch(tag){
 754			case "area", "base", "basefont", "br", "col", "embed", "frame",	"hr", "img", "input",
 755					"keygen", "link", "meta", "param", "source", "track", "wbr":
 756				is_singular_tag = true;
 757				break;
 758			default:
 759		}
 760		assertp(!(is_singular_tag && has_child_nodes), "Singular HTML element '"~tag~"' may not have children.");
 761
 762		// opening tag
 763		buildHtmlTag(output, tag, level, attribs, is_singular_tag, ws_type.outer && prepend_whitespaces);
 764
 765		// parse any text contents (either using "= code" or as plain text)
 766		if( i < line.length && line[i] == '=' ){
 767			output.writeExprHtmlEscaped(ctstrip(line[i+1 .. line.length]));
 768		} else if( i+1 < line.length && line[i .. i+2] == "!=" ){
 769			output.writeExpr(ctstrip(line[i+2 .. line.length]));
 770		} else {
 771			string rawtext = line[i .. line.length];
 772			static if (TRANSLATE.length > 0) if (ws_type.isTranslated) rawtext = TRANSLATE[0](rawtext);
 773			if (hasInterpolations(rawtext)) {
 774				buildInterpolatedString(output, rawtext);
 775			} else {
 776				output.writeRawString(sanitizeEscaping(rawtext));
 777			}
 778		}
 779
 780		// closing tag
 781		if( has_child_nodes ) output.pushNode("</" ~ tag ~ ">", ws_type.inner, ws_type.outer);
 782		else if( !is_singular_tag ) output.writeString("</" ~ tag ~ ">");
 783		prepend_whitespaces = has_child_nodes ? ws_type.inner : ws_type.outer;
 784
 785		return ws_type.block_tag;
 786	}
 787
 788	private void buildRawNodeWriter(OutputContext output, in ref string tag, in string tagline, int level,
 789			int base_level, in Line[] lines)
 790	{
 791		// parse the HTML tag leaving any trailing text as tagline[i .. $]
 792		size_t i;
 793		HTMLAttribute[] attribs;
 794		parseHtmlTag(tagline, i, attribs);
 795
 796		// write the tag
 797		buildHtmlTag(output, tag, level, attribs, false);
 798
 799		string indent_string = "\t";
 800		foreach (j; 0 .. output.m_baseIndent) indent_string ~= '\t';
 801		foreach (j; 0 .. level ) if( output.m_nodeStack[j][0] != '-') indent_string ~= '\t';
 802
 803		// write the block contents wrapped in a CDATA for old browsers
 804		if( tag == "script" ) output.writeString("\n"~indent_string~"//<![CDATA[\n");
 805		else output.writeString("\n"~indent_string~"<!--\n");
 806
 807		// write out all lines
 808		void writeLine(string str){
 809			if( !hasInterpolations(str) ){
 810				output.writeString(indent_string ~ str ~ "\n");
 811			} else {
 812				output.writeString(indent_string);
 813				buildInterpolatedString(output, str);
 814			}
 815		}
 816		if( i < tagline.length ) writeLine(tagline[i .. $]);
 817		foreach( j; 0 .. lines.length ){
 818			// remove indentation
 819			string lnstr = lines[j].text[(level-base_level+1)*indentStyle.length .. $];
 820			writeLine(lnstr);
 821		}
 822		if( tag == "script" ) output.writeString(indent_string~"//]]>\n");
 823		else output.writeString(indent_string~"-->\n");
 824		output.writeString(indent_string[0 .. $-1] ~ "</" ~ tag ~ ">");
 825	}
 826
 827	private void buildFilterNodeWriter(OutputContext output, in ref string tagline, int tagline_number,
 828		int indent, in Line[] lines)
 829	{
 830		// find all filters
 831		size_t j = 0;
 832		string[] filters;
 833		do {
 834			j++;
 835			filters ~= skipIdent(tagline, j);
 836			skipWhitespace(tagline, j);
 837		} while( j < tagline.length && tagline[j] == ':' );
 838
 839		// assemble child lines to one string
 840		string content = tagline[j .. $];
 841		int lc = content.length ? tagline_number : tagline_number+1;
 842		foreach( i; 0 .. lines.length ){
 843			while( lc < lines[i].number ){ // DMDBUG: while(lc++ < lines[i].number) silently loops and only executes the last iteration
 844				content ~= '\n';
 845				lc++;
 846			}
 847			content ~= lines[i].text[(indent+1)*indentStyle.length .. $];
 848		}
 849
 850		auto out_indent = output.m_baseIndent + indent;
 851
 852		// compile-time filter whats possible
 853		filter_loop:
 854		foreach_reverse( f; filters ){
 855			bool found = true;
 856			switch(f){
 857				default: found = false; break;//break filter_loop;
 858				case "css": content = filterCSS(content, out_indent); break;
 859				case "javascript": content = filterJavaScript(content, out_indent); break;
 860				case "markdown": content = filterMarkdown(content, out_indent); break;
 861				case "htmlescape": content = filterHtmlEscape(content, out_indent); break;
 862			}
 863			if (found) filters.length = filters.length-1;
 864			else break;
 865		}
 866
 867		// the rest of the filtering will happen at run time
 868		string filter_expr;
 869		foreach_reverse( flt; filters ) filter_expr ~= "s_filters[\""~dstringEscape(flt)~"\"](";
 870		filter_expr ~= "\"" ~ dstringEscape(content) ~ "\"";
 871		foreach( i; 0 .. filters.length ) filter_expr ~= ", "~cttostring(out_indent)~")";
 872
 873		output.writeStringExpr(filter_expr);
 874	}
 875
 876	private auto parseHtmlTag(in ref string line, out size_t i, out HTMLAttribute[] attribs)
 877	{
 878		struct WSType {
 879			bool inner = true;
 880			bool outer = true;
 881			bool block_tag = false;
 882			bool isTranslated;
 883		}
 884
 885		i = 0;
 886
 887		string id;
 888		string classes;
 889
 890		WSType ws_type;
 891
 892		// parse #id and .classes
 893		while( i < line.length ){
 894			if( line[i] == '#' ){
 895				i++;
 896				assertp(id.length == 0, "Id may only be set once.");
 897				id = skipIdent(line, i, "-_");
 898
 899				// put #id and .classes into the attribs list
 900				if( id.length ) attribs ~= HTMLAttribute("id", '"'~id~'"');
 901			} else if (line[i] == '&') {
 902				i++;
 903				assertp(i >= line.length || line[i] == ' ' || line[i] == '.');
 904				ws_type.isTranslated = true;
 905			} else if( line[i] == '.' ){
 906				i++;
 907				// check if tag ends with dot
 908				if (i == line.length || line[i] == ' ') {
 909					i = line.length;
 910					ws_type.block_tag = true;
 911					break;
 912				}
 913				auto cls = skipIdent(line, i, "-_");
 914				if( classes.length == 0 ) classes = cls;
 915				else classes ~= " " ~ cls;
 916			} else if (line[i] == '(') {
 917				// parse other attributes
 918				i++;
 919				string attribstring = skipUntilClosingClamp(line, i);
 920				parseAttributes(attribstring, attribs);
 921				i++;
 922			} else break;
 923		}
 924
 925		// parse whitespaces removal tokens
 926		for(; i < line.length; i++) {
 927			if(line[i] == '<') ws_type.inner = false;
 928			else if(line[i] == '>') ws_type.outer = false;
 929			else break;
 930		}
 931
 932		// add special attribute for extra classes that is handled by buildHtmlTag
 933		if( classes.length ){
 934			bool has_class = false;
 935			foreach( a; attribs )
 936				if( a.key == "class" ){
 937					has_class = true;
 938					break;
 939				}
 940
 941			if( has_class ) attribs ~= HTMLAttribute("$class", classes);
 942			else attribs ~= HTMLAttribute("class", "\"" ~ classes ~ "\"");
 943		}
 944
 945		// skip until the optional tag text contents begin
 946		skipWhitespace(line, i);
 947
 948		return ws_type;
 949	}
 950
 951	private void buildHtmlTag(OutputContext output, in ref string tag, int level, ref HTMLAttribute[] attribs, bool is_singular_tag, bool outer_whitespaces = true)
 952	{
 953		if (outer_whitespaces) {
 954			output.writeString("\n");
 955			assertp(output.stackSize >= level);
 956			output.writeIndent(level);
 957		}
 958		output.writeString("<" ~ tag);
 959		foreach( att; attribs ){
 960			if( att.key[0] == '$' ) continue; // ignore special attributes
 961			if( isStringLiteral(att.value) ){
 962				output.writeString(" "~att.key~"=\"");
 963				if( !hasInterpolations(att.value) ) output.writeString(htmlAttribEscape(dstringUnescape(att.value[1 .. $-1])));
 964				else buildInterpolatedString(output, att.value[1 .. $-1], true);
 965
 966				// output extra classes given as .class
 967				if( att.key == "class" ){
 968					foreach( a; attribs )
 969						if( a.key == "$class" ){
 970							output.writeString(" " ~ a.value);
 971							break;
 972						}
 973				}
 974
 975				output.writeString("\"");
 976			} else {
 977				output.writeCodeLine("static if(is(typeof("~att.value~") == bool)){ if("~att.value~"){");
 978				if (!output.m_isHTML5)
 979					output.writeString(` `~att.key~`="`~att.key~`"`);
 980				else
 981					output.writeString(` `~att.key);
 982				output.writeCodeLine("}} else static if(is(typeof("~att.value~") == string[])){\n");
 983				output.writeString(` `~att.key~`="`);
 984				output.writeExprHtmlAttribEscaped(`join(`~att.value~`, " ")`);
 985				output.writeString(`"`);
 986				output.writeCodeLine("} else static if(is(typeof("~att.value~") == string)) {");
 987				output.writeCodeLine("if ("~att.value~"){");
 988				output.writeString(` `~att.key~`="`);
 989				output.writeExprHtmlAttribEscaped(att.value);
 990				output.writeString(`"`);
 991				output.writeCodeLine("}");
 992				output.writeCodeLine("} else {");
 993				output.writeString(` `~att.key~`="`);
 994				output.writeExprHtmlAttribEscaped(att.value);
 995				output.writeString(`"`);
 996				output.writeCodeLine("}");
 997			}
 998		}
 999
1000		output.writeString(is_singular_tag ? "/>" : ">");
1001	}
1002
1003	private void parseAttributes(in ref string str, ref HTMLAttribute[] attribs)
1004	{
1005		size_t i = 0;
1006		skipWhitespace(str, i);
1007		while( i < str.length ){
1008			string name = skipIdent(str, i, "-:");
1009			string value;
1010			skipWhitespace(str, i);
1011			if( str[i] == '=' ){
1012				i++;
1013				skipWhitespace(str, i);
1014				assertp(i < str.length, "'=' must be followed by attribute string.");
1015				value = skipExpression(str, i);
1016				if( isStringLiteral(value) && value[0] == '\'' ){
1017					value = '"' ~ value[1 .. $-1] ~ '"';
1018				}
1019			} else value = "true";
1020
1021			assertp(i == str.length || str[i] == ',', "Unexpected text following attribute: '"~str[0..i]~"' ('"~str[i..$]~"')");
1022			if( i < str.length ){
1023				i++;
1024				skipWhitespace(str, i);
1025			}
1026
1027			if (name == "class" && value == `""`) continue;
1028			attribs ~= HTMLAttribute(name, value);
1029		}
1030	}
1031
1032	private bool hasInterpolations(in char[] str)
1033	{
1034		size_t i = 0;
1035		while( i < str.length ){
1036			if( str[i] == '\\' ){
1037				i += 2;
1038				continue;
1039			}
1040			if( i+1 < str.length && (str[i] == '#' || str[i] == '!') ){
1041				if( str[i+1] == str[i] ){
1042					i += 2;
1043					continue;
1044				} else if( str[i+1] == '{' ){
1045					return true;
1046				}
1047			}
1048			i++;
1049		}
1050		return false;
1051	}
1052
1053	private void buildInterpolatedString(OutputContext output, string str, bool escape_quotes = false)
1054	{
1055		size_t start = 0, i = 0;
1056		while( i < str.length ){
1057			// check for escaped characters
1058			if( str[i] == '\\' ){
1059				if( i > start ) output.writeString(str[start .. i]);
1060				output.writeRawString(sanitizeEscaping(str[i .. i+2]));
1061				i += 2;
1062				start = i;
1063				continue;
1064			}
1065
1066			if( (str[i] == '#' || str[i] == '!') && i+1 < str.length ){
1067				bool escape = str[i] == '#';
1068				if( i > start ){
1069					output.writeString(str[start .. i]);
1070					start = i;
1071				}
1072				assertp(str[i+1] != str[i], "Please use \\ to escape # or ! instead of ## or !!.");
1073				if( str[i+1] == '{' ){
1074					i += 2;
1075					auto expr = dstringUnescape(skipUntilClosingBrace(str, i));
1076					if( escape && !escape_quotes ) output.writeExprHtmlEscaped(expr);
1077					else if( escape ) output.writeExprHtmlAttribEscaped(expr);
1078					else output.writeExpr(expr);
1079					i++;
1080					start = i;
1081				} else i++;
1082			} else i++;
1083		}
1084
1085		if( i > start ) output.writeString(str[start .. i]);
1086	}
1087
1088	private string skipIdent(in ref string s, ref size_t idx, string additional_chars = null)
1089	{
1090		size_t start = idx;
1091		while( idx < s.length ){
1092			if( isAlpha(s[idx]) ) idx++;
1093			else if( start != idx && s[idx] >= '0' && s[idx] <= '9' ) idx++;
1094			else {
1095				bool found = false;
1096				foreach( ch; additional_chars )
1097					if( s[idx] == ch ){
1098						found = true;
1099						idx++;
1100						break;
1101					}
1102				if( !found ){
1103					assertp(start != idx, "Expected identifier but got '"~s[idx]~"'.");
1104					return s[start .. idx];
1105				}
1106			}
1107		}
1108		assertp(start != idx, "Expected identifier but got nothing.");
1109		return s[start .. idx];
1110	}
1111
1112	private string skipWhitespace(in ref string s, ref size_t idx)
1113	{
1114		size_t start = idx;
1115		while( idx < s.length ){
1116			if( s[idx] == ' ' ) idx++;
1117			else break;
1118		}
1119		return s[start .. idx];
1120	}
1121
1122	private string skipUntilClosingBrace(in ref string s, ref size_t idx)
1123	{
1124		int level = 0;
1125		auto start = idx;
1126		while( idx < s.length ){
1127			if( s[idx] == '{' ) level++;
1128			else if( s[idx] == '}' ) level--;
1129			if( level < 0 ) return s[start .. idx];
1130			idx++;
1131		}
1132		assertp(false, "Missing closing brace");
1133		assert(false);
1134	}
1135
1136	private string skipUntilClosingClamp(in ref string s, ref size_t idx)
1137	{
1138		int level = 0;
1139		auto start = idx;
1140		while( idx < s.length ){
1141			if( s[idx] == '(' ) level++;
1142			else if( s[idx] == ')' ) level--;
1143			if( level < 0 ) return s[start .. idx];
1144			idx++;
1145		}
1146		assertp(false, "Missing closing clamp");
1147		assert(false);
1148	}
1149
1150	private string skipAttribString(in ref string s, ref size_t idx, char delimiter)
1151	{
1152		size_t start = idx;
1153		while( idx < s.length ){
1154			if( s[idx] == '\\' ){
1155				// pass escape character through - will be handled later by buildInterpolatedString
1156				idx++;
1157				assertp(idx < s.length, "'\\' must be followed by something (escaped character)!");
1158			} else if( s[idx] == delimiter ) break;
1159			idx++;
1160		}
1161		return s[start .. idx];
1162	}
1163
1164	private string skipExpression(in ref string s, ref size_t idx)
1165	{
1166		string clamp_stack;
1167		size_t start = idx;
1168		while( idx < s.length ){
1169			switch( s[idx] ){
1170				default: break;
1171				case ',':
1172					if( clamp_stack.length == 0 )
1173						return s[start .. idx];
1174					break;
1175				case '"', '\'':
1176					idx++;
1177					skipAttribString(s, idx, s[idx-1]);
1178					break;
1179				case '(': clamp_stack ~= ')'; break;
1180				case '[': clamp_stack ~= ']'; break;
1181				case '{': clamp_stack ~= '}'; break;
1182				case ')', ']', '}':
1183					if( s[idx] == ')' && clamp_stack.length == 0 )
1184						return s[start .. idx];
1185					assertp(clamp_stack.length > 0 && clamp_stack[$-1] == s[idx],
1186						"Unexpected '"~s[idx]~"'");
1187					clamp_stack.length--;
1188					break;
1189			}
1190			idx++;
1191		}
1192
1193		assertp(clamp_stack.length == 0, "Expected '"~clamp_stack[$-1]~"' before end of attribute expression.");
1194		return s[start .. $];
1195	}
1196
1197	private string unindent(in ref string str, in ref string indent)
1198	{
1199		size_t lvl = indentLevel(str, indent);
1200		return str[lvl*indent.length .. $];
1201	}
1202
1203	private string unindent(in ref string str, in ref string indent, size_t level)
1204	{
1205		assert(level <= indentLevel(str, indent));
1206		return str[level*indent.length .. $];
1207	}
1208
1209	private int indentLevel(in ref string s, in ref string indent, bool strict = true)
1210	{
1211		if( indent.length == 0 ) return 0;
1212		assertp(!strict || (s[0] != ' ' && s[0] != '\t') || s[0] == indent[0],
1213			"Indentation style is inconsistent with previous lines.");
1214		int l = 0;
1215		while( l+indent.length <= s.length && s[l .. l+indent.length] == indent )
1216			l += cast(int)indent.length;
1217		assertp(!strict || s[l] != ' ', "Indent is not a multiple of '"~indent~"'");
1218		return l / cast(int)indent.length;
1219	}
1220
1221	private int indentLevel(in ref Line[] ln, string indent)
1222	{
1223		return ln.length == 0 ? 0 : indentLevel(ln[0].text, indent);
1224	}
1225
1226	private void assertp(bool cond, lazy string text = null, string file = __FILE__, int cline = __LINE__)
1227	{
1228		Line ln;
1229		if( m_lineIndex < lineCount ) ln = line(m_lineIndex);
1230		assert(cond, "template "~ln.file~" line "~cttostring(ln.number)~": "~text~"("~file~":"~cttostring(cline)~")");
1231	}
1232
1233	private TemplateBlock* getFile(string filename)
1234	{
1235		foreach( i; 0 .. m_files.length )
1236			if( (*m_files)[i].name == filename )
1237				return &(*m_files)[i];
1238		assertp(false, "Bug: include input file "~filename~" not found in internal list!?");
1239		assert(false);
1240	}
1241
1242	private TemplateBlock* getBlock(string name)
1243	{
1244		foreach( i; 0 .. m_blocks.blocks.length )
1245			if( m_blocks.blocks[i].name == name )
1246				return &m_blocks.blocks[i];
1247		return null;
1248	}
1249}
1250
1251private struct HTMLAttribute {
1252	string key;
1253	string value;
1254}
1255
1256/// private
1257private void buildSpecialTag(OutputContext output, string tag, int level)
1258{
1259	output.writeString("\n");
1260	output.writeIndent(level);
1261	output.writeString("<" ~ tag ~ ">");
1262}
1263
1264private bool isStringLiteral(string str)
1265{
1266	size_t i = 0;
1267	while( i < str.length && (str[i] == ' ' || str[i] == '\t') ) i++;
1268	if( i >= str.length ) return false;
1269	char delimiter = str[i];
1270	if( delimiter != '"' && delimiter != '\'' ) return false;
1271	while( i < str.length && str[i] != delimiter ){
1272		if( str[i] == '\\' ) i++;
1273		i++;
1274	}
1275	return i < str.length;
1276}
1277
1278/// Internal function used for converting an interpolation expression to string
1279string _toString(T)(T v)
1280{
1281	static if( is(T == string) ) return v;
1282	else static if( __traits(compiles, v.opCast!string()) ) return cast(string)v;
1283	else static if( __traits(compiles, v.toString()) ) return v.toString();
1284	else return to!string(v);
1285}
1286
1287unittest {
1288	static string compile(string diet, ALIASES...)() {
1289		import vibe.stream.memory;
1290		auto dst = new MemoryOutputStream;
1291		compileDietString!(diet, ALIASES)(dst);
1292		return strip(cast(string)(dst.data));
1293	}
1294
1295	assert(compile!(`!!! 5`) == `<!DOCTYPE html>`, `_`~compile!(`!!! 5`)~`_`);
1296	assert(compile!(`!!! html`) == `<!DOCTYPE html>`);
1297	assert(compile!(`doctype html`) == `<!DOCTYPE html>`);
1298	assert(compile!(`p= 5`) == `<p>5</p>`);
1299	assert(compile!(`script= 5`) == `<script>5</script>`);
1300	assert(compile!(`style= 5`) == `<style>5</style>`);
1301	assert(compile!(`include #{"p Hello"}`) == "<p>Hello</p>");
1302
1303	// issue 372
1304	assert(compile!(`div(class="")`) == `<div></div>`);
1305	assert(compile!(`div.foo(class="")`) == `<div class="foo"></div>`);
1306	assert(compile!(`div.foo(class="bar")`) == `<div class="bar foo"></div>`);
1307	assert(compile!(`div(class="foo")`) == `<div class="foo"></div>`);
1308	assert(compile!(`div#foo(class='')`) == `<div id="foo"></div>`);
1309
1310	// issue 520
1311	assert(compile!("- auto cond = true;\ndiv(someattr=cond ? \"foo\" : null)") == "<div someattr=\"foo\"></div>");
1312	assert(compile!("- auto cond = false;\ndiv(someattr=cond ? \"foo\" : null)") == "<div></div>");
1313	assert(compile!("- auto cond = false;\ndiv(someattr=cond ? true : false)") == "<div></div>");
1314	assert(compile!("- auto cond = true;\ndiv(someattr=cond ? true : false)") == "<div someattr=\"someattr\"></div>");
1315	assert(compile!("doctype html\n- auto cond = true;\ndiv(someattr=cond ? true : false)") 
1316		== "<!DOCTYPE html>\n<div someattr></div>");
1317	assert(compile!("doctype html\n- auto cond = false;\ndiv(someattr=cond ? true : false)") 
1318		== "<!DOCTYPE html>\n<div></div>");
1319
1320	// issue 510
1321	assert(compile!("pre.test\n\tfoo") == "<pre class=\"test\">\n\t<foo></foo></pre>");
1322	assert(compile!("pre.test.\n\tfoo") == "<pre class=\"test\">\nfoo</pre>");
1323	assert(compile!("pre.test. foo") == "<pre class=\"test\"></pre>");
1324	assert(compile!("pre().\n\tfoo") == "<pre>\nfoo</pre>");
1325	assert(compile!("pre#foo.test(data-img=\"sth\",class=\"meh\"). something\n\tmeh") ==
1326	       "<pre id=\"foo\" data-img=\"sth\" class=\"meh test\">\nmeh</pre>");
1327}
1328
1329
1330/**************************************************************************************************/
1331/* Compile time filters                                                                           */
1332/**************************************************************************************************/
1333
1334private string filterCSS(string text, size_t indent)
1335{
1336	auto lines = splitLines(text);
1337
1338	string indent_string = "\n";
1339	while( indent-- > 0 ) indent_string ~= '\t';
1340
1341	string ret = indent_string~"<style type=\"text/css\"><!--";
1342	indent_string = indent_string ~ '\t';
1343	foreach( ln; lines ) ret ~= indent_string ~ ln;
1344	indent_string = indent_string[0 .. $-1];
1345	ret ~= indent_string ~ "--></style>";
1346
1347	return ret;
1348}
1349
1350
1351private string filterJavaScript(string text, size_t indent)
1352{
1353	auto lines = splitLines(text);
1354
1355	string indent_string = "\n";
1356	while( indent-- > 0 ) indent_string ~= '\t';
1357
1358	string ret = indent_string[0 .. $-1]~"<script type=\"text/javascript\">";
1359	ret ~= indent_string~"//<![CDATA[";
1360	foreach( ln; lines ) ret ~= indent_string ~ ln;
1361	ret ~= indent_string ~ "//]]>"~indent_string[0 .. $-1]~"</script>";
1362
1363	return ret;
1364}
1365
1366private string filterMarkdown(string text, size_t)
1367{
1368	// TODO: indent
1369	return vibe.textfilter.markdown.filterMarkdown(text);
1370}
1371
1372private string filterHtmlEscape(string text, size_t)
1373{
1374	// TODO: indent
1375	return htmlEscape(text);
1376}