1 /**
2 * Copyright © Yurai Web Framework 2021
3 * License: MIT (https://github.com/YuraiWeb/yurai/blob/main/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module yurai.templates.parser;
7 
8 import yurai.templates.templatetype;
9 import yurai.templates.templatedata;
10 
11 public final class Token
12 {
13   public:
14   final:
15   TemplateType templateType;
16   string content;
17 
18   this(TemplateType templateType)
19   {
20     this.templateType = templateType;
21 
22     content = "";
23   }
24 
25   override string toString()
26   {
27     return content;
28   }
29 }
30 
31 public final class StringReader
32 {
33   private:
34   string _content;
35   size_t _index;
36 
37   public:
38   final:
39   this(string content)
40   {
41     _content = content;
42     _index = 0;
43   }
44 
45   StringReader move()
46   {
47     _index++;
48 
49     return this;
50   }
51 
52   StringReader back()
53   {
54     _index--;
55 
56     return this;
57   }
58 
59   char peek(size_t amount)
60   {
61     auto index = _index + amount;
62 
63     return index < (_content.length - 1) ? _content[index] : '\0';
64   }
65 
66   @property
67   {
68     bool has() { return _index < _content.length; }
69 
70     char last() { return _index > 0 ? _content[_index - 1] : '\0'; }
71 
72     char current() { return _content[_index]; }
73 
74     char next() { return peek(1); }
75   }
76 }
77 
78 public class Scope
79 {
80   char scopeCharStart;
81   char scopeCharEnd;
82   size_t scopeCount;
83 
84   this(char scopeCharStart, char scopeCharEnd)
85   {
86     this.scopeCharStart = scopeCharStart;
87     this.scopeCharEnd = scopeCharEnd;
88     this.scopeCount = 0;
89   }
90 }
91 
92 public class ScopeStack
93 {
94   Scope[] _stack;
95 
96   public:
97   final:
98   this()
99   {
100     _stack = [];
101   }
102 
103   void push(Scope s)
104   {
105     _stack ~= s;
106   }
107 
108   void pop()
109   {
110     if (_stack.length == 1)
111     {
112       _stack = [];
113     }
114     else if (_stack.length > 1)
115     {
116       _stack = _stack[0 .. $-1];
117     }
118   }
119 
120   @property
121   {
122     bool has() { return _stack.length > 0; }
123 
124     Scope currentScope() { return _stack[$-1]; }
125   }
126 }
127 
128 public:
129 Token[] parse(string content)
130 {
131   import std.stdio : writeln, writefln;
132 
133   Token[] tokens = [];
134 
135   bool inComment = false;
136 
137   auto reader = new StringReader(content);
138 
139   auto scopeStack = new ScopeStack();
140 
141   auto contentToken = new Token(TemplateType.content);
142 
143   while (reader.has)
144   {
145     if (inComment)
146     {
147       if (reader.current == '@' && reader.last == '*')
148       {
149         inComment = false;
150         continue;
151       }
152     }
153 
154     if (reader.current == '*' && reader.last == '@')
155     {
156       inComment = true;
157     }
158     else
159     {
160       if (reader.current == '@' && reader.last != '\\')
161       {
162         if (reader.next == '[')
163         {
164           if (contentToken && contentToken.content && contentToken.content.length)
165           {
166             tokens ~= contentToken;
167             contentToken = new Token(TemplateType.content);
168           }
169 
170           auto token = new Token(TemplateType.meta);
171           // skip @ and [
172           reader.move().move();
173 
174           // parse content as meta
175           reader.basicDepthTagParsing('[', ']', token, tokens);
176         }
177         else if (reader.next == '#' && reader.peek(2) == '(')
178         {
179           if (contentToken && contentToken.content && contentToken.content.length)
180           {
181             tokens ~= contentToken;
182             contentToken = new Token(TemplateType.content);
183           }
184 
185           auto token = new Token(TemplateType.placeholderValue);
186           // skip @ # and (
187           reader.move().move().move();
188 
189           // parse content as placeholderValue
190           reader.basicDepthTagParsing('(', ')', token, tokens);
191         }
192         else if (reader.next == '(')
193         {
194           if (contentToken && contentToken.content && contentToken.content.length)
195           {
196             tokens ~= contentToken;
197             contentToken = new Token(TemplateType.content);
198           }
199 
200           auto token = new Token(TemplateType.placeholder);
201           // skip @ and (
202           reader.move().move();
203 
204           // parse content as placeholder
205           reader.basicDepthTagParsing('(', ')', token, tokens);
206         }
207         else if (reader.next == '{')
208         {
209           if (contentToken && contentToken.content && contentToken.content.length)
210           {
211             tokens ~= contentToken;
212             contentToken = new Token(TemplateType.content);
213           }
214 
215           auto token = new Token(TemplateType.mixinCodeBlock);
216           // skip @ and {
217           reader.move().move();
218 
219           // parse content as mixinCodeBlock
220           reader.basicDepthTagParsing('{', '}', token, tokens);
221         }
222         else if (reader.next == '$' && reader.peek(2) == '=')
223         {
224           if (contentToken && contentToken.content && contentToken.content.length)
225           {
226             tokens ~= contentToken;
227             contentToken = new Token(TemplateType.content);
228           }
229 
230           auto token = new Token(TemplateType.mixinExpression);
231           // skip @ $ and =
232           reader.move().move().move();
233 
234           // parse content as mixinExpression
235           reader.basicTagParsing(';', token, tokens);
236         }
237         else if (reader.next == '=')
238         {
239           if (contentToken && contentToken.content && contentToken.content.length)
240           {
241             tokens ~= contentToken;
242             contentToken = new Token(TemplateType.content);
243           }
244 
245           auto token = new Token(TemplateType.mixinEscapeExpression);
246           // skip @ and =
247           reader.move().move();
248 
249           // parse content as mixinEscapeExpression
250           reader.basicTagParsing(';', token, tokens);
251         }
252         else if (reader.next == ':')
253         {
254           if (contentToken && contentToken.content && contentToken.content.length)
255           {
256             tokens ~= contentToken;
257             contentToken = new Token(TemplateType.content);
258           }
259 
260           auto token = new Token(TemplateType.mixinStatement);
261           // skip @ and :
262           reader.move().move();
263 
264           // parse content as mixinStatement
265           auto scopeCharacter = reader.basicScopedTagParsing(token, tokens);
266 
267           if (scopeCharacter == '{')
268           {
269             scopeStack.push(new Scope(scopeCharacter, '}'));
270           }
271           else if (scopeCharacter == '(')
272           {
273             scopeStack.push(new Scope(scopeCharacter, ')'));
274           }
275           else if (scopeCharacter == '[')
276           {
277             scopeStack.push(new Scope(scopeCharacter, ']'));
278           }
279         }
280         else if (reader.next == '<')
281         {
282           if (contentToken && contentToken.content && contentToken.content.length)
283           {
284             tokens ~= contentToken;
285             contentToken = new Token(TemplateType.content);
286           }
287 
288           auto token = new Token(TemplateType.partialView);
289           // skip @ and <
290           reader.move().move();
291 
292           // parse content as partialView
293           reader.basicDepthTagParsing('<', '>', token, tokens);
294         }
295         else
296         {
297           contentToken.content ~= reader.current;
298         }
299       }
300       else if (reader.current != '\\' || reader.next != '@')
301       {
302         if (scopeStack.has)
303         {
304           if (reader.current == scopeStack.currentScope.scopeCharStart)
305           {
306             scopeStack.currentScope.scopeCount++;
307           }
308           else if (reader.current == scopeStack.currentScope.scopeCharEnd)
309           {
310             if (scopeStack.currentScope.scopeCount == 0)
311             {
312               if (contentToken && contentToken.content && contentToken.content.length)
313               {
314                 tokens ~= contentToken;
315                 contentToken = new Token(TemplateType.content);
316               }
317 
318               auto token = new Token(TemplateType.mixinStatement);
319               token.content ~= scopeStack.currentScope.scopeCharEnd;
320               tokens ~= token;
321 
322               scopeStack.pop();
323               reader.move();
324               continue;
325             }
326 
327             scopeStack.currentScope.scopeCount--;
328           }
329         }
330 
331         contentToken.content ~= reader.current;
332       }
333     }
334 
335     reader.move();
336   }
337 
338   if (contentToken && contentToken.content && contentToken.content.length)
339   {
340     tokens ~= contentToken;
341     contentToken = new Token(TemplateType.content);
342   }
343 
344   return tokens;
345 }
346 
347 private:
348 void basicDepthTagParsing(StringReader reader, char tagStart, char tagEnd, Token token, ref Token[] tokens)
349 {
350   ptrdiff_t tagDepth = 0;
351 
352   while (reader.has)
353   {
354     if (reader.current == tagStart)
355     {
356       tagDepth++;
357     }
358     else if (reader.current == tagEnd)
359     {
360       if (tagDepth == 0)
361       {
362         tokens ~= token;
363         break;
364       }
365       tagDepth--;
366     }
367 
368     token.content ~= reader.current;
369 
370     reader.move();
371   }
372 }
373 
374 void basicTagParsing(StringReader reader, char tagEnd, Token token, ref Token[] tokens)
375 {
376   while (reader.has)
377   {
378     if (reader.current == tagEnd)
379     {
380       tokens ~= token;
381       break;
382     }
383 
384     token.content ~= reader.current;
385 
386     reader.move();
387   }
388 }
389 
390 char basicScopedTagParsing(StringReader reader, Token token, ref Token[] tokens)
391 {
392   while (reader.has)
393   {
394     if (reader.current == '\r' || reader.current == '\n')
395     {
396       auto scopeCharacter = reader.last;
397 
398       if (reader.current == '\r' && reader.next == '\n')
399       {
400         reader.move(); // skip the \n too ...
401       }
402 
403       tokens ~= token;
404       return scopeCharacter;
405     }
406 
407     token.content ~= reader.current;
408 
409     reader.move();
410   }
411 
412   return '\0';
413 }