PageRenderTime 53ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/source/vibe/templ/diet.d

https://github.com/odis-project/vibe.d
D | 1104 lines | 863 code | 116 blank | 125 comment | 196 complexity | 4a6b19944562f6acf7aeaf2aca5472ed 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 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.variant;
  26. /*
  27. TODO:
  28. support string interpolations in filter blocks
  29. to!string and htmlEscape should not be used in conjunction with ~ at run time. instead,
  30. use filterHtmlEncode().
  31. */
  32. /**
  33. Parses the given diet template at compile time and writes the resulting
  34. HTML code into 'stream'.
  35. Note that this function currently suffers from multiple DMD bugs in conjunction with local
  36. variables passed as alias template parameters.
  37. */
  38. void compileDietFile(string template_file, ALIASES...)(OutputStream stream__)
  39. {
  40. // some imports to make available by default inside templates
  41. import vibe.http.common;
  42. import vibe.utils.string;
  43. pragma(msg, "Compiling diet template '"~template_file~"'...");
  44. static if( ALIASES.length > 0 ){
  45. pragma(msg, "Warning: using render!() or parseDietFile!() with aliases is unsafe,");
  46. pragma(msg, " please consider using renderCompat!()/parseDietFileCompat!()");
  47. pragma(msg, " until DMD is fully stable regarding local alias template arguments.");
  48. }
  49. //pragma(msg, localAliases!(0, ALIASES));
  50. mixin(localAliases!(0, ALIASES));
  51. // Generate the D source code for the diet template
  52. //pragma(msg, dietParser!template_file());
  53. mixin(dietParser!template_file);
  54. #line 66 "diet.d"
  55. static assert(__LINE__ == 66);
  56. }
  57. /// compatibility alias
  58. alias compileDietFile parseDietFile;
  59. /**
  60. Compatibility version of parseDietFile().
  61. This function should only be called indirectly through HTTPServerResponse.renderCompat().
  62. */
  63. void compileDietFileCompat(string template_file, TYPES_AND_NAMES...)(OutputStream stream__, ...)
  64. {
  65. compileDietFileCompatV!(template_file, TYPES_AND_NAMES)(stream__, _argptr, _arguments);
  66. }
  67. /// ditto
  68. void compileDietFileCompatV(string template_file, TYPES_AND_NAMES...)(OutputStream stream__, void* _argptr, TypeInfo[] _arguments)
  69. {
  70. // some imports to make available by default inside templates
  71. import vibe.http.common;
  72. import vibe.utils.string;
  73. import core.vararg;
  74. pragma(msg, "Compiling diet template '"~template_file~"' (compat)...");
  75. //pragma(msg, localAliasesCompat!(0, TYPES_AND_NAMES));
  76. mixin(localAliasesCompat!(0, TYPES_AND_NAMES));
  77. // Generate the D source code for the diet template
  78. //pragma(msg, dietParser!template_file());
  79. mixin(dietParser!template_file);
  80. #line 98 "diet.d"
  81. static assert(__LINE__ == 98);
  82. }
  83. /// compatibility alias
  84. alias compileDietFileCompat parseDietFileCompat;
  85. /**
  86. Generates a diet template compiler to use as a mixin.
  87. This can be used as an alternative to compileDietFile or compileDietFileCompat. It allows
  88. the template to use all symbols visible in the enclosing scope. In situations where many
  89. variables from the calling function's scope are used within the template, it can reduce the
  90. amount of code required for invoking the template.
  91. Note that even if this method of using diet templates can reduce the amount of source code. It
  92. is generally recommended to use compileDietFile(Compat) instead, as those
  93. facilitate a cleaner interface between D code and diet code by explicity documenting the
  94. symbols usable inside of the template and thus avoiding unwanted, hidden dependencies. A
  95. possible alternative for passing many variables is to pass a struct or class value to
  96. compileDietFile(Compat).
  97. Examples:
  98. ---
  99. void handleRequest(HTTPServerRequest req, HTTPServerResponse res)
  100. {
  101. int this_variable_is_automatically_visible_to_the_template;
  102. mixin(compileDietFileMixin!("index.dt", "res.bodyWriter"));
  103. }
  104. ---
  105. */
  106. template compileDietFileMixin(string template_file, string stream_variable)
  107. {
  108. enum compileDietFileMixin = "OutputStream stream__ = "~stream_variable~";\n" ~ dietParser!template_file;
  109. }
  110. /**
  111. Registers a new text filter for use in Diet templates.
  112. The filter will be available using :filtername inside of the template. The following filters are
  113. predefined: css, javascript, markdown
  114. */
  115. void registerDietTextFilter(string name, string function(string, int indent) filter)
  116. {
  117. s_filters[name] = filter;
  118. }
  119. /**************************************************************************************************/
  120. /* private functions */
  121. /**************************************************************************************************/
  122. private {
  123. enum string StreamVariableName = "stream__";
  124. string function(string, int indent)[string] s_filters;
  125. }
  126. static this()
  127. {
  128. registerDietTextFilter("css", &filterCSS);
  129. registerDietTextFilter("javascript", &filterJavaScript);
  130. registerDietTextFilter("markdown", &filterMarkdown);
  131. registerDietTextFilter("htmlescape", &filterHtmlEscape);
  132. }
  133. private @property string dietParser(string template_file)()
  134. {
  135. TemplateBlock[] files;
  136. readFileRec!(template_file)(files);
  137. auto compiler = DietCompiler(&files[0], &files, new BlockStore);
  138. return compiler.buildWriter();
  139. }
  140. /******************************************************************************/
  141. /* Reading of input files */
  142. /******************************************************************************/
  143. private struct TemplateBlock {
  144. string name;
  145. int mode = 0; // -1: prepend, 0: replace, 1: append
  146. string indentStyle;
  147. Line[] lines;
  148. }
  149. private class BlockStore {
  150. TemplateBlock[] blocks;
  151. }
  152. /// private
  153. private void readFileRec(string FILE, ALREADY_READ...)(ref TemplateBlock[] dst)
  154. {
  155. static if( !isPartOf!(FILE, ALREADY_READ)() ){
  156. enum LINES = removeEmptyLines(import(FILE), FILE);
  157. TemplateBlock ret;
  158. ret.name = FILE;
  159. ret.lines = LINES;
  160. ret.indentStyle = detectIndentStyle(ret.lines);
  161. enum DEPS = extractDependencies(LINES);
  162. dst ~= ret;
  163. readFilesRec!(DEPS, ALREADY_READ, FILE)(dst);
  164. }
  165. }
  166. /// private
  167. private void readFilesRec(alias FILES, ALREADY_READ...)(ref TemplateBlock[] dst)
  168. {
  169. static if( FILES.length > 0 ){
  170. readFileRec!(FILES[0], ALREADY_READ)(dst);
  171. readFilesRec!(FILES[1 .. $], ALREADY_READ, FILES[0])(dst);
  172. }
  173. }
  174. /// private
  175. private bool isPartOf(string str, STRINGS...)()
  176. {
  177. foreach( s; STRINGS )
  178. if( str == s )
  179. return true;
  180. return false;
  181. }
  182. private string[] extractDependencies(in Line[] lines)
  183. {
  184. string[] ret;
  185. foreach( ref ln; lines ){
  186. auto lnstr = ln.text.ctstrip();
  187. if( lnstr.startsWith("extends ") ) ret ~= lnstr[8 .. $].ctstrip() ~ ".dt";
  188. else if( lnstr.startsWith("include ") ) ret ~= lnstr[8 .. $].ctstrip() ~ ".dt";
  189. }
  190. return ret;
  191. }
  192. /******************************************************************************/
  193. /* The Diet compiler */
  194. /******************************************************************************/
  195. private class OutputContext {
  196. enum State {
  197. Code,
  198. String
  199. }
  200. State m_state = State.Code;
  201. string[] m_nodeStack;
  202. string m_result;
  203. Line m_line = Line(null, -1, null);
  204. void markInputLine(in ref Line line)
  205. {
  206. if( m_state == State.Code ){
  207. m_result ~= lineMarker(line);
  208. } else {
  209. m_line = Line(line.file, line.number, null);
  210. }
  211. }
  212. @property size_t stackSize() const { return m_nodeStack.length; }
  213. void pushNode(string str) { m_nodeStack ~= str; }
  214. void pushDummyNode() { pushNode("-"); }
  215. void popNodes(int next_indent_level)
  216. {
  217. // close all tags/blocks until we reach the level of the next line
  218. while( m_nodeStack.length > next_indent_level ){
  219. if( m_nodeStack[$-1][0] == '-' ){
  220. if( m_nodeStack[$-1].length > 1 ){
  221. writeCodeLine(m_nodeStack[$-1][1 .. $]);
  222. }
  223. } else if( m_nodeStack[$-1].length ){
  224. string str;
  225. if( m_nodeStack[$-1] != "</pre>" ){
  226. str = "\n";
  227. foreach( j; 0 .. m_nodeStack.length-1 ) if( m_nodeStack[j][0] != '-' ) str ~= "\t";
  228. }
  229. str ~= m_nodeStack[$-1];
  230. writeString(str);
  231. }
  232. m_nodeStack.length--;
  233. }
  234. }
  235. // TODO: avoid runtime allocations by replacing htmlEscape/_toString calls with
  236. // filtering functions
  237. void writeRawString(string str) { enterState(State.String); m_result ~= str; }
  238. void writeString(string str) { writeRawString(dstringEscape(str)); }
  239. void writeStringHtmlEscaped(string str) { writeString(htmlEscape(str)); }
  240. void writeStringExpr(string str) { writeCodeLine(StreamVariableName~".write("~str~");"); }
  241. void writeStringExprHtmlEscaped(string str) { writeStringExpr("htmlEscape("~str~")"); }
  242. void writeStringExprHtmlAttribEscaped(string str) { writeStringExpr("htmlAttribEscape("~str~")"); }
  243. void writeExpr(string str) { writeStringExpr("_toString("~str~")"); }
  244. void writeExprHtmlEscaped(string str) { writeStringExprHtmlEscaped("_toString("~str~")"); }
  245. void writeExprHtmlAttribEscaped(string str) { writeStringExprHtmlAttribEscaped("_toString("~str~")"); }
  246. void writeCodeLine(string stmt)
  247. {
  248. if( !enterState(State.Code) )
  249. m_result ~= lineMarker(m_line);
  250. m_result ~= stmt ~ "\n";
  251. }
  252. private bool enterState(State state)
  253. {
  254. if( state == m_state ) return false;
  255. if( state != m_state.Code ) enterState(State.Code);
  256. final switch(state){
  257. case State.Code:
  258. if( m_state == State.String ) m_result ~= "\", false);\n";
  259. else m_result ~= ", false);\n";
  260. m_result ~= lineMarker(m_line);
  261. break;
  262. case State.String:
  263. m_result ~= StreamVariableName ~ ".write(\"";
  264. break;
  265. }
  266. m_state = state;
  267. return true;
  268. }
  269. }
  270. private struct DietCompiler {
  271. private {
  272. size_t m_lineIndex = 0;
  273. TemplateBlock* m_block;
  274. TemplateBlock[]* m_files;
  275. BlockStore m_blocks;
  276. }
  277. @property ref string indentStyle() { return m_block.indentStyle; }
  278. @property size_t lineCount() { return m_block.lines.length; }
  279. ref Line line(size_t ln) { return m_block.lines[ln]; }
  280. ref Line currLine() { return m_block.lines[m_lineIndex]; }
  281. ref string currLineText() { return m_block.lines[m_lineIndex].text; }
  282. Line[] lineRange(size_t from, size_t to) { return m_block.lines[from .. to]; }
  283. @disable this();
  284. this(TemplateBlock* block, TemplateBlock[]* files, BlockStore blocks)
  285. {
  286. m_block = block;
  287. m_files = files;
  288. m_blocks = blocks;
  289. }
  290. string buildWriter()
  291. {
  292. auto output = new OutputContext;
  293. buildWriter(output, 0);
  294. assert(output.m_nodeStack.length == 0, "Template writer did not consume all nodes!?");
  295. return output.m_result;
  296. }
  297. void buildWriter(OutputContext output, int base_level)
  298. {
  299. assert(m_blocks !is null, "Trying to compile template with no blocks specified.");
  300. while(true){
  301. if( lineCount == 0 ) return;
  302. auto firstline = line(m_lineIndex);
  303. auto firstlinetext = firstline.text;
  304. if( firstlinetext.startsWith("extends ") ){
  305. string layout_file = firstlinetext[8 .. $].ctstrip() ~ ".dt";
  306. auto extfile = getFile(layout_file);
  307. m_lineIndex++;
  308. // extract all blocks
  309. while( m_lineIndex < lineCount ){
  310. TemplateBlock subblock;
  311. // read block header
  312. string blockheader = line(m_lineIndex).text;
  313. size_t spidx = 0;
  314. auto mode = skipIdent(line(m_lineIndex).text, spidx, "");
  315. assertp(spidx > 0, "Expected block/append/prepend.");
  316. subblock.name = blockheader[spidx .. $].ctstrip();
  317. if( mode == "block" ) subblock.mode = 0;
  318. else if( mode == "append" ) subblock.mode = 1;
  319. else if( mode == "prepend" ) subblock.mode = -1;
  320. else assertp(false, "Expected block/append/prepend.");
  321. m_lineIndex++;
  322. // skip to next block
  323. auto block_start = m_lineIndex;
  324. while( m_lineIndex < lineCount ){
  325. auto lvl = indentLevel(line(m_lineIndex).text, indentStyle, false);
  326. if( lvl == 0 ) break;
  327. m_lineIndex++;
  328. }
  329. // append block to compiler
  330. subblock.lines = lineRange(block_start, m_lineIndex);
  331. subblock.indentStyle = indentStyle;
  332. m_blocks.blocks ~= subblock;
  333. //output.writeString("<!-- found block "~subblock.name~" in "~line(0).file ~ "-->\n");
  334. }
  335. // change to layout file and start over
  336. m_block = extfile;
  337. m_lineIndex = 0;
  338. } else {
  339. auto start_indent_level = indentLevel(firstlinetext, indentStyle);
  340. //assertp(start_indent_level == 0, "Indentation must start at level zero.");
  341. buildBodyWriter(output, base_level, start_indent_level);
  342. break;
  343. }
  344. }
  345. output.enterState(OutputContext.State.Code);
  346. }
  347. private void buildBodyWriter(OutputContext output, int base_level, int start_indent_level)
  348. {
  349. assert(m_blocks !is null, "Trying to compile template body with no blocks specified.");
  350. assertp(output.stackSize >= base_level);
  351. int computeNextIndentLevel(){
  352. return (m_lineIndex+1 < lineCount ? indentLevel(line(m_lineIndex+1).text, indentStyle, false) - start_indent_level : 0) + base_level;
  353. }
  354. for( ; m_lineIndex < lineCount; m_lineIndex++ ){
  355. auto curline = line(m_lineIndex);
  356. output.markInputLine(curline);
  357. auto level = indentLevel(curline.text, indentStyle) - start_indent_level + base_level;
  358. assertp(level <= output.stackSize+1);
  359. auto ln = unindent(curline.text, indentStyle);
  360. assertp(ln.length > 0);
  361. int next_indent_level = computeNextIndentLevel();
  362. assertp(output.stackSize >= level, cttostring(output.stackSize) ~ ">=" ~ cttostring(level));
  363. assertp(next_indent_level <= level+1, "The next line is indented by more than one level deeper. Please unindent accordingly.");
  364. if( ln[0] == '-' ){ // embedded D code
  365. assertp(ln[$-1] != '{', "Use indentation to nest D statements instead of braces.");
  366. output.writeCodeLine(ln[1 .. $] ~ "{");
  367. output.pushNode("-}");
  368. } else if( ln[0] == '|' ){ // plain text node
  369. buildTextNodeWriter(output, ln[1 .. ln.length], level);
  370. } else if( ln[0] == ':' ){ // filter node (filtered raw text)
  371. // find all child lines
  372. size_t next_tag = m_lineIndex+1;
  373. while( next_tag < lineCount &&
  374. indentLevel(line(next_tag).text, indentStyle, false) - start_indent_level > level-base_level )
  375. {
  376. next_tag++;
  377. }
  378. buildFilterNodeWriter(output, ln, curline.number, level + start_indent_level - base_level,
  379. lineRange(m_lineIndex+1, next_tag));
  380. // skip to the next tag
  381. //output.pushDummyNode();
  382. m_lineIndex = next_tag-1;
  383. next_indent_level = computeNextIndentLevel();
  384. } else {
  385. size_t j = 0;
  386. auto tag = isAlpha(ln[0]) || ln[0] == '/' ? skipIdent(ln, j, "/:-_") : "div";
  387. if( ln.startsWith("!!! ") ) tag = "!!!";
  388. switch(tag){
  389. default:
  390. buildHtmlNodeWriter(output, tag, ln[j .. $], level, next_indent_level > level);
  391. break;
  392. case "!!!": // HTML Doctype header
  393. buildSpecialTag(output, "!DOCTYPE html", level);
  394. break;
  395. case "//": // HTML comment
  396. skipWhitespace(ln, j);
  397. output.writeString("<!-- " ~ htmlEscape(ln[j .. $]));
  398. output.pushNode(" -->");
  399. break;
  400. case "//-": // non-output comment
  401. // find all child lines
  402. size_t next_tag = m_lineIndex+1;
  403. while( next_tag < lineCount &&
  404. indentLevel(line(next_tag).text, indentStyle, false) - start_indent_level > level-base_level )
  405. {
  406. next_tag++;
  407. }
  408. // skip to the next tag
  409. m_lineIndex = next_tag-1;
  410. next_indent_level = computeNextIndentLevel();
  411. break;
  412. case "//if": // IE conditional comment
  413. skipWhitespace(ln, j);
  414. buildSpecialTag(output, "!--[if "~ln[j .. $]~"]", level);
  415. output.pushNode("<![endif]-->");
  416. break;
  417. case "block": // Block insertion place
  418. assertp(next_indent_level <= level, "Child elements for 'include' are not supported.");
  419. output.pushDummyNode();
  420. auto block = getBlock(ln[6 .. $].ctstrip());
  421. if( block ){
  422. output.writeString("<!-- using block " ~ ln[6 .. $] ~ " in " ~ curline.file ~ "-->");
  423. if( block.mode == 1 ){
  424. // output defaults
  425. }
  426. auto blockcompiler = new DietCompiler(block, m_files, m_blocks);
  427. /*blockcompiler.m_block = block;
  428. blockcompiler.m_blocks = m_blocks;*/
  429. blockcompiler.buildWriter(output, cast(int)output.m_nodeStack.length);
  430. if( block.mode == -1 ){
  431. // output defaults
  432. }
  433. } else {
  434. // output defaults
  435. output.writeString("<!-- Default block " ~ ln[6 .. $] ~ " in " ~ curline.file ~ "-->");
  436. }
  437. break;
  438. case "include": // Diet file include
  439. assertp(next_indent_level <= level, "Child elements for 'include' are not supported.");
  440. auto filename = ln[8 .. $].ctstrip() ~ ".dt";
  441. auto file = getFile(filename);
  442. auto includecompiler = new DietCompiler(file, m_files, m_blocks);
  443. //includecompiler.m_blocks = m_blocks;
  444. includecompiler.buildWriter(output, level);
  445. break;
  446. case "script":
  447. case "style":
  448. if( tag == "script" && next_indent_level <= level){
  449. buildHtmlNodeWriter(output, tag, ln[j .. $], level, false);
  450. } else {
  451. // pass all child lines to buildRawTag and continue with the next sibling
  452. size_t next_tag = m_lineIndex+1;
  453. while( next_tag < lineCount &&
  454. indentLevel(line(next_tag).text, indentStyle, false) - start_indent_level > level-base_level )
  455. {
  456. next_tag++;
  457. }
  458. buildRawNodeWriter(output, tag, ln[j .. $], level, base_level,
  459. lineRange(m_lineIndex+1, next_tag));
  460. m_lineIndex = next_tag-1;
  461. next_indent_level = computeNextIndentLevel();
  462. }
  463. break;
  464. case "each":
  465. case "for":
  466. case "if":
  467. case "unless":
  468. case "mixin":
  469. assertp(false, "'"~tag~"' is not supported.");
  470. break;
  471. }
  472. }
  473. output.popNodes(next_indent_level);
  474. }
  475. }
  476. private void buildTextNodeWriter(OutputContext output, in string textline, int level)
  477. {
  478. output.writeString("\n");
  479. if( textline.length >= 1 && textline[0] == '=' ){
  480. output.writeExprHtmlEscaped(textline[1 .. $]);
  481. } else if( textline.length >= 2 && textline[0 .. 2] == "!=" ){
  482. output.writeExpr(textline[2 .. $]);
  483. } else {
  484. buildInterpolatedString(output, textline);
  485. }
  486. output.pushDummyNode();
  487. }
  488. private void buildHtmlNodeWriter(OutputContext output, in ref string tag, in string line, int level, bool has_child_nodes)
  489. {
  490. // parse the HTML tag, leaving any trailing text as line[i .. $]
  491. size_t i;
  492. Tuple!(string, string)[] attribs;
  493. parseHtmlTag(line, i, attribs);
  494. // determine if we need a closing tag
  495. bool is_singular_tag = false;
  496. switch(tag){
  497. case "area", "base", "basefont", "br", "col", "embed", "frame", "hr", "img", "input",
  498. "keygen", "link", "meta", "param", "source", "track", "wbr":
  499. is_singular_tag = true;
  500. break;
  501. default:
  502. }
  503. assertp(!(is_singular_tag && has_child_nodes), "Singular HTML element '"~tag~"' may not have children.");
  504. // opening tag
  505. buildHtmlTag(output, tag, level, attribs, is_singular_tag);
  506. // parse any text contents (either using "= code" or as plain text)
  507. string textstring;
  508. bool textstring_isdynamic = true;
  509. if( i < line.length && line[i] == '=' ){
  510. output.writeExprHtmlEscaped(ctstrip(line[i+1 .. line.length]));
  511. } else if( i+1 < line.length && line[i .. i+2] == "!=" ){
  512. output.writeExpr(ctstrip(line[i+2 .. line.length]));
  513. } else {
  514. if( hasInterpolations(line[i .. line.length]) ){
  515. buildInterpolatedString(output, line[i .. line.length]);
  516. } else {
  517. output.writeRawString(sanitizeEscaping(line[i .. line.length]));
  518. }
  519. }
  520. // closing tag
  521. if( has_child_nodes ) output.pushNode("</"~tag~">");
  522. else if( !is_singular_tag ) output.writeString("</" ~ tag ~ ">");
  523. }
  524. private void buildRawNodeWriter(OutputContext output, in ref string tag, in string tagline, int level,
  525. int base_level, in Line[] lines)
  526. {
  527. // parse the HTML tag leaving any trailing text as tagline[i .. $]
  528. size_t i;
  529. Tuple!(string, string)[] attribs;
  530. parseHtmlTag(tagline, i, attribs);
  531. // write the tag
  532. buildHtmlTag(output, tag, level, attribs, false);
  533. string indent_string = "\t";
  534. foreach( j; 0 .. level ) if( output.m_nodeStack[j][0] != '-' ) indent_string ~= "\t";
  535. // write the block contents wrapped in a CDATA for old browsers
  536. if( tag == "script" ) output.writeString("\n"~indent_string~"//<![CDATA[\n");
  537. else output.writeString("\n"~indent_string~"<!--\n");
  538. // write out all lines
  539. void writeLine(string str){
  540. if( !hasInterpolations(str) ){
  541. output.writeString(indent_string ~ str ~ "\n");
  542. } else {
  543. output.writeString(indent_string);
  544. buildInterpolatedString(output, str);
  545. }
  546. }
  547. if( i < tagline.length ) writeLine(tagline[i .. $]);
  548. foreach( j; 0 .. lines.length ){
  549. // remove indentation
  550. string lnstr = lines[j].text[(level-base_level+1)*indentStyle.length .. $];
  551. writeLine(lnstr);
  552. }
  553. if( tag == "script" ) output.writeString(indent_string~"//]]>\n");
  554. else output.writeString(indent_string~"-->\n");
  555. output.writeString(indent_string[0 .. $-1] ~ "</" ~ tag ~ ">");
  556. }
  557. private void buildFilterNodeWriter(OutputContext output, in ref string tagline, int tagline_number,
  558. int indent, in Line[] lines)
  559. {
  560. // find all filters
  561. size_t j = 0;
  562. string[] filters;
  563. do {
  564. j++;
  565. filters ~= skipIdent(tagline, j);
  566. skipWhitespace(tagline, j);
  567. } while( j < tagline.length && tagline[j] == ':' );
  568. // assemble child lines to one string
  569. string content = tagline[j .. $];
  570. int lc = content.length ? tagline_number : tagline_number+1;
  571. foreach( i; 0 .. lines.length ){
  572. while( lc < lines[i].number ){ // DMDBUG: while(lc++ < lines[i].number) silently loops and only executes the last iteration
  573. content ~= '\n';
  574. lc++;
  575. }
  576. content ~= lines[i].text[(indent+1)*indentStyle.length .. $];
  577. }
  578. // compile-time filter whats possible
  579. filter_loop:
  580. foreach_reverse( f; filters ){
  581. switch(f){
  582. default: break filter_loop;
  583. case "css": content = filterCSS(content, indent); break;
  584. case "javascript": content = filterJavaScript(content, indent); break;
  585. case "markdown": content = filterMarkdown(content, indent); break;
  586. }
  587. filters.length = filters.length-1;
  588. }
  589. // the rest of the filtering will happen at run time
  590. string filter_expr;
  591. foreach_reverse( flt; filters ) filter_expr ~= "s_filters[\""~dstringEscape(flt)~"\"](";
  592. filter_expr ~= "\"" ~ dstringEscape(content) ~ "\"";
  593. foreach( i; 0 .. filters.length ) filter_expr ~= ", "~cttostring(indent)~")";
  594. output.writeStringExpr(filter_expr);
  595. }
  596. private void parseHtmlTag(in ref string line, out size_t i, out Tuple!(string, string)[] attribs)
  597. {
  598. i = 0;
  599. string id;
  600. string classes;
  601. // parse #id and .classes
  602. while( i < line.length ){
  603. if( line[i] == '#' ){
  604. i++;
  605. assertp(id.length == 0, "Id may only be set once.");
  606. id = skipIdent(line, i, "-_");
  607. } else if( line[i] == '.' ){
  608. i++;
  609. auto cls = skipIdent(line, i, "-_");
  610. if( classes.length == 0 ) classes = cls;
  611. else classes ~= " " ~ cls;
  612. } else break;
  613. }
  614. // put #id and .classes into the attribs list
  615. if( id.length ) attribs ~= tuple("id", '"'~id~'"');
  616. // parse other attributes
  617. if( i < line.length && line[i] == '(' ){
  618. i++;
  619. string attribstring = skipUntilClosingClamp(line, i);
  620. parseAttributes(attribstring, attribs);
  621. i++;
  622. }
  623. // add special attribute for extra classes that is handled by buildHtmlTag
  624. if( classes.length ){
  625. bool has_class = false;
  626. foreach( a; attribs )
  627. if( a[0] == "class" ){
  628. has_class = true;
  629. break;
  630. }
  631. if( has_class ) attribs ~= tuple("$class", classes);
  632. else attribs ~= tuple("class", "\"" ~ classes ~ "\"");
  633. }
  634. // skip until the optional tag text contents begin
  635. skipWhitespace(line, i);
  636. }
  637. private void buildHtmlTag(OutputContext output, in ref string tag, int level, ref Tuple!(string, string)[] attribs, bool is_singular_tag)
  638. {
  639. output.writeString("\n");
  640. assertp(output.stackSize >= level);
  641. foreach( j; 0 .. level ) if( output.m_nodeStack[j][0] != '-' ) output.writeString("\t");
  642. output.writeString("<" ~ tag);
  643. foreach( att; attribs ){
  644. if( att[0][0] == '$' ) continue; // ignore special attributes
  645. if( isStringLiteral(att[1]) ){
  646. output.writeString(" "~att[0]~"=\"");
  647. if( !hasInterpolations(att[1]) ) output.writeString(htmlAttribEscape(dstringUnescape(att[1][1 .. $-1])));
  648. else buildInterpolatedString(output, att[1][1 .. $-1], true);
  649. // output extra classes given as .class
  650. if( att[0] == "class" ){
  651. foreach( a; attribs )
  652. if( a[0] == "$class" ){
  653. output.writeString(" " ~ a[1]);
  654. break;
  655. }
  656. }
  657. output.writeString("\"");
  658. } else {
  659. output.writeCodeLine("static if(is(typeof("~att[1]~") == bool)){ if("~att[1]~"){");
  660. output.writeString(` `~att[0]~`="`~att[0]~`"`);
  661. output.writeCodeLine("}} else static if(is(typeof("~att[1]~") == string[])){\n");
  662. output.writeString(` `~att[0]~`="`);
  663. output.writeExprHtmlAttribEscaped(`join(`~att[1]~`, " ")`);
  664. output.writeString(`"`);
  665. output.writeCodeLine("} else {");
  666. output.writeString(` `~att[0]~`="`);
  667. output.writeExprHtmlAttribEscaped(att[1]);
  668. output.writeString(`"`);
  669. output.writeCodeLine("}");
  670. }
  671. }
  672. output.writeString(is_singular_tag ? "/>" : ">");
  673. }
  674. private void parseAttributes(in ref string str, ref Tuple!(string, string)[] attribs)
  675. {
  676. size_t i = 0;
  677. skipWhitespace(str, i);
  678. while( i < str.length ){
  679. string name = skipIdent(str, i, "-:");
  680. string value;
  681. skipWhitespace(str, i);
  682. if( str[i] == '=' ){
  683. i++;
  684. skipWhitespace(str, i);
  685. assertp(i < str.length, "'=' must be followed by attribute string.");
  686. value = skipExpression(str, i);
  687. if( isStringLiteral(value) && value[0] == '\'' ){
  688. value = '"' ~ value[1 .. $-1] ~ '"';
  689. }
  690. } else value = "true";
  691. assertp(i == str.length || str[i] == ',', "Unexpected text following attribute: '"~str[0..i]~"' ('"~str[i..$]~"')");
  692. if( i < str.length ){
  693. i++;
  694. skipWhitespace(str, i);
  695. }
  696. attribs ~= tuple(name, value);
  697. }
  698. }
  699. private bool hasInterpolations(in char[] str)
  700. {
  701. size_t i = 0;
  702. while( i < str.length ){
  703. if( str[i] == '\\' ){
  704. i += 2;
  705. continue;
  706. }
  707. if( i+1 < str.length && (str[i] == '#' || str[i] == '!') ){
  708. if( str[i+1] == str[i] ){
  709. i += 2;
  710. continue;
  711. } else if( str[i+1] == '{' ){
  712. return true;
  713. }
  714. }
  715. i++;
  716. }
  717. return false;
  718. }
  719. private void buildInterpolatedString(OutputContext output, string str, bool escape_quotes = false)
  720. {
  721. size_t start = 0, i = 0;
  722. while( i < str.length ){
  723. // check for escaped characters
  724. if( str[i] == '\\' ){
  725. if( i > start ) output.writeString(str[start .. i]);
  726. output.writeRawString(sanitizeEscaping(str[i .. i+2]));
  727. i += 2;
  728. start = i;
  729. continue;
  730. }
  731. if( (str[i] == '#' || str[i] == '!') && i+1 < str.length ){
  732. bool escape = str[i] == '#';
  733. if( i > start ){
  734. output.writeString(str[start .. i]);
  735. start = i;
  736. }
  737. assertp(str[i+1] != str[i], "Please use \\ to escape # or ! instead of ## or !!.");
  738. if( str[i+1] == '{' ){
  739. i += 2;
  740. auto expr = dstringUnescape(skipUntilClosingBrace(str, i));
  741. if( escape && !escape_quotes ) output.writeExprHtmlEscaped(expr);
  742. else if( escape ) output.writeExprHtmlAttribEscaped(expr);
  743. else output.writeExpr(expr);
  744. i++;
  745. start = i;
  746. } else i++;
  747. } else i++;
  748. }
  749. if( i > start ) output.writeString(str[start .. i]);
  750. }
  751. private string skipIdent(in ref string s, ref size_t idx, string additional_chars = null)
  752. {
  753. size_t start = idx;
  754. while( idx < s.length ){
  755. if( isAlpha(s[idx]) ) idx++;
  756. else if( start != idx && s[idx] >= '0' && s[idx] <= '9' ) idx++;
  757. else {
  758. bool found = false;
  759. foreach( ch; additional_chars )
  760. if( s[idx] == ch ){
  761. found = true;
  762. idx++;
  763. break;
  764. }
  765. if( !found ){
  766. assertp(start != idx, "Expected identifier but got '"~s[idx]~"'.");
  767. return s[start .. idx];
  768. }
  769. }
  770. }
  771. assertp(start != idx, "Expected identifier but got nothing.");
  772. return s[start .. idx];
  773. }
  774. private string skipWhitespace(in ref string s, ref size_t idx)
  775. {
  776. size_t start = idx;
  777. while( idx < s.length ){
  778. if( s[idx] == ' ' ) idx++;
  779. else break;
  780. }
  781. return s[start .. idx];
  782. }
  783. private string skipUntilClosingBrace(in ref string s, ref size_t idx)
  784. {
  785. int level = 0;
  786. auto start = idx;
  787. while( idx < s.length ){
  788. if( s[idx] == '{' ) level++;
  789. else if( s[idx] == '}' ) level--;
  790. if( level < 0 ) return s[start .. idx];
  791. idx++;
  792. }
  793. assertp(false, "Missing closing brace");
  794. assert(false);
  795. }
  796. private string skipUntilClosingClamp(in ref string s, ref size_t idx)
  797. {
  798. int level = 0;
  799. auto start = idx;
  800. while( idx < s.length ){
  801. if( s[idx] == '(' ) level++;
  802. else if( s[idx] == ')' ) level--;
  803. if( level < 0 ) return s[start .. idx];
  804. idx++;
  805. }
  806. assertp(false, "Missing closing clamp");
  807. assert(false);
  808. }
  809. private string skipAttribString(in ref string s, ref size_t idx, char delimiter)
  810. {
  811. size_t start = idx;
  812. while( idx < s.length ){
  813. if( s[idx] == '\\' ){
  814. // pass escape character through - will be handled later by buildInterpolatedString
  815. idx++;
  816. assertp(idx < s.length, "'\\' must be followed by something (escaped character)!");
  817. } else if( s[idx] == delimiter ) break;
  818. idx++;
  819. }
  820. return s[start .. idx];
  821. }
  822. private string skipExpression(in ref string s, ref size_t idx)
  823. {
  824. string clamp_stack;
  825. size_t start = idx;
  826. while( idx < s.length ){
  827. switch( s[idx] ){
  828. default: break;
  829. case ',':
  830. if( clamp_stack.length == 0 )
  831. return s[start .. idx];
  832. break;
  833. case '"', '\'':
  834. idx++;
  835. skipAttribString(s, idx, s[idx-1]);
  836. break;
  837. case '(': clamp_stack ~= ')'; break;
  838. case '[': clamp_stack ~= ']'; break;
  839. case '{': clamp_stack ~= '}'; break;
  840. case ')', ']', '}':
  841. if( s[idx] == ')' && clamp_stack.length == 0 )
  842. return s[start .. idx];
  843. assertp(clamp_stack.length > 0 && clamp_stack[$-1] == s[idx],
  844. "Unexpected '"~s[idx]~"'");
  845. clamp_stack.length--;
  846. break;
  847. }
  848. idx++;
  849. }
  850. assertp(clamp_stack.length == 0, "Expected '"~clamp_stack[$-1]~"' before end of attribute expression.");
  851. return s[start .. $];
  852. }
  853. private string unindent(in ref string str, in ref string indent)
  854. {
  855. size_t lvl = indentLevel(str, indent);
  856. return str[lvl*indent.length .. $];
  857. }
  858. private int indentLevel(in ref string s, in ref string indent, bool strict = true)
  859. {
  860. if( indent.length == 0 ) return 0;
  861. assertp(!strict || (s[0] != ' ' && s[0] != '\t') || s[0] == indent[0],
  862. "Indentation style is inconsistent with previous lines.");
  863. int l = 0;
  864. while( l+indent.length <= s.length && s[l .. l+indent.length] == indent )
  865. l += cast(int)indent.length;
  866. assertp(!strict || s[l] != ' ', "Indent is not a multiple of '"~indent~"'");
  867. return l / cast(int)indent.length;
  868. }
  869. private int indentLevel(in ref Line[] ln, string indent)
  870. {
  871. return ln.length == 0 ? 0 : indentLevel(ln[0].text, indent);
  872. }
  873. private void assertp(bool cond, lazy string text = null, string file = __FILE__, int cline = __LINE__)
  874. {
  875. Line ln;
  876. if( m_lineIndex < lineCount ) ln = line(m_lineIndex);
  877. assert(cond, "template "~ln.file~" line "~cttostring(ln.number)~": "~text~"("~file~":"~cttostring(cline)~")");
  878. }
  879. private TemplateBlock* getFile(string filename)
  880. {
  881. foreach( i; 0 .. m_files.length )
  882. if( (*m_files)[i].name == filename )
  883. return &(*m_files)[i];
  884. assertp(false, "Bug: include input file "~filename~" not found in internal list!?");
  885. assert(false);
  886. }
  887. private TemplateBlock* getBlock(string name)
  888. {
  889. foreach( i; 0 .. m_blocks.blocks.length )
  890. if( m_blocks.blocks[i].name == name )
  891. return &m_blocks.blocks[i];
  892. return null;
  893. }
  894. }
  895. /// private
  896. private void buildSpecialTag(OutputContext output, string tag, int level)
  897. {
  898. output.writeString("\n");
  899. foreach( j; 0 .. level ) if( output.m_nodeStack[j][0] != '-' ) output.writeString("\t");
  900. output.writeString("<" ~ tag ~ ">");
  901. }
  902. private bool isStringLiteral(string str)
  903. {
  904. size_t i = 0;
  905. while( i < str.length && (str[i] == ' ' || str[i] == '\t') ) i++;
  906. if( i >= str.length ) return false;
  907. char delimiter = str[i];
  908. if( delimiter != '"' && delimiter != '\'' ) return false;
  909. while( i < str.length && str[i] != delimiter ){
  910. if( str[i] == '\\' ) i++;
  911. i++;
  912. }
  913. return i < str.length;
  914. }
  915. /// Internal function used for converting an interpolation expression to string
  916. string _toString(T)(T v)
  917. {
  918. static if( is(T == string) ) return v;
  919. else static if( __traits(compiles, v.opCast!string()) ) return cast(string)v;
  920. else static if( __traits(compiles, v.toString()) ) return v.toString();
  921. else return to!string(v);
  922. }
  923. /**************************************************************************************************/
  924. /* Compile time filters */
  925. /**************************************************************************************************/
  926. private string filterCSS(string text, int indent)
  927. {
  928. auto lines = splitLines(text);
  929. string indent_string = "\n";
  930. while( indent-- > 0 ) indent_string ~= '\t';
  931. string ret = indent_string~"<style type=\"text/css\"><!--";
  932. indent_string = indent_string ~ '\t';
  933. foreach( ln; lines ) ret ~= indent_string ~ ln;
  934. indent_string = indent_string[0 .. $-1];
  935. ret ~= indent_string ~ "--></style>";
  936. return ret;
  937. }
  938. private string filterJavaScript(string text, int indent)
  939. {
  940. auto lines = splitLines(text);
  941. string indent_string = "\n";
  942. while( indent-- >= 0 ) indent_string ~= '\t';
  943. string ret = indent_string[0 .. $-1]~"<script type=\"text/javascript\">";
  944. ret ~= indent_string~"//<![CDATA[";
  945. foreach( ln; lines ) ret ~= indent_string ~ ln;
  946. ret ~= indent_string ~ "//]]>"~indent_string[0 .. $-1]~"</script>";
  947. return ret;
  948. }
  949. private string filterMarkdown(string text, int indent)
  950. {
  951. return vibe.textfilter.markdown.filterMarkdown(text);
  952. }
  953. private string filterHtmlEscape(string text, int indent)
  954. {
  955. return htmlEscape(text);
  956. }