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 }