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