1 /**
2 * Copyright © DiamondMVC 2019
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       "expressionEscaped", '=', ';',
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       "expressionValue", '$', ';',
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 
100   size_t curlyBracketCount = 0;
101   size_t squareBracketcount = 0;
102   size_t parenthesisCount = 0;
103   string currentSection = "";
104 
105   foreach (ref i; 0 .. content.length)
106   {
107     auto beforeChar = i > 0 ? content[i - 1] : '\0';
108     auto currentChar = content[i];
109     auto afterChar = i < (content.length - 1) ? content[i + 1] : '\0';
110     auto beforeSecondaryChar = i > 1 ? content[i - 2] : '\0';
111 
112     if (currentChar == '@' && !current.currentGrammar)
113     {
114       if (current._content && current._content.length && afterChar != '.')
115       {
116         parts[currentSection] ~= current;
117         current = new Part;
118       }
119 
120       if (afterChar != '@' && afterChar != '.')
121       {
122         auto grammar = grammars.get(afterChar, null);
123 
124         if (grammar && beforeChar != '@')
125         {
126           current.currentGrammar = grammar;
127 
128           if (afterChar == ':')
129           {
130             auto searchSource = content[i .. $];
131             searchSource = searchSource[0 .. searchSource.indexOf('\n')];
132 
133             curlyBracketCount += searchSource.count!(c => c == '{');
134             squareBracketcount += searchSource.count!(c => c == '[');
135             parenthesisCount += searchSource.count!(c => c == '(');
136 
137             curlyBracketCount -= searchSource.count!(c => c == '}');
138             squareBracketcount -= searchSource.count!(c => c == ']');
139             parenthesisCount -= searchSource.count!(c => c == ')');
140           }
141         }
142         else
143         {
144           current._content ~= currentChar;
145         }
146       }
147       else if (afterChar == '.')
148       {
149         current._content ~= currentChar;
150       }
151     }
152     else
153     {
154       if (current.currentGrammar)
155       {
156         if
157         (
158           current.currentGrammar.mandatoryStartCharacter != '\0' &&
159           beforeSecondaryChar == '@' &&
160           beforeChar == current.currentGrammar.startCharacter &&
161           currentChar == current.currentGrammar.mandatoryStartCharacter
162         )
163         {
164           continue;
165         }
166 
167         if
168         (
169           currentChar == current.currentGrammar.startCharacter &&
170           (!current.currentGrammar.ignoreDepth || !current.isStart())
171         )
172         {
173           current.increaseSeekIndex();
174 
175           if (current.isStart())
176           {
177             continue;
178           }
179         }
180         else if (currentChar == current.currentGrammar.endCharacter)
181         {
182           current.decreaseSeekIndex();
183         }
184       }
185 
186       if (current.isEnd(currentChar))
187       {
188         switch (current.currentGrammar.characterIncludeMode)
189         {
190           case CharacterIncludeMode.start:
191             current._content =
192               to!string(current.currentGrammar.startCharacter)
193               ~ current.content;
194             break;
195 
196           case CharacterIncludeMode.end:
197             current._content ~= current.currentGrammar.endCharacter;
198             break;
199 
200           case CharacterIncludeMode.both:
201             current._content =
202               to!string(current.currentGrammar.startCharacter) ~
203               current.content ~ to!string(current.currentGrammar.endCharacter);
204             break;
205 
206           default: break;
207         }
208 
209         if (current.currentGrammar &&
210         current.currentGrammar.includeStatementCharacter)
211         {
212           current._content = "@" ~ current.content;
213         }
214 
215         if (current._currentGrammar.name == "section")
216         {
217           import std..string : strip;
218 
219           auto sectionName = current.content ? current.content.strip() : "";
220 
221           currentSection = sectionName;
222         }
223         else
224         {
225           parts[currentSection] ~= current;
226         }
227 
228         current = new Part;
229       }
230       else
231       {
232         // TODO: Simplify this ...
233         if (curlyBracketCount && currentChar == '}')
234         {
235           curlyBracketCount--;
236 
237           parts[currentSection] ~= current;
238 
239           current = new Part;
240           current.currentGrammar = grammars.get('{', null);
241           current._content = "}";
242 
243           if (afterChar == ';')
244           {
245             current._content ~= ";";
246             i++;
247           }
248 
249           parts[currentSection] ~= current;
250 
251           current = new Part;
252         }
253         else if (squareBracketcount && currentChar == ']')
254         {
255           squareBracketcount--;
256 
257           parts[currentSection] ~= current;
258 
259           current = new Part;
260           current.currentGrammar = grammars.get('{', null);
261           current._content = "]";
262 
263           if (afterChar == ';')
264           {
265             current._content ~= ";";
266             i++;
267           }
268 
269           parts[currentSection] ~= current;
270 
271           current = new Part;
272         }
273         else if (parenthesisCount && currentChar == ')')
274         {
275           parenthesisCount--;
276 
277           parts[currentSection] ~= current;
278 
279           current = new Part;
280           current.currentGrammar = grammars.get('{', null);
281           current._content = ")";
282 
283           if (afterChar == ';')
284           {
285             current._content ~= ";";
286             i++;
287           }
288 
289           parts[currentSection] ~= current;
290 
291           current = new Part;
292         }
293         else
294         {
295           current._content ~= currentChar;
296         }
297       }
298     }
299   }
300 
301   parts[currentSection] ~= current;
302 
303   return parts;
304 }