1 <?php
2 
3 namespace ComboStrap;
4 
5 
6 use ComboStrap\Meta\Field\PageTemplateName;
7 use ComboStrap\TagAttribute\StyleAttribute;
8 use ComboStrap\Web\UrlEndpoint;
9 use ComboStrap\Xml\XmlDocument;
10 use ComboStrap\Xml\XmlElement;
11 use Symfony\Component\Yaml\Yaml;
12 
13 /**
14  * A page template is the object
15  * that generates a HTML page
16  * (ie the templating engine)
17  *
18  * It's used by Fetcher that creates pages such
19  * as {@link FetcherPage}, {@link FetcherMarkupWebcode} or {@link FetcherPageBundler}
20  */
21 class TemplateForWebPage
22 {
23 
24 
25     /**
26      * An internal configuration
27      * to tell if the page is social
28      * (ie seo, search engine, friendly)
29      */
30     const CONF_INTERNAL_IS_SOCIAL = "web-page-is-social";
31 
32     /**
33      * DocType is required by bootstrap and chrome
34      * https://developer.chrome.com/docs/lighthouse/best-practices/doctype/
35      * https://getbootstrap.com/docs/5.0/getting-started/introduction/#html5-doctype
36      * <!doctype html>
37      *
38      * The eol `\n` is needed for lightouse
39      */
40     const DOCTYPE = "<!doctype html>\n";
41 
42     private array $templateDefinition;
43     const CANONICAL = "template";
44 
45 
46     public const UTF_8_CHARSET_VALUE = "utf-8";
47     public const VIEWPORT_RESPONSIVE_VALUE = "width=device-width, initial-scale=1";
48     public const TASK_RUNNER_ID = "task-runner";
49     public const APPLE_TOUCH_ICON_REL_VALUE = "apple-touch-icon";
50 
51     public const PRELOAD_TAG = "preload";
52 
53     private string $templateName;
54 
55 
56     private string $requestedTitle;
57 
58 
59     private bool $requestedEnableTaskRunner = true;
60     private WikiPath $requestedContextPath;
61     private Lang $requestedLang;
62     private Toc $toc;
63     private bool $isSocial;
64     private string $mainContent;
65     private string $templateString;
66     private array $model;
67     private bool $hadMessages = false;
68     private string $requestedTheme;
69     private bool $isIframe = false;
70     private array $slots;
71 
72 
73     public static function create(): TemplateForWebPage
74     {
75         return new TemplateForWebPage();
76     }
77 
78     public static function config(): TemplateForWebPage
79     {
80         return new TemplateForWebPage();
81     }
82 
83     public static function getPoweredBy(): string
84     {
85         $domain = PluginUtility::$URL_APEX;
86         $version = PluginUtility::$INFO_PLUGIN['version'] . " (" . PluginUtility::$INFO_PLUGIN['date'] . ")";
87         $poweredBy = "<div class=\"mx-auto\" style=\"width: 300px;text-align: center;margin-bottom: 1rem\">";
88         $poweredBy .= "  <small><i>Powered by <a href=\"$domain\" title=\"ComboStrap " . $version . "\" style=\"color:#495057\">ComboStrap</a></i></small>";
89         $poweredBy .= '</div>';
90         return $poweredBy;
91     }
92 
93 
94     /**
95      * @throws ExceptionNotFound
96      */
97     public function getHtmlTemplatePath(): LocalPath
98     {
99         return $this->getEngine()->searchTemplateByName($this->templateName . "." . TemplateEngine::EXTENSION_HBS);
100     }
101 
102     public function setTemplateString(string $templateString): TemplateForWebPage
103     {
104         $this->templateString = $templateString;
105         return $this;
106     }
107 
108     public function setModel(array $model): TemplateForWebPage
109     {
110         $this->model = $model;
111         return $this;
112     }
113 
114     /**
115      * @return WikiPath from where the markup slot should be searched
116      * @throws ExceptionNotFound
117      */
118     public function getRequestedContextPath(): WikiPath
119     {
120         if (!isset($this->requestedContextPath)) {
121             throw new ExceptionNotFound("A requested context path was not found");
122         }
123         return $this->requestedContextPath;
124     }
125 
126     /**
127      *
128      * @return string - the page as html string (not dom because that's not how works dokuwiki)
129      *
130      */
131     public function render(): string
132     {
133 
134         $executionContext = (ExecutionContext::getActualOrCreateFromEnv())
135             ->setExecutingPageTemplate($this);
136         /**
137          * The deprecated report are just messing up html
138          */
139         $oldLevel = error_reporting(E_ALL ^ E_DEPRECATED);
140         try {
141 
142 
143             $pageTemplateEngine = $this->getEngine();
144             if ($this->isTemplateStringExecutionMode()) {
145                 $template = $this->templateString;
146             } else {
147                 $pageTemplateEngine = $this->getEngine();
148                 $template = $this->getTemplateName();
149                 if (!$pageTemplateEngine->templateExists($template)) {
150                     $defaultTemplate = PageTemplateName::HOLY_TEMPLATE_VALUE;
151                     LogUtility::warning("The template ($template) was not found, the default template ($defaultTemplate) was used instead.");
152                     $template = $defaultTemplate;
153                     $this->setRequestedTemplateName($template);
154                 }
155             }
156 
157             /**
158              * Get model should came after template validation
159              * as the template definition is named dependent
160              * (Create a builder, nom de dieu)
161              */
162             $model = $this->getModel();
163 
164 
165             return self::DOCTYPE . $pageTemplateEngine->renderWebPage($template, $model);
166 
167 
168         } finally {
169             error_reporting($oldLevel);
170             $executionContext
171                 ->closeExecutingPageTemplate();
172         }
173 
174     }
175 
176     /**
177      * @return string[]
178      */
179     public function getElementIds(): array
180     {
181         $definition = $this->getDefinition();
182         $elements = $definition['elements'] ?? null;
183         if ($elements == null) {
184             return [];
185         }
186         return $elements;
187 
188     }
189 
190 
191     /**
192      * @throws ExceptionNotFound
193      */
194     private function getRequestedLang(): Lang
195     {
196         if (!isset($this->requestedLang)) {
197             throw new ExceptionNotFound("No requested lang");
198         }
199         return $this->requestedLang;
200     }
201 
202 
203     public function getTemplateName(): string
204     {
205         if (isset($this->templateName)) {
206             return $this->templateName;
207         }
208         try {
209             $requestedPath = $this->getRequestedContextPath();
210             return PageTemplateName::createFromPage(MarkupPath::createPageFromPathObject($requestedPath))
211                 ->getValueOrDefault();
212         } catch (ExceptionNotFound $e) {
213             // no requested path
214         }
215         return ExecutionContext::getActualOrCreateFromEnv()
216             ->getConfig()
217             ->getDefaultLayoutName();
218     }
219 
220 
221     public function __toString()
222     {
223         return $this->templateName;
224     }
225 
226     /**
227      * @throws ExceptionNotFound
228      */
229     public function getCssPath(): LocalPath
230     {
231         return $this->getEngine()->searchTemplateByName("$this->templateName.css");
232     }
233 
234     /**
235      * @throws ExceptionNotFound
236      */
237     public function getJsPath(): LocalPath
238     {
239         $jsPath = $this->getEngine()->searchTemplateByName("$this->templateName.js");
240         if (!FileSystems::exists($jsPath)) {
241             throw new ExceptionNotFound("No js file");
242         }
243         return $jsPath;
244     }
245 
246     public function hasMessages(): bool
247     {
248         return $this->hadMessages;
249     }
250 
251     public function setRequestedTheme(string $themeName): TemplateForWebPage
252     {
253         $this->requestedTheme = $themeName;
254         return $this;
255     }
256 
257     public function hasElement(string $elementId): bool
258     {
259         return in_array($elementId, $this->getElementIds());
260     }
261 
262     public function isSocial(): bool
263     {
264         if (isset($this->isSocial)) {
265             return $this->isSocial;
266         }
267         try {
268             $path = $this->getRequestedContextPath();
269             if (!FileSystems::exists($path)) {
270                 return false;
271             }
272             $markup = MarkupPath::createPageFromPathObject($path);
273             if ($markup->isSlot()) {
274                 // slot are not social
275                 return false;
276             }
277         } catch (ExceptionNotFound $e) {
278             // not a path run
279             return false;
280         }
281         if ($this->isIframe) {
282             return false;
283         }
284         return ExecutionContext::getActualOrCreateFromEnv()
285             ->getConfig()
286             ->getBooleanValue(self::CONF_INTERNAL_IS_SOCIAL, true);
287 
288     }
289 
290     public function setIsIframe(bool $isIframe): TemplateForWebPage
291     {
292         $this->isIframe = $isIframe;
293         return $this;
294     }
295 
296     /**
297      * @return TemplateSlot[]
298      */
299     public function getSlots(): array
300     {
301         if (isset($this->slots)) {
302             return $this->slots;
303         }
304         $this->slots = [];
305         foreach ($this->getElementIds() as $elementId) {
306             if ($elementId === TemplateSlot::MAIN_TOC_ID) {
307                 /**
308                  * Main toc element is not a slot
309                  */
310                 continue;
311             }
312 
313             try {
314                 $this->slots[] = TemplateSlot::createFromElementId($elementId, $this->getRequestedContextPath());
315             } catch (ExceptionNotFound $e) {
316                 LogUtility::internalError("This template is not for a markup path, it cannot have slots then.");
317             }
318         }
319         return $this->slots;
320     }
321 
322 
323     /**
324      * Character set
325      * Note: avoid using {@link Html::encode() character entities} in your HTML,
326      * provided their encoding matches that of the document (generally UTF-8)
327      */
328     private function checkCharSetMeta(XmlElement $head)
329     {
330         $charsetValue = TemplateForWebPage::UTF_8_CHARSET_VALUE;
331         try {
332             $metaCharset = $head->querySelector("meta[charset]");
333             $charsetActualValue = $metaCharset->getAttribute("charset");
334             if ($charsetActualValue !== $charsetValue) {
335                 LogUtility::warning("The actual charset ($charsetActualValue) should be $charsetValue");
336             }
337         } catch (ExceptionBadSyntax|ExceptionNotFound $e) {
338             try {
339                 $metaCharset = $head->getDocument()
340                     ->createElement("meta")
341                     ->setAttribute("charset", $charsetValue);
342                 $head->appendChild($metaCharset);
343             } catch (\DOMException $e) {
344                 throw new ExceptionRuntimeInternal("Bad local name meta, should not occur", self::CANONICAL, 1, $e);
345             }
346         }
347     }
348 
349     /**
350      * @param XmlElement $head
351      * @return void
352      * Adapted from {@link TplUtility::renderFaviconMetaLinks()}
353      */
354     private function getPageIconHeadLinkHtml(): string
355     {
356         $html = $this->getShortcutFavIconHtmlLink();
357         $html .= $this->getIconHtmlLink();
358         $html .= $this->getAppleTouchIconHtmlLink();
359         return $html;
360     }
361 
362     /**
363      * Add a favIcon.ico
364      *
365      */
366     private function getShortcutFavIconHtmlLink(): string
367     {
368 
369         $internalFavIcon = WikiPath::createComboResource('images:favicon.ico');
370         $iconPaths = array(
371             WikiPath::createMediaPathFromId(':favicon.ico'),
372             WikiPath::createMediaPathFromId(':wiki:favicon.ico'),
373             $internalFavIcon
374         );
375         try {
376             /**
377              * @var WikiPath $icoWikiPath - we give wiki paths, we get wiki path
378              */
379             $icoWikiPath = FileSystems::getFirstExistingPath($iconPaths);
380         } catch (ExceptionNotFound $e) {
381             LogUtility::internalError("The internal fav icon ($internalFavIcon) should be at minimal found", self::CANONICAL);
382             return "";
383         }
384 
385         return TagAttributes::createEmpty()
386             ->addOutputAttributeValue("rel", "shortcut icon")
387             ->addOutputAttributeValue("href", FetcherRawLocalPath::createFromPath($icoWikiPath)->getFetchUrl()->toAbsoluteUrl()->toString())
388             ->toHtmlEmptyTag("link");
389 
390     }
391 
392     /**
393      * Add Icon Png (16x16 and 32x32)
394      * @return string
395      */
396     private function getIconHtmlLink(): string
397     {
398 
399         $html = "";
400         $sizeValues = ["32x32", "16x16"];
401         foreach ($sizeValues as $sizeValue) {
402 
403             $internalIcon = WikiPath::createComboResource(":images:favicon-$sizeValue.png");
404             $iconPaths = array(
405                 WikiPath::createMediaPathFromId(":favicon-$sizeValue.png"),
406                 WikiPath::createMediaPathFromId(":wiki:favicon-$sizeValue.png"),
407                 $internalIcon
408             );
409             try {
410                 /**
411                  * @var WikiPath $iconPath - to say to the linter that this is a wiki path
412                  */
413                 $iconPath = FileSystems::getFirstExistingPath($iconPaths);
414             } catch (ExceptionNotFound $e) {
415                 LogUtility::internalError("The internal icon ($internalIcon) should be at minimal found", self::CANONICAL);
416                 continue;
417             }
418             $html .= TagAttributes::createEmpty()
419                 ->addOutputAttributeValue("rel", "icon")
420                 ->addOutputAttributeValue("sizes", $sizeValue)
421                 ->addOutputAttributeValue("type", Mime::PNG)
422                 ->addOutputAttributeValue("href", FetcherRawLocalPath::createFromPath($iconPath)->getFetchUrl()->toAbsoluteUrl()->toString())
423                 ->toHtmlEmptyTag("link");
424         }
425         return $html;
426     }
427 
428     /**
429      * Add Apple touch icon
430      *
431      * @return string
432      */
433     private function getAppleTouchIconHtmlLink(): string
434     {
435 
436         $internalIcon = WikiPath::createComboResource(":images:apple-touch-icon.png");
437         $iconPaths = array(
438             WikiPath::createMediaPathFromId(":apple-touch-icon.png"),
439             WikiPath::createMediaPathFromId(":wiki:apple-touch-icon.png"),
440             $internalIcon
441         );
442         try {
443             /**
444              * @var WikiPath $iconPath - to say to the linter that this is a wiki path
445              */
446             $iconPath = FileSystems::getFirstExistingPath($iconPaths);
447         } catch (ExceptionNotFound $e) {
448             LogUtility::internalError("The internal apple icon ($internalIcon) should be at minimal found", self::CANONICAL);
449             return "";
450         }
451         try {
452             $fetcherLocalPath = FetcherRaster::createImageRasterFetchFromPath($iconPath);
453             $sizesValue = "{$fetcherLocalPath->getIntrinsicWidth()}x{$fetcherLocalPath->getIntrinsicHeight()}";
454 
455             return TagAttributes::createEmpty()
456                 ->addOutputAttributeValue("rel", self::APPLE_TOUCH_ICON_REL_VALUE)
457                 ->addOutputAttributeValue("sizes", $sizesValue)
458                 ->addOutputAttributeValue("type", Mime::PNG)
459                 ->addOutputAttributeValue("href", $fetcherLocalPath->getFetchUrl()->toAbsoluteUrl()->toString())
460                 ->toHtmlEmptyTag("link");
461         } catch (\Exception $e) {
462             LogUtility::internalError("The file ($iconPath) should be found and the local name should be good. Error: {$e->getMessage()}");
463             return "";
464         }
465     }
466 
467     public
468     function getModel(): array
469     {
470 
471         $executionConfig = ExecutionContext::getActualOrCreateFromEnv()->getConfig();
472 
473         /**
474          * Mandatory HTML attributes
475          */
476         $model =
477             [
478                 PageTitle::PROPERTY_NAME => $this->getRequestedTitleOrDefault(),
479                 Lang::PROPERTY_NAME => $this->getRequestedLangOrDefault()->getValueOrDefault(),
480                 // The direction is not yet calculated from the page, we let the browser determine it from the lang
481                 // dokuwiki has a direction config also ...
482                 // "dir" => $this->getRequestedLangOrDefault()->getDirection()
483             ];
484 
485         if (isset($this->model)) {
486             return array_merge($model, $this->model);
487         }
488 
489         /**
490          * The width of the layout
491          */
492         $container = $executionConfig->getValue(ContainerTag::DEFAULT_LAYOUT_CONTAINER_CONF, ContainerTag::DEFAULT_LAYOUT_CONTAINER_DEFAULT_VALUE);
493         $containerClass = ContainerTag::getClassName($container);
494         $model["layout-container-class"] = $containerClass;
495 
496 
497         /**
498          * The rem
499          */
500         try {
501             $model["rem-size"] = $executionConfig->getRemFontSize();
502         } catch (ExceptionNotFound $e) {
503             // ok none
504         }
505 
506 
507         /**
508          * Body class
509          * {@link tpl_classes} will add the dokuwiki class.
510          * See https://www.dokuwiki.org/devel:templates#dokuwiki_class
511          * dokuwiki__top ID is needed for the "Back to top" utility
512          * used also by some plugins
513          * dokwuiki as class is also needed as it's used by the linkwizard
514          * to locate where to add the node (ie .appendTo('.dokuwiki:first'))
515          */
516         $bodyDokuwikiClass = tpl_classes();
517         try {
518             $bodyTemplateIdentifierClass = StyleAttribute::addComboStrapSuffix("{$this->getTheme()}-{$this->getTemplateName()}");
519         } catch (\Exception $e) {
520             $bodyTemplateIdentifierClass = StyleAttribute::addComboStrapSuffix("template-string");
521         }
522         // position relative is for the toast and messages that are in the corner
523         $model['body-classes'] = "$bodyDokuwikiClass position-relative $bodyTemplateIdentifierClass";
524 
525         /**
526          * Data coupled to a page
527          */
528         try {
529 
530             $contextPath = $this->getRequestedContextPath();
531             $markupPath = MarkupPath::createPageFromPathObject($contextPath);
532             /**
533              * Meta
534              */
535             $metadata = $markupPath->getMetadataForRendering();
536             $model = array_merge($metadata, $model);
537 
538 
539             /**
540              * Railbar
541              * You can define the layout type by page
542              * This is not a handelbars helper because it needs some css snippet.
543              */
544             $railBarLayout = $this->getRailbarLayout();
545             try {
546                 $model["railbar-html"] = FetcherRailBar::createRailBar()
547                     ->setRequestedLayout($railBarLayout)
548                     ->setRequestedPath($contextPath)
549                     ->getFetchString();
550             } catch (ExceptionBadArgument $e) {
551                 LogUtility::error("Error while creating the railbar layout");
552             }
553 
554             /**
555              * Css Variables Colors
556              * Added for now in `head-partial.hbs`
557              */
558             try {
559                 $primaryColor = $executionConfig->getPrimaryColor();
560                 $model[BrandingColors::PRIMARY_COLOR_TEMPLATE_ATTRIBUTE] = $primaryColor->toCssValue();
561                 $model[BrandingColors::PRIMARY_COLOR_TEXT_ATTRIBUTE] = ColorSystem::toTextColor($primaryColor);
562                 $model[BrandingColors::PRIMARY_COLOR_TEXT_HOVER_ATTRIBUTE] = ColorSystem::toTextHoverColor($primaryColor);
563             } catch (ExceptionNotFound $e) {
564                 // not found
565                 $model[BrandingColors::PRIMARY_COLOR_TEMPLATE_ATTRIBUTE] = null;
566             }
567             try {
568                 $secondaryColor = $executionConfig->getSecondaryColor();
569                 $model[BrandingColors::SECONDARY_COLOR_TEMPLATE_ATTRIBUTE] = $secondaryColor->toCssValue();
570             } catch (ExceptionNotFound $e) {
571                 // not found
572             }
573 
574 
575             /**
576              * Main
577              */
578             if (isset($this->mainContent)) {
579                 $model["main-content-html"] = $this->mainContent;
580             } else {
581                 try {
582                     if (!$markupPath->isSlot()) {
583                         $requestedContextPathForMain = $this->getRequestedContextPath();
584                     } else {
585                         try {
586                             $markupContextPath = SlotSystem::getContextPath();
587                             SlotSystem::sendContextPathMessage($markupContextPath);
588                             $requestedContextPathForMain = $markupContextPath->toWikiPath();
589                         } catch (ExceptionNotFound|ExceptionCast $e) {
590                             $requestedContextPathForMain = $this->getRequestedContextPath();
591                         }
592                     }
593                     $model["main-content-html"] = FetcherMarkup::confRoot()
594                         ->setRequestedMimeToXhtml()
595                         ->setRequestedContextPath($requestedContextPathForMain)
596                         ->setRequestedExecutingPath($this->getRequestedContextPath())
597                         ->build()
598                         ->getFetchString();
599                 } catch (ExceptionCompile|ExceptionNotExists|ExceptionNotExists $e) {
600                     LogUtility::error("Error while rendering the page content.", self::CANONICAL, $e);
601                     $model["main-content-html"] = "An error has occured. " . $e->getMessage();
602                 }
603             }
604 
605             /**
606              * Toc (after main execution please)
607              */
608             $model['toc-class'] = Toc::getClass();
609             $model['toc-html'] = $this->getTocOrDefault()->toXhtml();
610 
611             /**
612              * Slots
613              */
614             foreach ($this->getSlots() as $slot) {
615 
616                 $elementId = $slot->getElementId();
617                 try {
618                     $model["$elementId-html"] = $slot->getMarkupFetcher()->getFetchString();
619                 } catch (ExceptionNotFound|ExceptionNotExists $e) {
620                     // no slot found
621                 } catch (ExceptionCompile $e) {
622                     LogUtility::error("Error while rendering the slot $elementId for the template ($this)", self::CANONICAL, $e);
623                     $model["$elementId-html"] = LogUtility::wrapInRedForHtml("Error: " . $e->getMessage());
624                 }
625             }
626 
627             /**
628              * Found in {@link tpl_content()}
629              * Used to add html such as {@link \action_plugin_combo_routermessage}
630              * Not sure if this is the right place to add it.
631              */
632             ob_start();
633             global $ACT;
634             \dokuwiki\Extension\Event::createAndTrigger('TPL_ACT_RENDER', $ACT);
635             $tplActRenderOutput = ob_get_clean();
636             if (!empty($tplActRenderOutput)) {
637                 $model["main-content-afterbegin-html"] = $tplActRenderOutput;
638                 $this->hadMessages = true;
639             }
640 
641         } catch (ExceptionNotFound $e) {
642             // no context path
643             if (isset($this->mainContent)) {
644                 $model["main-content-html"] = $this->mainContent;
645             }
646         }
647 
648 
649         /**
650          * Head Html
651          * Snippet, Css and Js from the layout if any
652          *
653          * Note that head tag may be added during rendering and must be then called after rendering and toc
654          * (ie at last then)
655          */
656         $model['head-html'] = $this->getHeadHtml();
657 
658         /**
659          * Preloaded Css
660          * (It must come after the head processing as this is where the preloaded script are defined)
661          * (Not really useful but legacy)
662          * We add it just before the end of the body tag
663          */
664         try {
665             $model['preloaded-stylesheet-html'] = $this->getHtmlForPreloadedStyleSheets();
666         } catch (ExceptionNotFound $e) {
667             // no preloaded stylesheet resources
668         }
669 
670         /**
671          * Powered by
672          */
673         $model['powered-by'] = self::getPoweredBy();
674 
675         /**
676          * Messages
677          * (Should come just before the page creation
678          * due to the $MSG_shown mechanism in {@link html_msgarea()}
679          * We may also get messages in the head
680          */
681         try {
682             $model['messages-html'] = $this->getMessages();
683             /**
684              * Because they must be problem and message with the {@link self::getHeadHtml()}
685              * We process the messages at the end
686              * It means that the needed script needs to be added manually
687              */
688             $model['head-html'] .= Snippet::getOrCreateFromComponentId("toast", Snippet::EXTENSION_JS)->toXhtml();
689         } catch (ExceptionNotFound $e) {
690             // no messages
691         } catch (ExceptionBadState $e) {
692             throw ExceptionRuntimeInternal::withMessageAndError("The toast snippet should have been found", $e);
693         }
694 
695         /**
696          * Task runner needs the id
697          */
698         if ($this->requestedEnableTaskRunner && isset($this->requestedContextPath)) {
699             $model['task-runner-html'] = $this->getTaskRunnerImg();
700         }
701 
702         return $model;
703     }
704 
705 
706     private
707     function getRequestedTitleOrDefault(): string
708     {
709 
710         if (isset($this->requestedTitle)) {
711             return $this->requestedTitle;
712         }
713 
714         try {
715             $path = $this->getRequestedContextPath();
716             $markupPath = MarkupPath::createPageFromPathObject($path);
717             return PageTitle::createForMarkup($markupPath)->getValueOrDefault();
718         } catch (ExceptionNotFound $e) {
719             //
720         }
721         throw new ExceptionBadSyntaxRuntime("A title is mandatory");
722 
723 
724     }
725 
726 
727     /**
728      * @throws ExceptionNotFound
729      */
730     private
731     function getTocOrDefault(): Toc
732     {
733 
734         if (isset($this->toc)) {
735             /**
736              * The {@link FetcherPageBundler}
737              * bundle pages can create a toc for multiples pages
738              */
739             return $this->toc;
740         }
741 
742         $wikiPath = $this->getRequestedContextPath();
743         if (FileSystems::isDirectory($wikiPath)) {
744             LogUtility::error("We have a found an inconsistency. The context path is a directory and does have therefore no toc but the template ($this) has a toc.");
745         }
746         $markup = MarkupPath::createPageFromPathObject($wikiPath);
747         return Toc::createForPage($markup);
748 
749     }
750 
751     public
752     function setMainContent(string $mainContent): TemplateForWebPage
753     {
754         $this->mainContent = $mainContent;
755         return $this;
756     }
757 
758 
759     /**
760      * @throws ExceptionBadSyntax
761      */
762     public
763     function renderAsDom(): XmlDocument
764     {
765         return XmlDocument::createHtmlDocFromMarkup($this->render());
766     }
767 
768     /**
769      * Add the preloaded CSS resources
770      * at the end
771      * @throws ExceptionNotFound
772      */
773     private
774     function getHtmlForPreloadedStyleSheets(): string
775     {
776 
777         // For the preload if any
778         try {
779             $executionContext = ExecutionContext::getActualOrCreateFromEnv();
780             $preloadedCss = $executionContext->getRuntimeObject(self::PRELOAD_TAG);
781         } catch (ExceptionNotFound $e) {
782             throw new ExceptionNotFound("No preloaded resources found");
783         }
784 
785         //
786         // Note: Adding this css in an animationFrame
787         // such as https://github.com/jakearchibald/svgomg/blob/master/src/index.html#L183
788         // would be difficult to test
789 
790         $class = StyleAttribute::addComboStrapSuffix(self::PRELOAD_TAG);
791         $preloadHtml = "<div class=\"$class\">";
792         foreach ($preloadedCss as $link) {
793             $htmlLink = '<link rel="stylesheet" href="' . $link['href'] . '" ';
794             if (($link['crossorigin'] ?? '') != "") {
795                 $htmlLink .= ' crossorigin="' . $link['crossorigin'] . '" ';
796             }
797             if (!empty(($link['class'] ?? null))) {
798                 $htmlLink .= ' class="' . $link['class'] . '" ';
799             }
800             // No integrity here
801             $htmlLink .= '>';
802             $preloadHtml .= $htmlLink;
803         }
804         $preloadHtml .= "</div>";
805         return $preloadHtml;
806 
807     }
808 
809     /**
810      * Variation of {@link html_msgarea()}
811      * @throws ExceptionNotFound
812      */
813     public
814     function getMessages(): string
815     {
816 
817         global $MSG;
818 
819         if (!isset($MSG)) {
820             throw new ExceptionNotFound("No messages");
821         }
822 
823         // deduplicate and auth
824         $uniqueMessages = [];
825         foreach ($MSG as $msg) {
826             if (!info_msg_allowed($msg)) {
827                 continue;
828             }
829             $hash = md5($msg['msg']);
830             $uniqueMessages[$hash] = $msg;
831         }
832 
833         $messagesByLevel = [];
834         foreach ($uniqueMessages as $message) {
835             $level = $message['lvl'];
836             $messagesByLevel[$level][] = $message;
837         }
838 
839         $toasts = "";
840         foreach ($messagesByLevel as $level => $messagesForLevel) {
841             $level = ucfirst($level);
842             switch ($level) {
843                 case "Error":
844                     $class = "text-danger";
845                     $levelName = "Error";
846                     break;
847                 case "Notify":
848                     $class = "text-warning";
849                     $levelName = "Warning";
850                     break;
851                 default:
852                     $levelName = $level;
853                     $class = "text-primary";
854                     break;
855             }
856             $autoHide = "false"; // auto-hidding is really bad ui
857             $toastMessage = "";
858             foreach ($messagesForLevel as $messageForLevel) {
859                 $toastMessage .= "<p>{$messageForLevel['msg']}</p>";
860             }
861 
862 
863             $toasts .= <<<EOF
864 <div role="alert" aria-live="assertive" aria-atomic="true" class="toast fade" data-bs-autohide="$autoHide">
865   <div class="toast-header">
866     <strong class="me-auto $class">{$levelName}</strong>
867     <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
868   </div>
869   <div class="toast-body">
870         $toastMessage
871   </div>
872 </div>
873 EOF;
874         }
875 
876         unset($GLOBALS['MSG']);
877 
878         if ($toasts === "") {
879             throw new ExceptionNotFound("No messages");
880         }
881 
882         $this->hadMessages = true;
883 
884         // position fixed to not participate into the grid
885         return <<<EOF
886 <div class="toast-container position-fixed mb-3 me-3 bottom-0 end-0" id="toastPlacement" style="z-index:1060">
887 $toasts
888 </div>
889 EOF;
890 
891     }
892 
893     private
894     function canBeCached(): bool
895     {
896         // no if message
897         return true;
898     }
899 
900     /**
901      * Adapted from {@link tpl_indexerWebBug()}
902      * @return string
903      */
904     private
905     function getTaskRunnerImg(): string
906     {
907 
908         try {
909             $htmlUrl = UrlEndpoint::createTaskRunnerUrl()
910                 ->addQueryParameter(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $this->getRequestedContextPath()->getWikiId())
911                 ->addQueryParameter(time())
912                 ->toString();
913         } catch (ExceptionNotFound $e) {
914             throw new ExceptionRuntimeInternal("A request path is mandatory when adding a task runner. Disable it if you don't want one in the layout ($this).");
915         }
916 
917         // no more 1x1 px image because of ad blockers
918         return TagAttributes::createEmpty()
919             ->addOutputAttributeValue("id", TemplateForWebPage::TASK_RUNNER_ID)
920             ->addClassName("d-none")
921             ->addOutputAttributeValue('width', 2)
922             ->addOutputAttributeValue('height', 1)
923             ->addOutputAttributeValue('alt', 'Task Runner')
924             ->addOutputAttributeValue('src', $htmlUrl)
925             ->toHtmlEmptyTag("img");
926     }
927 
928     private
929     function getRequestedLangOrDefault(): Lang
930     {
931         try {
932             return $this->getRequestedLang();
933         } catch (ExceptionNotFound $e) {
934             return Lang::createFromValue("en");
935         }
936     }
937 
938     private
939     function getTheme(): string
940     {
941         return $this->requestedTheme ?? ExecutionContext::getActualOrCreateFromEnv()->getConfig()->getTheme();
942     }
943 
944     private
945     function getHeadHtml(): string
946     {
947         $snippetManager = PluginUtility::getSnippetManager();
948 
949         if (!$this->isTemplateStringExecutionMode()) {
950 
951             /**
952              * Add the layout js and css first
953              */
954 
955             try {
956                 $cssPath = $this->getCssPath();
957                 $content = FileSystems::getContent($cssPath);
958                 $snippetManager->attachCssInternalStylesheet(self::CANONICAL, $content);
959             } catch (ExceptionNotFound $e) {
960                 // no css found, not a problem
961             }
962             try {
963                 $jsPath = $this->getJsPath();
964                 $snippetManager->attachInternalJavascriptFromPathForRequest(self::CANONICAL, $jsPath);
965             } catch (ExceptionNotFound $e) {
966                 // not found
967             }
968 
969 
970         }
971 
972         /**
973          * Dokuwiki Smiley does not have any height
974          */
975         $snippetManager->attachCssInternalStyleSheet("dokuwiki-smiley");
976 
977         /**
978          * Iframe
979          */
980         if ($this->isIframe) {
981             global $EVENT_HANDLER;
982             $EVENT_HANDLER->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'onlyIframeHeadTags');
983         }
984         /**
985          * Start the meta headers
986          */
987         ob_start();
988         try {
989             tpl_metaheaders();
990             $headIcon = $this->getPageIconHeadLinkHtml();
991             return $headIcon . ob_get_contents();
992         } finally {
993             ob_end_clean();
994         }
995 
996     }
997 
998 
999     public
1000     function setRequestedTemplateName(string $templateName): TemplateForWebPage
1001     {
1002         $this->templateName = $templateName;
1003         return $this;
1004     }
1005 
1006     /**
1007      * Add or not the task runner / web bug call
1008      * @param bool $b
1009      * @return TemplateForWebPage
1010      */
1011     public
1012     function setRequestedEnableTaskRunner(bool $b): TemplateForWebPage
1013     {
1014         $this->requestedEnableTaskRunner = $b;
1015         return $this;
1016     }
1017 
1018 
1019     /**
1020      * @param Lang $requestedLang
1021      * @return TemplateForWebPage
1022      */
1023     public
1024     function setRequestedLang(Lang $requestedLang): TemplateForWebPage
1025     {
1026         $this->requestedLang = $requestedLang;
1027         return $this;
1028     }
1029 
1030     /**
1031      * @param string $requestedTitle
1032      * @return TemplateForWebPage
1033      */
1034     public
1035     function setRequestedTitle(string $requestedTitle): TemplateForWebPage
1036     {
1037         $this->requestedTitle = $requestedTitle;
1038         return $this;
1039     }
1040 
1041     /**
1042      * Delete the social head tags
1043      * (ie the page should not be indexed)
1044      * This is used for iframe content for instance
1045      * @param bool $isSocial
1046      * @return TemplateForWebPage
1047      */
1048     public
1049     function setIsSocial(bool $isSocial): TemplateForWebPage
1050     {
1051         $this->isSocial = $isSocial;
1052         return $this;
1053     }
1054 
1055     public
1056     function setRequestedContextPath(WikiPath $contextPath): TemplateForWebPage
1057     {
1058         $this->requestedContextPath = $contextPath;
1059         return $this;
1060     }
1061 
1062     public
1063     function setToc(Toc $toc): TemplateForWebPage
1064     {
1065         $this->toc = $toc;
1066         return $this;
1067     }
1068 
1069     /**
1070      * There is two mode of execution, via:
1071      * * a file template (theme)
1072      * * or a string template (string)
1073      *
1074      * @return bool - true if this a string template executions
1075      */
1076     private
1077     function isTemplateStringExecutionMode(): bool
1078     {
1079         return isset($this->templateString);
1080     }
1081 
1082     private
1083     function getEngine(): TemplateEngine
1084     {
1085         if ($this->isTemplateStringExecutionMode()) {
1086             return TemplateEngine::createForString();
1087 
1088         } else {
1089             $theme = $this->getTheme();
1090             return TemplateEngine::createForTheme($theme);
1091         }
1092     }
1093 
1094     private
1095     function getDefinition(): array
1096     {
1097         try {
1098             if (isset($this->templateDefinition)) {
1099                 return $this->templateDefinition;
1100             }
1101             $file = $this->getEngine()->searchTemplateByName("{$this->getTemplateName()}.yml");
1102             if (!FileSystems::exists($file)) {
1103                 return [];
1104             }
1105             $this->templateDefinition = Yaml::parseFile($file->toAbsoluteId());
1106             return $this->templateDefinition;
1107         } catch (ExceptionNotFound $e) {
1108             // no template directory, not a theme run
1109             return [];
1110         }
1111     }
1112 
1113     private
1114     function getRailbarLayout(): string
1115     {
1116         $definition = $this->getDefinition();
1117         if (isset($definition['railbar']['layout'])) {
1118             return $definition['railbar']['layout'];
1119         }
1120         return FetcherRailBar::BOTH_LAYOUT;
1121     }
1122 
1123     /**
1124      * Keep the only iframe head tag needed
1125      * @param $event
1126      * @return void
1127      */
1128     public
1129     function onlyIframeHeadTags(&$event)
1130     {
1131 
1132         $data = &$event->data;
1133         foreach ($data as $tag => &$heads) {
1134             switch ($tag) {
1135                 case "link":
1136                     $deletedRel = ["manifest", "search", "start", "alternate", "canonical"];
1137                     foreach ($heads as $id => $headAttributes) {
1138                         if (isset($headAttributes['rel'])) {
1139                             $rel = $headAttributes['rel'];
1140                             if (in_array($rel, $deletedRel)) {
1141                                 unset($heads[$id]);
1142                             }
1143                             if ($rel === "stylesheet") {
1144                                 $href = $headAttributes['href'];
1145                                 if (strpos($href, "lib/exe/css.php") !== false) {
1146                                     unset($heads[$id]);
1147                                 }
1148                             }
1149                         }
1150                     }
1151                     break;
1152                 case "meta":
1153                     $deletedMeta = ["og:url", "og:description", "description", "robots"];
1154                     foreach ($heads as $id => $headAttributes) {
1155                         if (isset($headAttributes['name']) || isset($headAttributes['property'])) {
1156                             $rel = $headAttributes['name'] ?? null;
1157                             if ($rel === null) {
1158                                 $rel = $headAttributes['property'] ?? null;
1159                             }
1160                             if (in_array($rel, $deletedMeta)) {
1161                                 unset($heads[$id]);
1162                             }
1163                         }
1164                     }
1165                     break;
1166                 case "script":
1167                     foreach ($heads as $id => $headAttributes) {
1168                         if (isset($headAttributes['src'])) {
1169                             $src = $headAttributes['src'];
1170                             if (strpos($src, "lib/exe/js.php") !== false) {
1171                                 unset($heads[$id]);
1172                             }
1173                             if (strpos($src, "lib/exe/jquery.php") !== false) {
1174                                 unset($heads[$id]);
1175                             }
1176                         }
1177                     }
1178                     break;
1179             }
1180         }
1181     }
1182 
1183 }
1184