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.prebuilding.prebuildviews;
7 
8 import yurai.templates;
9 
10 void prebuildViews(string[] registeredViews)
11 {
12   import std.file : dirEntries, SpanMode, readText, remove;
13   import std.algorithm : filter, endsWith;
14 
15   ViewInformation[] viewInformations;
16 
17   foreach (string name; dirEntries("prebuild/views", SpanMode.depth).filter!(f => f.name.endsWith(".d")))
18   {
19     remove(name);
20   }
21 
22   foreach (string name; dirEntries("views", SpanMode.depth).filter!(f => f.name.endsWith(".dd")))
23   {
24     string content = readText(name);
25 
26     auto viewInformation = parseView(content);
27 
28     if (viewInformation.name && viewInformation.name.length)
29     {
30       viewInformations ~= viewInformation;
31     }
32   }
33 
34   foreach (content; registeredViews)
35   {
36     auto viewInformation = parseView(content);
37 
38     if (viewInformation.name && viewInformation.name.length)
39     {
40       viewInformations ~= viewInformation;
41     }
42   }
43 
44   prebuildPackage(viewInformations);
45   prebuildViewClasses(viewInformations);
46   prebuildViewsMap(viewInformations);
47 }
48 
49 private:
50 ViewInformation parseView(string content)
51 {
52   auto viewInformation = new ViewInformation;
53   auto tokens = parse(content);
54 
55   if (tokens && tokens.length)
56   {
57     foreach (token; tokens)
58     {
59       if (!token || !token.content || !token.content.length)
60       {
61         continue;
62       }
63 
64       if (token.templateType != TemplateType.content)
65       {
66         viewInformation.lastWasContent = false;
67       }
68 
69       switch (token.templateType)
70       {
71         case TemplateType.content:
72           parseContent(token, viewInformation);
73           viewInformation.lastWasContent = true;
74           break;
75 
76         case TemplateType.meta:
77           parseMeta(token, viewInformation);
78           break;
79 
80         case TemplateType.placeholderValue:
81           parsePlaceholderValue(token, viewInformation);
82           break;
83 
84         case TemplateType.placeholder:
85           parsePlaceholder(token, viewInformation);
86           viewInformation.lastWasContent = true;
87           break;
88 
89         case TemplateType.mixinStatement:
90           parseMixinStatement(token, viewInformation);
91           break;
92 
93         case TemplateType.mixinCodeBlock:
94           parseMixinCodeBlock(token, viewInformation);
95           break;
96 
97         case TemplateType.mixinExpression:
98           parseMixinExpression(token, viewInformation);
99           break;
100 
101         case TemplateType.mixinEscapeExpression:
102           parseMixinEscapedExpression(token, viewInformation);
103           break;
104 
105         case TemplateType.partialView:
106           parsePartialView(token, viewInformation);
107           viewInformation.lastWasContent = true;
108           break;
109 
110         default: break;
111       }
112     }
113   }
114 
115   return viewInformation;
116 }
117 
118 class ViewInformation
119 {
120   string name;
121   string layout;
122   string model;
123   string controller;
124   string[] routes;
125   string[] executePreContent;
126   string[] executeContent;
127   bool lastWasContent;
128   string contentType;
129 
130   this()
131   {
132     routes = [];
133     executePreContent = [];
134     executeContent = [];
135   }
136 }
137 
138 void parseContent(Token token, ViewInformation view)
139 {
140   import std.string : format,strip;
141   import std.array : replace;
142 
143   if ((!view.executeContent.length || !view.lastWasContent) && !token.content.strip.length)
144   {
145     return;
146   }
147 
148   view.executeContent ~= "append(`%s`);".format(token.content.replace("`", "`"));
149 }
150 
151 void parseMeta(Token token, ViewInformation view)
152 {
153   import std.string : strip, format;
154   import std.array : split;
155 
156   auto pair = token.content.split(":");
157 
158   if (!pair || pair.length != 2)
159   {
160     return;
161   }
162 
163   auto value = pair[1].strip;
164 
165   switch (pair[0].strip)
166   {
167     case "layout":
168       view.layout = value;
169       break;
170 
171     case "name":
172       view.name = value;
173       break;
174 
175     case "model":
176       view.model = value;
177       break;
178 
179     case "controller":
180       view.controller = value;
181       break;
182 
183     case "route":
184       view.routes ~= value;
185       break;
186 
187     case "content-type":
188       view.contentType = value;
189       break;
190 
191     case "section":
192       view.executeContent ~= "setSection(`%s`);".format(value);
193       break;
194 
195     default: break;
196   }
197 }
198 
199 void parsePlaceholderValue(Token token, ViewInformation view)
200 {
201   import std.string : format, strip;
202   import std.array : split;
203 
204   auto pair = token.content.split("|");
205 
206   if (!pair || pair.length != 2)
207   {
208     return;
209   }
210 
211   auto key = pair[0].strip;
212   auto value = pair[1].strip;
213 
214   view.executePreContent ~= "setPlaceholder(`%s`, `%s`);".format(key, value);
215 }
216 
217 void parsePlaceholder(Token token, ViewInformation view)
218 {
219   import std.string : format, strip;
220   import std.array : split;
221 
222   auto entries = token.content.split("|");
223 
224   if (!entries || !entries.length || entries.length > 2)
225   {
226     return;
227   }
228 
229   string key = null;
230   if (entries.length == 1)
231   {
232     key = entries[0].strip;
233 
234     view.executeContent ~= "append(getPlaceholder(`%s`));".format(key);
235   }
236 
237   if (entries.length == 2)
238   {
239     auto defaultText = entries[1].strip;
240 
241     view.executeContent ~= "append(getPlaceholder(`%s`, `%s`));".format(key, defaultText);
242   }
243 }
244 
245 void parseMixinStatement(Token token, ViewInformation view)
246 {
247   import std.string : format;
248 
249   view.executeContent ~= token.content;
250 }
251 
252 void parseMixinCodeBlock(Token token, ViewInformation view)
253 {
254   import std.string : format;
255 
256   view.executeContent ~= token.content;
257 }
258 
259 void parseMixinExpression(Token token, ViewInformation view)
260 {
261   import std.string : format;
262 
263   view.executeContent ~= "append(%s);".format(token.content);
264 }
265 
266 void parseMixinEscapedExpression(Token token, ViewInformation view)
267 {
268   import std.string : format;
269 
270   view.executeContent ~= "escaped(%s);".format(token.content);
271 }
272 
273 void parsePartialView(Token token, ViewInformation view)
274 {
275   import std.string : strip, format;
276   import std.array : split;
277 
278   auto pair = token.content.split(":");
279 
280   if (!pair || pair.length < 1 || pair.length > 2)
281   {
282     return;
283   }
284 
285   auto viewName = pair[0].strip;
286 
287   if (pair.length == 2)
288   {
289     auto viewModel = pair[1].strip;
290 
291     view.executeContent ~= "renderPartialModel!`%s`(%s);".format(viewName, viewModel);
292   }
293   else
294   {
295     view.executeContent ~= "renderPartial(`%s`);".format(viewName);
296   }
297 }
298 
299 void prebuildPackage(ViewInformation[] viewInformations)
300 {
301   import std.string : format;
302   import std.file : write;
303   import std.array : join;
304 
305   enum finalModule = `module yurai.prebuild.views;
306 
307   public
308   {
309 %s
310   }
311 `;
312 
313   enum importFormat = "    import yurai.prebuild.views.view_%s;";
314 
315   string[] importList = [];
316 
317   foreach (viewInformation; viewInformations)
318   {
319     importList ~= importFormat.format(viewInformation.name);
320   }
321 
322   write("prebuild/views/package.d", finalModule.format(importList.join("\r\n")));
323 }
324 
325 void prebuildViewClasses(ViewInformation[] viewInformations)
326 {
327   import std.string : format;
328   import std.array : join, array;
329   import std.algorithm : map;
330   import std.file : write;
331 
332   enum finalModule = `module yurai.prebuild.views.view_%s;
333 
334 import yurai;
335 
336 import models;
337 
338 public final class view_%s : View
339 {
340   public:
341   %s
342   final:
343   this(IHttpRequest request, IHttpResponse response)
344   {
345     super("%s", request,response, [%s]);
346   }
347 
348   override ViewResult generate(bool processLayout)
349   {
350     %s
351 
352     return generateFinal(processLayout);
353   }
354   ViewResult generateModel(%s)
355   {
356     %s
357     return generateFinal(true);
358   }
359   override ViewResult generateFinal(bool processLayout)
360   {
361     %s
362     %s
363 
364     %s
365 
366     return finalizeContent(%s);
367   }
368 }
369 `;
370 
371   foreach (view; viewInformations)
372   {
373     string controllerCall = "";
374 
375     if (view.controller)
376     {
377       controllerCall ~= "import controllers;\r\n";
378       controllerCall ~= "auto controller = new " ~ view.controller ~ "!(view_" ~ view.name ~ ")(this, request, response);\r\n";
379       controllerCall ~= "auto status = controller.handle();\r\n";
380       controllerCall ~= "if (status == Status.end) return new ViewResult();\r\n";
381       controllerCall ~= "if (status == Status.notFound) return null;";
382     }
383 
384     string moduleCode = finalModule
385       .format(
386         view.name,
387         view.name,
388         view.model && view.model.length ? (view.model ~ " model;") : "",
389         view.name,
390         view.routes ? view.routes.map!(r => "`%s`".format(r)).array.join(",") : "",
391         controllerCall,
392         view.model && view.model.length ? (view.model ~ " model") : "",
393         view.model && view.model.length ? ("this.model = model;") : "",
394         view.executePreContent.join("\r\n"),
395         view.executeContent.join("\r\n"),
396         view.contentType ? "setContentType(\"%s\");".format(view.contentType) : "",
397         view.layout && view.layout.length ? ("`" ~ view.layout ~ "`,processLayout") : "null,processLayout");
398 
399     write("prebuild/views/" ~ view.name ~ ".d", moduleCode);
400   }
401 }
402 
403 void prebuildViewsMap(ViewInformation[] viewInformations)
404 {
405   import std.string : format;
406   import std.array : join, array;
407   import std.file : write;
408   import std.algorithm : map;
409 
410   enum finalModule = `module yurai.prebuild.viewsmap;
411 
412   import yurai.prebuild.views;
413   import yurai.views;
414   import yurai.core;
415 
416   string[] getAllRoutes()
417   {
418     return [%s];
419   }
420 
421   View getView(string name, IHttpRequest request, IHttpResponse response)
422   {
423     switch (name)
424     {
425 %s
426 
427       default:
428         return null;
429     }
430   }
431 
432   ViewResult processView(string route, IHttpRequest request, IHttpResponse response, bool processLayout = true)
433   {
434     switch (route)
435     {
436 %s
437 
438       default:
439         return null;
440     }
441   }
442 `;
443   enum viewGetFormat = `      case "%s":
444         return new view_%s(request, response);`;
445   enum viewProcessCaseFormat = `      case "%s":`;
446   enum viewProcessFormat = `%s
447         return new view_%s(request, response).generate(processLayout);`;
448 
449   string[] viewGet = [];
450   string[] viewProcessing = [];
451   string[] viewRoutes = [];
452 
453   foreach (view; viewInformations)
454   {
455     viewGet ~= viewGetFormat.format(view.name, view.name);
456 
457     if (view.routes && view.routes.length)
458     {
459       viewRoutes ~= view.routes;
460 
461       string cases = view.routes.map!(r => viewProcessCaseFormat.format(r)).array.join("\r\n");
462 
463       viewProcessing ~= viewProcessFormat.format(cases, view.name);
464     }
465   }
466 
467   auto finalViewRoutes = viewRoutes.map!(r => "`%s`".format(r)).array.join(",");
468 
469   write("prebuild/viewsmap.d", finalModule.format(finalViewRoutes, viewGet.join("\r\n"), viewProcessing.join("\r\n")));
470 }