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(createRenderingCode(embd_code, embd_start, embd_end, embd_evalCodes));
68         }
69     };
70 }
71 
72 private:
73 unittest {
74     static class MyContext : embd.Context {
75         uint until = 20;
76 
77         void write(string content, dchar evalCode) {
78             import std.stdio;
79             write(content);
80         }
81 
82         mixin(renderer);
83     }
84 
85     auto ctx = new MyContext();
86     ctx.render!(import("test.embd.html"), `=`)();
87 }
88 
89 string createRenderingCode(string embd_code, 
90                            string embd_start, string embd_end, 
91                            const(dchar)[] embd_evalCodes) {
92     // convert to dstring for slicing
93     dstring inCode = embd_code.to!dstring();
94     dstring startDelim = embd_start.to!dstring();
95     dstring endDelim = embd_end.to!dstring();
96     string outCode = "";
97 
98     // two states: static content and dynamic content
99     dstring staticBuffer = "";
100     while (!inCode.empty) {
101         if (inCode.startsWith(startDelim)) {
102             outCode ~= `write(`~generateQuotesFor(staticBuffer)~`, dchar.init);`;
103             staticBuffer = "";
104             outCode ~= getDynamicContent(inCode, startDelim, endDelim, embd_evalCodes);
105         } else {
106             staticBuffer ~= inCode.front;
107             inCode.popFront();
108         }
109     }
110     if (staticBuffer.length) {
111         outCode ~= `write(`~generateQuotesFor(staticBuffer)~`, dchar.init);`;
112     }
113 
114     return outCode.to!string();
115 }
116 
117 string getDynamicContent(ref dstring inCode, 
118                          dstring startDelim, dstring endDelim, 
119                          const(dchar)[] evalCodes) {
120     // TODO allow endDelim to appear in strings
121     void notEmpty() {
122         enforce(!inCode.empty, 
123                 xformat("Starting '%s' not matched by closing '%s'.", startDelim, endDelim));
124     }
125 
126     inCode = inCode[startDelim.length .. $];
127     notEmpty();
128     dchar evalCode = dchar.init;
129     if (evalCodes.canFind(inCode.front)) {
130         evalCode = inCode.front;
131         inCode.popFront();
132     }
133     string outCode = "";
134     while (true) {
135         notEmpty();
136         if (inCode.startsWith(endDelim)) {
137             inCode = inCode[endDelim.length .. $];
138             break;
139         } else {
140             outCode ~= inCode.front.to!string();
141             inCode.popFront();
142         }
143     }
144 
145     if (evalCode == dchar.init) {
146         return outCode;
147     } else {
148         return xformat(`write(%s, '\u%.4x');`, outCode, evalCode);
149     }
150 }
151 
152 unittest {
153     assert(generateQuotesFor(` `) == `x"20"c`);
154     assert(generateQuotesFor(`ẍ`) == `x"e1ba8d"c`);
155 }
156 
157 string generateQuotesFor(dstring buffer) {
158     // convert to ubyte so it doesn't convert back into a range of dchars
159     return xformat(`x"%-(%.2x%)"c`, cast(immutable(ubyte)[])buffer.to!string());
160 }