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