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