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