1<?php
2
3namespace ComboStrap;
4
5use ComboStrap\Tag\ShareTag;
6use Handlebars\Context;
7use Handlebars\Handlebars;
8use Handlebars\Loader\FilesystemLoader;
9
10class 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>
322EOF;
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