1 <?php
2 
3 namespace ComboStrap;
4 
5 use ComboStrap\Tag\ShareTag;
6 use Handlebars\Context;
7 use Handlebars\Handlebars;
8 use Handlebars\Loader\FilesystemLoader;
9 
10 class TemplateEngine
11 {
12 
13 
14     /**
15      * We use hbs and not html as extension because it permits
16      * to have syntax highlighting in idea
17      */
18     const EXTENSION_HBS = "hbs";
19     const CANONICAL = "theme";
20     public const CONF_THEME_DEFAULT = "default";
21     public const CONF_THEME = "combo-conf-005";
22 
23 
24     private Handlebars $handleBarsForPage;
25     /**
26      * @var LocalPath[]
27      */
28     private array $templateSearchDirectories;
29     /**
30      * This path are wiki path because
31      * they should be able to be accessed externally (fetched)
32      * @var WikiPath[]
33      */
34     private array $componentCssSearchDirectories;
35 
36     /**
37      * @var Handlebars for component
38      */
39     private Handlebars $handleBarsForComponents;
40 
41 
42     static public function createForTheme(string $themeName): TemplateEngine
43     {
44 
45         $handleBarsObjectId = "handlebar-theme-$themeName";
46         $executionContext = ExecutionContext::getActualOrCreateFromEnv();
47 
48         try {
49             return $executionContext->getRuntimeObject($handleBarsObjectId);
50         } catch (ExceptionNotFound $e) {
51             // not found
52         }
53 
54 
55         try {
56 
57             /**
58              * Default
59              */
60             $default = self::CONF_THEME_DEFAULT;
61             /**
62              * @var WikiPath[] $componentsCssSearchDirectories
63              */
64             $componentsCssSearchDirectories = array(); // a list of directories where to search the component stylesheet
65             $componentsHtmlSearchDirectories = array(); // a list of directories where to search the component html templates
66             /**
67              * @var LocalPath[] $templatesSearchDirectories
68              */
69             $templatesSearchDirectories = array(); // a list of directories where to search the template
70             /**
71              * @var LocalPath[] $partialSearchDirectories
72              */
73             $partialSearchDirectories = array(); // a list of directories where to search the partials
74             if ($themeName !== $default) {
75                 $themeDirectory = self::getThemeHomeAsWikiPath()->resolve($themeName);
76                 $themePagesTemplateDirectory = $themeDirectory->resolve("pages:templates:")->toLocalPath();
77                 $themePagesPartialsDirectory = $themeDirectory->resolve("pages:partials:")->toLocalPath();
78                 $themeComponentsCssDirectory = $themeDirectory->resolve("components:css:");
79                 $themeComponentsHtmlDirectory = $themeDirectory->resolve("components:html:")->toLocalPath();
80                 if (PluginUtility::isTest()) {
81                     try {
82                         FileSystems::createDirectoryIfNotExists($themePagesTemplateDirectory);
83                         FileSystems::createDirectoryIfNotExists($themePagesPartialsDirectory);
84                     } catch (ExceptionCompile $e) {
85                         throw new ExceptionRuntimeInternal($e);
86                     }
87                 }
88 
89                 if (FileSystems::exists($themePagesTemplateDirectory)) {
90                     $templatesSearchDirectories[] = $themePagesTemplateDirectory;
91                 } else {
92                     LogUtility::warning("The template theme directory ($themeDirectory) does not exists and was not taken into account");
93                 }
94                 if (FileSystems::exists($themePagesPartialsDirectory)) {
95                     $partialSearchDirectories[] = $themePagesPartialsDirectory;
96                 } else {
97                     LogUtility::warning("The partials theme directory ($themeDirectory) does not exists");
98                 }
99                 if (FileSystems::exists($themeComponentsCssDirectory)) {
100                     $componentsCssSearchDirectories[] = $themeComponentsCssDirectory;
101                 }
102                 if (FileSystems::exists($themeComponentsHtmlDirectory)) {
103                     $componentsHtmlSearchDirectories[] = $themeComponentsHtmlDirectory;
104                 }
105             }
106 
107             /**
108              * Default as last directory to search
109              */
110             $defaultTemplateDirectory = WikiPath::createComboResource(":theme:$default:pages:templates")->toLocalPath();
111             $templatesSearchDirectories[] = $defaultTemplateDirectory;
112             $partialSearchDirectories[] = WikiPath::createComboResource(":theme:$default:pages:partials")->toLocalPath();
113             $componentsCssSearchDirectories[] = WikiPath::createComboResource(":theme:$default:components:css");
114             $componentsHtmlSearchDirectories[] = WikiPath::createComboResource(":theme:$default:components:html")->toLocalPath();
115 
116             /**
117              * Handlebars Page
118              */
119             $templatesSearchDirectoriesAsStringPath = array_map(function ($element) {
120                 return $element->toAbsoluteId();
121             }, $templatesSearchDirectories);
122             $partialSearchDirectoriesAsStringPath = array_map(function ($element) {
123                 return $element->toAbsoluteId();
124             }, $partialSearchDirectories);
125             $pagesTemplatesLoader = new FilesystemLoader($templatesSearchDirectoriesAsStringPath, ["extension" => self::EXTENSION_HBS]);
126             $pagesPartialLoader = new FilesystemLoader($partialSearchDirectoriesAsStringPath, ["extension" => self::EXTENSION_HBS]);
127             $handleBarsForPages = new Handlebars([
128                 "loader" => $pagesTemplatesLoader,
129                 "partials_loader" => $pagesPartialLoader
130             ]);
131             self::addHelper($handleBarsForPages);
132 
133             /**
134              * Handlebars Html Component
135              */
136             $componentsHtmlSearchDirectoriesAsStringPath = array_map(function ($element) {
137                 return $element->toAbsoluteId();
138             }, $componentsHtmlSearchDirectories);
139             $componentsHtmlTemplatesLoader = new FilesystemLoader($componentsHtmlSearchDirectoriesAsStringPath, ["extension" => self::EXTENSION_HBS]);
140             $handleBarsForComponents = new Handlebars([
141                 "loader" => $componentsHtmlTemplatesLoader,
142                 "partials_loader" => $componentsHtmlTemplatesLoader
143             ]);
144 
145         } catch (ExceptionCast $e) {
146             // should not happen as combo resource is a known directory but yeah
147             throw ExceptionRuntimeInternal::withMessageAndError("Error while instantiating handlebars for page", $e);
148         }
149 
150 
151         $newPageTemplateEngine = new TemplateEngine();
152         $newPageTemplateEngine->handleBarsForPage = $handleBarsForPages;
153         $newPageTemplateEngine->handleBarsForComponents = $handleBarsForComponents;
154         $newPageTemplateEngine->templateSearchDirectories = $templatesSearchDirectories;
155         $newPageTemplateEngine->componentCssSearchDirectories = $componentsCssSearchDirectories;
156         $executionContext->setRuntimeObject($handleBarsObjectId, $newPageTemplateEngine);
157         return $newPageTemplateEngine;
158 
159 
160     }
161 
162     static public function createForString(): TemplateEngine
163     {
164 
165         $handleBarsObjectId = "handlebar-string";
166         $executionContext = ExecutionContext::getActualOrCreateFromEnv();
167 
168         try {
169             return $executionContext->getRuntimeObject($handleBarsObjectId);
170         } catch (ExceptionNotFound $e) {
171             // not found
172         }
173 
174 
175         $handleBars = new Handlebars();
176 
177         self::addHelper($handleBars);
178 
179         $newPageTemplateEngine = new TemplateEngine();
180         $newPageTemplateEngine->handleBarsForPage = $handleBars;
181         $executionContext->setRuntimeObject($handleBarsObjectId, $newPageTemplateEngine);
182         return $newPageTemplateEngine;
183 
184 
185     }
186 
187     private static function addHelper(Handlebars $handleBars)
188     {
189         $handleBars->addHelper("share",
190             function ($template, $context, $args, $source) {
191                 $knownType = ShareTag::getKnownTypes();
192                 $tagAttributes = TagAttributes::createFromTagMatch("<share $args/>", [], $knownType);
193                 return ShareTag::renderSpecialEnter($tagAttributes, DOKU_LEXER_SPECIAL);
194             }
195         );
196         /**
197          * Used in test
198          */
199         $handleBars->addHelper("echo",
200             function ($template, $context, $args, $source) {
201                 return "echo";
202             }
203         );
204         /**
205          * Hierachical breadcrumb
206          */
207         $handleBars->addHelper("breadcrumb",
208             function ($template, Context $context, $args, $source) {
209                 $knownType = BreadcrumbTag::TYPES;
210                 $default = BreadcrumbTag::getDefaultBlockAttributes();
211                 $tagAttributes = TagAttributes::createFromTagMatch("<breadcrumb $args/>", $default, $knownType);
212                 return BreadcrumbTag::toBreadCrumbHtml($tagAttributes);
213             }
214         );
215 
216         /**
217          * Page Image
218          */
219         $handleBars->addHelper("page-image",
220             function ($template, Context $context, $args, $source) {
221                 $knownType = PageImageTag::TYPES;
222                 $default = PageImageTag::getDefaultAttributes();
223                 $tagAttributes = TagAttributes::createFromTagMatch("<page-image $args/>", $default, $knownType);
224                 return PageImageTag::render($tagAttributes,[]);
225             }
226         );
227     }
228 
229     public static function createForDefaultTheme(): TemplateEngine
230     {
231         return self::createForTheme(self::CONF_THEME_DEFAULT);
232     }
233 
234     public static function createFromContext(): TemplateEngine
235     {
236         $theme = ExecutionContext::getActualOrCreateFromEnv()
237             ->getConfig()
238             ->getTheme();
239         return self::createForTheme($theme);
240     }
241 
242     public static function getThemes(): array
243     {
244         $theme = [self::CONF_THEME_DEFAULT];
245         $directories = FileSystems::getChildrenContainer(self::getThemeHomeAsWikiPath());
246         foreach ($directories as $directory) {
247             try {
248                 $theme[] = $directory->getLastName();
249             } catch (ExceptionNotFound $e) {
250                 LogUtility::internalError("The theme home is not the root file system", self::CANONICAL, $e);
251             }
252         }
253         return $theme;
254     }
255 
256     /**
257      * @return WikiPath - where the theme should be stored
258      */
259     private static function getThemeHomeAsWikiPath(): WikiPath
260     {
261         return WikiPath::getComboCustomThemeHomeDirectory();
262     }
263 
264 
265     public function renderWebPage(string $template, array $model): string
266     {
267         return $this->handleBarsForPage->render($template, $model);
268     }
269 
270     public function renderWebComponent(string $template, array $model): string
271     {
272         return $this->handleBarsForComponents->render($template, $model);
273     }
274 
275     /**
276      * @return LocalPath[]
277      * @throws ExceptionNotFound
278      */
279     public function getTemplateSearchDirectories(): array
280     {
281         if (isset($this->templateSearchDirectories)) {
282             return $this->templateSearchDirectories;
283         }
284         throw new ExceptionNotFound("No template directory as this is not a file engine");
285 
286     }
287 
288     public function templateExists(string $templateName): bool
289     {
290         try {
291             $this->handleBarsForPage->getLoader()->load($templateName);
292             return true;
293         } catch (\Exception $e) {
294             return false;
295         }
296 
297     }
298 
299     /**
300      * Create a file template (used mostly for test purpose)
301      * @param string $templateName - the name (without extension)
302      * @param string|null $templateContent - the content
303      * @return $this
304      */
305     public function createTemplate(string $templateName, string $templateContent = null): TemplateEngine
306     {
307 
308         if (count($this->templateSearchDirectories) !== 2) {
309             // only one, this is the default, we need two
310             throw new ExceptionRuntimeInternal("We can create a template only in a custom theme directory");
311         }
312         $theme = $this->templateSearchDirectories[0];
313         $templateFile = $theme->resolve($templateName . "." . self::EXTENSION_HBS);
314         if ($templateContent === null) {
315             $templateContent = <<<EOF
316 <html lang="en">
317 <head><title>{{ title }}</title></head>
318 <body>
319 <p>Test template</p>
320 </body>
321 </html>
322 EOF;
323         }
324         FileSystems::setContent($templateFile, $templateContent);
325         return $this;
326     }
327 
328     /**
329      * @throws ExceptionNotFound
330      */
331     public function searchTemplateByName(string $name): LocalPath
332     {
333         foreach ($this->templateSearchDirectories as $templateSearchDirectory) {
334             $file = $templateSearchDirectory->resolve($name);
335             if (FileSystems::exists($file)) {
336                 return $file;
337             }
338         }
339         throw new ExceptionNotFound("No file named $name found");
340     }
341 
342 
343     public function getComponentStylePathByName(string $nameWithExtenson): WikiPath
344     {
345         $file = null;
346         foreach ($this->componentCssSearchDirectories as $componentSearchDirectory) {
347             $file = $componentSearchDirectory->resolve($nameWithExtenson);
348             if (FileSystems::exists($file)) {
349                 return $file;
350             }
351         }
352         /**
353          * We return the last one that should be the default theme
354          */
355         return $file;
356     }
357 
358     public function getComponentTemplatePathByName(string $LOGICAL_TAG)
359     {
360 
361     }
362 
363 
364 }
365