1 // Written in the D Programming Language 2 3 // embd 4 // Copyright (C) 2013 Nathan M. Swan 5 // Available under the MIT (Expat) License 6 7 /++ 8 + Low-level API for embedding D code into text. 9 + 10 + Copyright: Copyright © 2013 Nathan M. Swan 11 + License: MIT (Expat) License 12 + Authors: Nathan M. Swan 13 +/ 14 module embd; 15 16 import std.algorithm; 17 import std.array; 18 import std.conv; 19 import std.exception; 20 import std.string; 21 import std.utf; 22 23 public: 24 /// The object which manages the rendering of an embd template. 25 /// 26 /// To use, create a subclass, manually implement write, and 27 /// automatically implement render by mixin(renderer). 28 /// 29 /// This allows you to access fields and other methods of your subclass 30 /// from the template (which is generated to be within the body of render). 31 interface Context { 32 33 /// Write the content to whatever you are writing to. 34 /// 35 /// Params: 36 /// content = the text to write 37 /// evalCode = what evaluation character occured after the start delimiter, 38 /// dchar.init if static content. 39 /// 40 /// Example: 41 /// --- 42 /// content -> write("content", dchar.init); 43 /// <%= expr() %> -> write(expr(), '='); 44 /// --- 45 void write(string content, dchar evalCode); 46 47 /// Renders the template. Don't implement this manually, instead 48 /// mixin(renderer) in your subclass. 49 /// 50 /// Params: 51 /// embd_code = the embd template 52 /// embd_evalCodes = the allowed evaluation codes, passed to write 53 /// to signal how to postprocess the dynamic content 54 /// (e.g. whether to html escape or not) 55 /// embd_start = the delimeter signalling the start of embedded code 56 /// embd_end = the delimeter signalling the end of embedded code 57 void render(string embd_code, 58 const(dchar)[] embd_evalCodes, 59 string embd_start, string embd_end)(); 60 61 /// The render implementation to mixin to your subclass. 62 enum renderer = q{ 63 public void render(string embd_code, 64 const(dchar)[] embd_evalCodes=`=`, 65 string embd_start=`<%`, string embd_end=`%>` 66 )() { 67 mixin(embd__createRenderingCode(embd_code, embd_start, embd_end, embd_evalCodes)); 68 } 69 }; 70 } 71 72 73 version (Have_vibe_d) 74 { 75 import vibe.core.stream : OutputStream; 76 import vibe.stream.wrapper : streamOutputRange; 77 import vibe.http.server : HTTPServerResponse; 78 import diet.input; 79 import vibe.textfilter.html : filterHTMLEscape; 80 81 /** Renders an EMBD template to an output stream. 82 83 These functions provice means to render EMPD templates in a way similar 84 to the render!() function of vibe.d for rendering Diet templates. 85 86 Note that these functions are only available if "vibe-d" is available 87 as a dependency or if a "Have_vibe_d" version identifier is specified 88 manually. 89 90 Examples: 91 92 --- 93 string caption = "Hello, World!"; 94 //res.renderEmbd!("test.embd", caption)(); 95 res.bodyWriter.renderEmbdCompat!("test.embd", string, "caption")(caption); 96 --- 97 */ 98 @safe 99 void renderEmbd(string FILE, ALIASES...)(OutputStream dst) 100 { 101 mixin(diet.input.localAliasesMixin!(0, ALIASES)); 102 103 class LocalContext : Context { 104 OutputStream output__; 105 106 mixin(renderer); 107 108 void write(string content, dchar eval_code) 109 { 110 if (eval_code == '=') { 111 auto buf = streamOutputRange!1024(output__); 112 filterHTMLEscape(buf, content); 113 } else { 114 output__.write(content, false); 115 } 116 } 117 } 118 119 scope ctx = new LocalContext; 120 ctx.output__ = dst; 121 ctx.render!(import(FILE), `!=`, `<%`, `%>`)(); 122 } 123 124 /// ditto 125 @safe 126 void renderEmbd(string FILE, ALIASES...)(HTTPServerResponse res, string content_type = "text/html; charset=UTF-8") 127 { 128 res.contentType = content_type; 129 renderEmbd!(FILE, ALIASES)(res.bodyWriter); 130 } 131 132 /// ditto 133 @safe 134 void renderEmbdCompat(string FILE, TYPES_AND_NAMES...)(OutputStream dst, ...) 135 { 136 import core.vararg; 137 import std.variant; 138 mixin(localAliasesCompat!(0, TYPES_AND_NAMES)); 139 140 class LocalContext : Context { 141 OutputStream output__; 142 143 mixin(renderer); 144 145 void write(string content, dchar eval_code) 146 { 147 if (eval_code == '=') { 148 auto buf = streamOutputRange!1024(output__); 149 filterHTMLEscape(buf, content); 150 } else { 151 output__.write(content, false); 152 } 153 } 154 } 155 156 scope ctx = new LocalContext; 157 ctx.output__ = dst; 158 ctx.render!(import(FILE), `!=`, `<%`, `%>`)(); 159 } 160 } 161 162 163 private: 164 unittest { 165 static class MyContext : embd.Context { 166 uint until = 20; 167 168 void write(string content, dchar evalCode) { 169 import std.stdio; 170 std.stdio.write(content); 171 } 172 173 mixin(renderer); 174 } 175 176 auto ctx = new MyContext(); 177 ctx.render!(import("test.embd.html"), `=`)(); 178 } 179 180 // public so it can be accessed from the mixin 181 public string embd__createRenderingCode(string embd_code, 182 string embd_start, string embd_end, 183 const(dchar)[] embd_evalCodes) { 184 // convert to dstring for slicing 185 dstring inCode = embd_code.to!dstring(); 186 dstring startDelim = embd_start.to!dstring(); 187 dstring endDelim = embd_end.to!dstring(); 188 string outCode = ""; 189 190 // two states: static content and dynamic content 191 dstring staticBuffer = ""; 192 while (!inCode.empty) { 193 if (inCode.startsWith(startDelim)) { 194 outCode ~= `write(`~generateQuotesFor(staticBuffer)~`, dchar.init);`; 195 staticBuffer = ""; 196 outCode ~= getDynamicContent(inCode, startDelim, endDelim, embd_evalCodes); 197 } else { 198 staticBuffer ~= inCode.front; 199 inCode.popFront(); 200 } 201 } 202 if (staticBuffer.length) { 203 outCode ~= `write(`~generateQuotesFor(staticBuffer)~`, dchar.init);`; 204 } 205 206 return outCode.to!string(); 207 } 208 209 string getDynamicContent(ref dstring inCode, 210 dstring startDelim, dstring endDelim, 211 const(dchar)[] evalCodes) { 212 // TODO allow endDelim to appear in strings 213 void notEmpty() { 214 enforce(!inCode.empty, 215 format("Starting '%s' not matched by closing '%s'.", startDelim, endDelim)); 216 } 217 218 inCode = inCode[startDelim.length .. $]; 219 notEmpty(); 220 dchar evalCode = dchar.init; 221 if (evalCodes.canFind(inCode.front)) { 222 evalCode = inCode.front; 223 inCode.popFront(); 224 } 225 string outCode = ""; 226 while (true) { 227 notEmpty(); 228 if (inCode.startsWith(endDelim)) { 229 inCode = inCode[endDelim.length .. $]; 230 break; 231 } else { 232 outCode ~= inCode.front.to!string(); 233 inCode.popFront(); 234 } 235 } 236 237 if (evalCode == dchar.init) { 238 return outCode; 239 } else { 240 return format(`write(%s, '\u%.4x');`, outCode, evalCode); 241 } 242 } 243 244 unittest { 245 assert(generateQuotesFor(` `) == `x"20"c`); 246 assert(generateQuotesFor(`ẍ`) == `x"e1ba8d"c`); 247 } 248 249 string generateQuotesFor(dstring buffer) { 250 // convert to ubyte so it doesn't convert back into a range of dchars 251 return format(`x"%-(%.2x%)"c`, cast(immutable(ubyte)[])buffer.to!string()); 252 }