1 /**
2 * Copyright © DiamondMVC 2018
3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module diamond.templates.parser;
7 
8 import std.conv : to;
9 import std.algorithm : count;
10 import std..string : indexOf;
11 
12 import diamond.templates.grammar;
13 import diamond.templates.contentmode;
14 import diamond.templates.characterincludemode;
15 import diamond.templates.part;
16 
17 private
18 {
19   // HACK: to make AA's with classes work at compile-time.
20   @property grammars()
21   {
22     Grammar[char] grammars;
23     grammars['['] = new Grammar(
24       "metadata", '[', ']',
25       ContentMode.metaContent, CharacterIncludeMode.none, false, false
26     );
27 
28     grammars['<'] = new Grammar(
29       "placeHolder", '<', '>',
30       ContentMode.appendContentPlaceHolder, CharacterIncludeMode.none, false, false
31     );
32 
33     grammars['{'] = new Grammar(
34       "code", '{', '}',
35       ContentMode.mixinContent, CharacterIncludeMode.none, false, false
36     );
37 
38     grammars[':'] = new Grammar(
39       "expression", ':', '\n',
40       ContentMode.mixinContent, CharacterIncludeMode.none, false, true
41     );
42 
43     grammars['='] = new Grammar(
44       "expressionValue", '=', ';',
45       ContentMode.appendContent, CharacterIncludeMode.none, false, true
46     );
47 
48     grammars['('] = new Grammar(
49       "escapedValue", '(', ')',
50       ContentMode.appendContent, CharacterIncludeMode.none, false, false
51     );
52 
53     grammars['$'] = new Grammar(
54       "expressionEscaped", '$', ';',
55       ContentMode.appendContent, CharacterIncludeMode.none, false, false,
56       '=' // Character that must follow the first character after @
57     );
58 
59     grammars['*'] = new Grammar(
60       "comment", '*', '*',
61       ContentMode.discardContent, CharacterIncludeMode.none, false, true
62     );
63 
64     grammars['!'] = new Grammar(
65       "section", '!', ':',
66       ContentMode.discardContent, CharacterIncludeMode.none, false, true
67     );
68 
69     import diamond.extensions;
70     mixin ExtensionEmit!(ExtensionType.customGrammar, q{
71       Grammar[char] customGrammars = {{extensionEntry}}.createGrammars();
72 
73       if (customGrammars)
74       {
75         foreach (key,value; customGrammars)
76         {
77           grammars[key] = value;
78         }
79       }
80     });
81     emitExtension();
82 
83     return grammars;
84   }
85 }
86 
87 /**
88 * Parses a diamond template.
89 * Params:
90 *   content = The content of the diamond template to parse.
91 * Returns:
92 *   An associative array of arrays holding the section's template parts.
93 */
94 auto parseTemplate(string content)
95 {
96   Part[][string] parts;
97 
98   auto current = new Part;
99   size_t curlyBracketCount = 0;
100   size_t squareBracketcount = 0;
101   size_t parenthesisCount = 0;
102   string currentSection = "";
103 
104   foreach (ref i; 0 .. content.length)
105   {
106     auto beforeChar = i > 0 ? content[i - 1] : '\0';
107     auto currentChar = content[i];
108     auto afterChar = i < (content.length - 1) ? content[i + 1] : '\0';
109     auto beforeSecondaryChar = i > 1 ? content[i - 2] : '\0';
110 
111     if (currentChar == '@' && !current.currentGrammar)
112     {
113       if (current._content && current._content.length && afterChar != '.')
114       {
115         parts[currentSection] ~= current;
116         current = new Part;
117       }
118 
119       if (afterChar != '@' && afterChar != '.')
120       {
121         auto grammar = grammars.get(afterChar, null);
122 
123         if (grammar && beforeChar != '@')
124         {
125           current.currentGrammar = grammar;
126 
127           if (afterChar == ':')
128           {
129             auto searchSource = content[i .. $];
130             searchSource = searchSource[0 .. searchSource.indexOf('\n')];
131 
132             curlyBracketCount += searchSource.count!(c => c == '{');
133             squareBracketcount += searchSource.count!(c => c == '[');
134             parenthesisCount += searchSource.count!(c => c == '(');
135 
136             curlyBracketCount -= searchSource.count!(c => c == '}');
137             squareBracketcount -= searchSource.count!(c => c == ']');
138             parenthesisCount -= searchSource.count!(c => c == ')');
139           }
140         }
141         else
142         {
143           current._content ~= currentChar;
144         }
145       }
146       else if (afterChar == '.')
147       {
148         current._content ~= currentChar;
149       }
150     }
151     else
152     {
153       if (current.currentGrammar)
154       {
155         if (current.currentGrammar.mandatoryStartCharacter != '\0' &&
156         beforeSecondaryChar == '@' &&
157         beforeChar == current.currentGrammar.startCharacter &&
158         currentChar == current.currentGrammar.mandatoryStartCharacter)
159         {
160           continue;
161         }
162 
163         if (currentChar == current.currentGrammar.startCharacter &&
164           (!current.currentGrammar.ignoreDepth || !current.isStart())
165         )
166         {
167           current.increaseSeekIndex();
168 
169           if (current.isStart())
170           {
171             continue;
172           }
173         }
174         else if (currentChar == current.currentGrammar.endCharacter)
175         {
176           current.decreaseSeekIndex();
177         }
178       }
179 
180       if (current.isEnd(currentChar))
181       {
182         switch (current.currentGrammar.characterIncludeMode)
183         {
184           case CharacterIncludeMode.start:
185             current._content =
186               to!string(current.currentGrammar.startCharacter)
187               ~ current.content;
188             break;
189 
190           case CharacterIncludeMode.end:
191             current._content ~= current.currentGrammar.endCharacter;
192             break;
193 
194           case CharacterIncludeMode.both:
195             current._content =
196               to!string(current.currentGrammar.startCharacter) ~
197               current.content ~ to!string(current.currentGrammar.endCharacter);
198             break;
199 
200           default: break;
201         }
202 
203         if (current.currentGrammar &&
204         current.currentGrammar.includeStatementCharacter)
205         {
206           current._content = "@" ~ current.content;
207         }
208 
209         if (current._currentGrammar.name == "section")
210         {
211           import std..string : strip;
212 
213           auto sectionName = current.content ? current.content.strip() : "";
214 
215           currentSection = sectionName;
216         }
217         else
218         {
219           parts[currentSection] ~= current;
220         }
221 
222         current = new Part;
223       }
224       else
225       {
226         // TODO: Simplify this ...
227         if (curlyBracketCount && currentChar == '}')
228         {
229           curlyBracketCount--;
230 
231           parts[currentSection] ~= current;
232 
233           current = new Part;
234           current.currentGrammar = grammars.get('{', null);
235           current._content = "}";
236 
237           if (afterChar == ';')
238           {
239             current._content ~= ";";
240             i++;
241           }
242 
243           parts[currentSection] ~= current;
244 
245           current = new Part;
246         }
247         else if (squareBracketcount && currentChar == ']')
248         {
249           squareBracketcount--;
250 
251           parts[currentSection] ~= current;
252 
253           current = new Part;
254           current.currentGrammar = grammars.get('{', null);
255           current._content = "]";
256 
257           if (afterChar == ';')
258           {
259             current._content ~= ";";
260             i++;
261           }
262 
263           parts[currentSection] ~= current;
264 
265           current = new Part;
266         }
267         else if (parenthesisCount && currentChar == ')')
268         {
269           parenthesisCount--;
270 
271           parts[currentSection] ~= current;
272 
273           current = new Part;
274           current.currentGrammar = grammars.get('{', null);
275           current._content = ")";
276 
277           if (afterChar == ';')
278           {
279             current._content ~= ";";
280             i++;
281           }
282 
283           parts[currentSection] ~= current;
284 
285           current = new Part;
286         }
287         else
288         {
289           current._content ~= currentChar;
290         }
291       }
292     }
293   }
294 
295   parts[currentSection] ~= current;
296 
297   return parts;
298 }