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;
76     import vibe.templ.utils;
77     import vibe.textfilter.html;
78 
79     /** Renders an EMBD template to an output stream.
80 
81         These functions provice means to render EMPD templates in a way similar
82         to the render!() function of vibe.d for rendering Diet templates.
83 
84         Note that these functions are only available if "vibe-d" is available
85         as a dependency or if a "Have_vibe_d" version identifier is specified
86         manually.
87 
88         Examples:
89 
90             ---
91             string caption = "Hello, World!";
92             //res.renderEmbd!("test.embd", caption)();
93             res.bodyWriter.renderEmbdCompat!("test.embd", string, "caption")(caption);
94             ---
95     */
96     void renderEmbd(string FILE, ALIASES...)(OutputStream dst)
97     {
98 
99         mixin(vibe.templ.utils.localAliases!(0, ALIASES));
100 
101         class LocalContext : Context {
102             OutputStream output__;
103 
104             mixin(renderer);
105 
106             void write(string content, dchar eval_code)
107             {
108                 if (eval_code == '=')
109                     filterHtmlEscape(output__, content);
110                 else
111                     output__.write(content, false);
112             }
113         }
114 
115         scope ctx = new LocalContext;
116         ctx.output__ = dst;
117         ctx.render!(import(FILE), `!=`, `<%`, `%>`)();
118     }
119 
120     /// ditto
121     void renderEmbd(string FILE, ALIASES...)(HTTPServerResponse res, string content_type = "text/html; charset=UTF-8")
122     {
123         res.contentType = content_type;
124         renderEmbd!(FILE, ALIASES)(res.bodyWriter);
125     }
126 
127     /// ditto
128     void renderEmbdCompat(string FILE, TYPES_AND_NAMES...)(OutputStream dst, ...)
129     {
130         import core.vararg;
131         import std.variant;
132         mixin(localAliasesCompat!(0, TYPES_AND_NAMES));
133 
134         class LocalContext : Context {
135             OutputStream output__;
136 
137             mixin(renderer);
138 
139             void write(string content, dchar eval_code)
140             {
141                 if (eval_code == '=')
142                     filterHtmlEscape(output__, content);
143                 else
144                     output__.write(content, false);
145             }
146         }
147 
148         scope ctx = new LocalContext;
149         ctx.output__ = dst;
150         ctx.render!(import(FILE), `!=`, `<%`, `%>`)();
151     }
152 }
153 
154 
155 private:
156 unittest {
157     static class MyContext : embd.Context {
158         uint until = 20;
159 
160         void write(string content, dchar evalCode) {
161             import std.stdio;
162             write(content);
163         }
164 
165         mixin(renderer);
166     }
167 
168     auto ctx = new MyContext();
169     ctx.render!(import("test.embd.html"), `=`)();
170 }
171 
172 // public so it can be accessed from the mixin
173 public string embd__createRenderingCode(string embd_code, 
174                                         string embd_start, string embd_end, 
175                                         const(dchar)[] embd_evalCodes) {
176     // convert to dstring for slicing
177     dstring inCode = embd_code.to!dstring();
178     dstring startDelim = embd_start.to!dstring();
179     dstring endDelim = embd_end.to!dstring();
180     string outCode = "";
181 
182     // two states: static content and dynamic content
183     dstring staticBuffer = "";
184     while (!inCode.empty) {
185         if (inCode.startsWith(startDelim)) {
186             outCode ~= `write(`~generateQuotesFor(staticBuffer)~`, dchar.init);`;
187             staticBuffer = "";
188             outCode ~= getDynamicContent(inCode, startDelim, endDelim, embd_evalCodes);
189         } else {
190             staticBuffer ~= inCode.front;
191             inCode.popFront();
192         }
193     }
194     if (staticBuffer.length) {
195         outCode ~= `write(`~generateQuotesFor(staticBuffer)~`, dchar.init);`;
196     }
197 
198     return outCode.to!string();
199 }
200 
201 string getDynamicContent(ref dstring inCode, 
202                          dstring startDelim, dstring endDelim, 
203                          const(dchar)[] evalCodes) {
204     // TODO allow endDelim to appear in strings
205     void notEmpty() {
206         enforce(!inCode.empty, 
207                 format("Starting '%s' not matched by closing '%s'.", startDelim, endDelim));
208     }
209 
210     inCode = inCode[startDelim.length .. $];
211     notEmpty();
212     dchar evalCode = dchar.init;
213     if (evalCodes.canFind(inCode.front)) {
214         evalCode = inCode.front;
215         inCode.popFront();
216     }
217     string outCode = "";
218     while (true) {
219         notEmpty();
220         if (inCode.startsWith(endDelim)) {
221             inCode = inCode[endDelim.length .. $];
222             break;
223         } else {
224             outCode ~= inCode.front.to!string();
225             inCode.popFront();
226         }
227     }
228 
229     if (evalCode == dchar.init) {
230         return outCode;
231     } else {
232         return format(`write(%s, '\u%.4x');`, outCode, evalCode);
233     }
234 }
235 
236 unittest {
237     assert(generateQuotesFor(` `) == `x"20"c`);
238     assert(generateQuotesFor(`ẍ`) == `x"e1ba8d"c`);
239 }
240 
241 string generateQuotesFor(dstring buffer) {
242     // convert to ubyte so it doesn't convert back into a range of dchars
243     return format(`x"%-(%.2x%)"c`, cast(immutable(ubyte)[])buffer.to!string());
244 }