*
* The eol `\n` is needed for lightouse
*/
const DOCTYPE = "\n";
private array $templateDefinition;
const CANONICAL = "template";
public const UTF_8_CHARSET_VALUE = "utf-8";
public const VIEWPORT_RESPONSIVE_VALUE = "width=device-width, initial-scale=1";
public const TASK_RUNNER_ID = "task-runner";
public const APPLE_TOUCH_ICON_REL_VALUE = "apple-touch-icon";
public const PRELOAD_TAG = "preload";
private string $templateName;
private string $requestedTitle;
private bool $requestedEnableTaskRunner = true;
private WikiPath $requestedContextPath;
private Lang $requestedLang;
private Toc $toc;
private bool $isSocial;
private string $mainContent;
private string $templateString;
private array $model;
private bool $hadMessages = false;
private string $requestedTheme;
private bool $isIframe = false;
private array $slots;
public static function create(): TemplateForWebPage
{
return new TemplateForWebPage();
}
public static function config(): TemplateForWebPage
{
return new TemplateForWebPage();
}
public static function getPoweredBy(): string
{
$domain = PluginUtility::$URL_APEX;
$version = PluginUtility::$INFO_PLUGIN['version'] . " (" . PluginUtility::$INFO_PLUGIN['date'] . ")";
$poweredBy = "
";
$poweredBy .= " Powered by ComboStrap";
$poweredBy .= '
';
return $poweredBy;
}
/**
* @throws ExceptionNotFound
*/
public function getHtmlTemplatePath(): LocalPath
{
return $this->getEngine()->searchTemplateByName($this->templateName . "." . TemplateEngine::EXTENSION_HBS);
}
public function setTemplateString(string $templateString): TemplateForWebPage
{
$this->templateString = $templateString;
return $this;
}
public function setModel(array $model): TemplateForWebPage
{
$this->model = $model;
return $this;
}
/**
* @return WikiPath from where the markup slot should be searched
* @throws ExceptionNotFound
*/
public function getRequestedContextPath(): WikiPath
{
if (!isset($this->requestedContextPath)) {
throw new ExceptionNotFound("A requested context path was not found");
}
return $this->requestedContextPath;
}
/**
*
* @return string - the page as html string (not dom because that's not how works dokuwiki)
*
*/
public function render(): string
{
$executionContext = (ExecutionContext::getActualOrCreateFromEnv())
->setExecutingPageTemplate($this);
/**
* The deprecated report are just messing up html
*/
$oldLevel = error_reporting(E_ALL ^ E_DEPRECATED);
try {
$pageTemplateEngine = $this->getEngine();
if ($this->isTemplateStringExecutionMode()) {
$template = $this->templateString;
} else {
$pageTemplateEngine = $this->getEngine();
$template = $this->getTemplateName();
if (!$pageTemplateEngine->templateExists($template)) {
$defaultTemplate = PageTemplateName::HOLY_TEMPLATE_VALUE;
LogUtility::warning("The template ($template) was not found, the default template ($defaultTemplate) was used instead.");
$template = $defaultTemplate;
$this->setRequestedTemplateName($template);
}
}
/**
* Get model should came after template validation
* as the template definition is named dependent
* (Create a builder, nom de dieu)
*/
$model = $this->getModel();
return self::DOCTYPE . $pageTemplateEngine->renderWebPage($template, $model);
} finally {
error_reporting($oldLevel);
$executionContext
->closeExecutingPageTemplate();
}
}
/**
* @return string[]
*/
public function getElementIds(): array
{
$definition = $this->getDefinition();
$elements = $definition['elements'] ?? null;
if ($elements == null) {
return [];
}
return $elements;
}
/**
* @throws ExceptionNotFound
*/
private function getRequestedLang(): Lang
{
if (!isset($this->requestedLang)) {
throw new ExceptionNotFound("No requested lang");
}
return $this->requestedLang;
}
public function getTemplateName(): string
{
if (isset($this->templateName)) {
return $this->templateName;
}
try {
$requestedPath = $this->getRequestedContextPath();
return PageTemplateName::createFromPage(MarkupPath::createPageFromPathObject($requestedPath))
->getValueOrDefault();
} catch (ExceptionNotFound $e) {
// no requested path
}
return ExecutionContext::getActualOrCreateFromEnv()
->getConfig()
->getDefaultLayoutName();
}
public function __toString()
{
return $this->templateName;
}
/**
* @throws ExceptionNotFound
*/
public function getCssPath(): LocalPath
{
return $this->getEngine()->searchTemplateByName("$this->templateName.css");
}
/**
* @throws ExceptionNotFound
*/
public function getJsPath(): LocalPath
{
$jsPath = $this->getEngine()->searchTemplateByName("$this->templateName.js");
if (!FileSystems::exists($jsPath)) {
throw new ExceptionNotFound("No js file");
}
return $jsPath;
}
public function hasMessages(): bool
{
return $this->hadMessages;
}
public function setRequestedTheme(string $themeName): TemplateForWebPage
{
$this->requestedTheme = $themeName;
return $this;
}
public function hasElement(string $elementId): bool
{
return in_array($elementId, $this->getElementIds());
}
public function isSocial(): bool
{
if (isset($this->isSocial)) {
return $this->isSocial;
}
try {
$path = $this->getRequestedContextPath();
if (!FileSystems::exists($path)) {
return false;
}
$markup = MarkupPath::createPageFromPathObject($path);
if ($markup->isSlot()) {
// slot are not social
return false;
}
} catch (ExceptionNotFound $e) {
// not a path run
return false;
}
if ($this->isIframe) {
return false;
}
return ExecutionContext::getActualOrCreateFromEnv()
->getConfig()
->getBooleanValue(self::CONF_INTERNAL_IS_SOCIAL, true);
}
public function setIsIframe(bool $isIframe): TemplateForWebPage
{
$this->isIframe = $isIframe;
return $this;
}
/**
* @return TemplateSlot[]
*/
public function getSlots(): array
{
if (isset($this->slots)) {
return $this->slots;
}
$this->slots = [];
foreach ($this->getElementIds() as $elementId) {
if ($elementId === TemplateSlot::MAIN_TOC_ID) {
/**
* Main toc element is not a slot
*/
continue;
}
try {
$this->slots[] = TemplateSlot::createFromElementId($elementId, $this->getRequestedContextPath());
} catch (ExceptionNotFound $e) {
LogUtility::internalError("This template is not for a markup path, it cannot have slots then.");
}
}
return $this->slots;
}
/**
* Character set
* Note: avoid using {@link Html::encode() character entities} in your HTML,
* provided their encoding matches that of the document (generally UTF-8)
*/
private function checkCharSetMeta(XmlElement $head)
{
$charsetValue = TemplateForWebPage::UTF_8_CHARSET_VALUE;
try {
$metaCharset = $head->querySelector("meta[charset]");
$charsetActualValue = $metaCharset->getAttribute("charset");
if ($charsetActualValue !== $charsetValue) {
LogUtility::warning("The actual charset ($charsetActualValue) should be $charsetValue");
}
} catch (ExceptionBadSyntax|ExceptionNotFound $e) {
try {
$metaCharset = $head->getDocument()
->createElement("meta")
->setAttribute("charset", $charsetValue);
$head->appendChild($metaCharset);
} catch (\DOMException $e) {
throw new ExceptionRuntimeInternal("Bad local name meta, should not occur", self::CANONICAL, 1, $e);
}
}
}
/**
* @param XmlElement $head
* @return void
* Adapted from {@link TplUtility::renderFaviconMetaLinks()}
*/
private function getPageIconHeadLinkHtml(): string
{
$html = $this->getShortcutFavIconHtmlLink();
$html .= $this->getIconHtmlLink();
$html .= $this->getAppleTouchIconHtmlLink();
return $html;
}
/**
* Add a favIcon.ico
*
*/
private function getShortcutFavIconHtmlLink(): string
{
$internalFavIcon = WikiPath::createComboResource('images:favicon.ico');
$iconPaths = array(
WikiPath::createMediaPathFromId(':favicon.ico'),
WikiPath::createMediaPathFromId(':wiki:favicon.ico'),
$internalFavIcon
);
try {
/**
* @var WikiPath $icoWikiPath - we give wiki paths, we get wiki path
*/
$icoWikiPath = FileSystems::getFirstExistingPath($iconPaths);
} catch (ExceptionNotFound $e) {
LogUtility::internalError("The internal fav icon ($internalFavIcon) should be at minimal found", self::CANONICAL);
return "";
}
return TagAttributes::createEmpty()
->addOutputAttributeValue("rel", "shortcut icon")
->addOutputAttributeValue("href", FetcherRawLocalPath::createFromPath($icoWikiPath)->getFetchUrl()->toAbsoluteUrl()->toString())
->toHtmlEmptyTag("link");
}
/**
* Add Icon Png (16x16 and 32x32)
* @return string
*/
private function getIconHtmlLink(): string
{
$html = "";
$sizeValues = ["32x32", "16x16"];
foreach ($sizeValues as $sizeValue) {
$internalIcon = WikiPath::createComboResource(":images:favicon-$sizeValue.png");
$iconPaths = array(
WikiPath::createMediaPathFromId(":favicon-$sizeValue.png"),
WikiPath::createMediaPathFromId(":wiki:favicon-$sizeValue.png"),
$internalIcon
);
try {
/**
* @var WikiPath $iconPath - to say to the linter that this is a wiki path
*/
$iconPath = FileSystems::getFirstExistingPath($iconPaths);
} catch (ExceptionNotFound $e) {
LogUtility::internalError("The internal icon ($internalIcon) should be at minimal found", self::CANONICAL);
continue;
}
$html .= TagAttributes::createEmpty()
->addOutputAttributeValue("rel", "icon")
->addOutputAttributeValue("sizes", $sizeValue)
->addOutputAttributeValue("type", Mime::PNG)
->addOutputAttributeValue("href", FetcherRawLocalPath::createFromPath($iconPath)->getFetchUrl()->toAbsoluteUrl()->toString())
->toHtmlEmptyTag("link");
}
return $html;
}
/**
* Add Apple touch icon
*
* @return string
*/
private function getAppleTouchIconHtmlLink(): string
{
$internalIcon = WikiPath::createComboResource(":images:apple-touch-icon.png");
$iconPaths = array(
WikiPath::createMediaPathFromId(":apple-touch-icon.png"),
WikiPath::createMediaPathFromId(":wiki:apple-touch-icon.png"),
$internalIcon
);
try {
/**
* @var WikiPath $iconPath - to say to the linter that this is a wiki path
*/
$iconPath = FileSystems::getFirstExistingPath($iconPaths);
} catch (ExceptionNotFound $e) {
LogUtility::internalError("The internal apple icon ($internalIcon) should be at minimal found", self::CANONICAL);
return "";
}
try {
$fetcherLocalPath = FetcherRaster::createImageRasterFetchFromPath($iconPath);
$sizesValue = "{$fetcherLocalPath->getIntrinsicWidth()}x{$fetcherLocalPath->getIntrinsicHeight()}";
return TagAttributes::createEmpty()
->addOutputAttributeValue("rel", self::APPLE_TOUCH_ICON_REL_VALUE)
->addOutputAttributeValue("sizes", $sizesValue)
->addOutputAttributeValue("type", Mime::PNG)
->addOutputAttributeValue("href", $fetcherLocalPath->getFetchUrl()->toAbsoluteUrl()->toString())
->toHtmlEmptyTag("link");
} catch (\Exception $e) {
LogUtility::internalError("The file ($iconPath) should be found and the local name should be good. Error: {$e->getMessage()}");
return "";
}
}
public
function getModel(): array
{
$executionConfig = ExecutionContext::getActualOrCreateFromEnv()->getConfig();
/**
* Mandatory HTML attributes
*/
$model =
[
PageTitle::PROPERTY_NAME => $this->getRequestedTitleOrDefault(),
Lang::PROPERTY_NAME => $this->getRequestedLangOrDefault()->getValueOrDefault(),
// The direction is not yet calculated from the page, we let the browser determine it from the lang
// dokuwiki has a direction config also ...
// "dir" => $this->getRequestedLangOrDefault()->getDirection()
];
if (isset($this->model)) {
return array_merge($model, $this->model);
}
/**
* The width of the layout
*/
$container = $executionConfig->getValue(ContainerTag::DEFAULT_LAYOUT_CONTAINER_CONF, ContainerTag::DEFAULT_LAYOUT_CONTAINER_DEFAULT_VALUE);
$containerClass = ContainerTag::getClassName($container);
$model["layout-container-class"] = $containerClass;
/**
* The rem
*/
try {
$model["rem-size"] = $executionConfig->getRemFontSize();
} catch (ExceptionNotFound $e) {
// ok none
}
/**
* Body class
* {@link tpl_classes} will add the dokuwiki class.
* See https://www.dokuwiki.org/devel:templates#dokuwiki_class
* dokuwiki__top ID is needed for the "Back to top" utility
* used also by some plugins
* dokwuiki as class is also needed as it's used by the linkwizard
* to locate where to add the node (ie .appendTo('.dokuwiki:first'))
*/
$bodyDokuwikiClass = tpl_classes();
try {
$bodyTemplateIdentifierClass = StyleAttribute::addComboStrapSuffix("{$this->getTheme()}-{$this->getTemplateName()}");
} catch (\Exception $e) {
$bodyTemplateIdentifierClass = StyleAttribute::addComboStrapSuffix("template-string");
}
// position relative is for the toast and messages that are in the corner
$model['body-classes'] = "$bodyDokuwikiClass position-relative $bodyTemplateIdentifierClass";
/**
* Data coupled to a page
*/
try {
$contextPath = $this->getRequestedContextPath();
$markupPath = MarkupPath::createPageFromPathObject($contextPath);
/**
* Meta
*/
$metadata = $markupPath->getMetadataForRendering();
$model = array_merge($metadata, $model);
/**
* Railbar
* You can define the layout type by page
* This is not a handelbars helper because it needs some css snippet.
*/
$railBarLayout = $this->getRailbarLayout();
try {
$model["railbar-html"] = FetcherRailBar::createRailBar()
->setRequestedLayout($railBarLayout)
->setRequestedPath($contextPath)
->getFetchString();
} catch (ExceptionBadArgument $e) {
LogUtility::error("Error while creating the railbar layout");
}
/**
* Css Variables Colors
* Added for now in `head-partial.hbs`
*/
try {
$primaryColor = $executionConfig->getPrimaryColor();
$model[BrandingColors::PRIMARY_COLOR_TEMPLATE_ATTRIBUTE] = $primaryColor->toCssValue();
$model[BrandingColors::PRIMARY_COLOR_TEXT_ATTRIBUTE] = ColorSystem::toTextColor($primaryColor);
$model[BrandingColors::PRIMARY_COLOR_TEXT_HOVER_ATTRIBUTE] = ColorSystem::toTextHoverColor($primaryColor);
} catch (ExceptionNotFound $e) {
// not found
$model[BrandingColors::PRIMARY_COLOR_TEMPLATE_ATTRIBUTE] = null;
}
try {
$secondaryColor = $executionConfig->getSecondaryColor();
$model[BrandingColors::SECONDARY_COLOR_TEMPLATE_ATTRIBUTE] = $secondaryColor->toCssValue();
} catch (ExceptionNotFound $e) {
// not found
}
/**
* Main
*/
if (isset($this->mainContent)) {
$model["main-content-html"] = $this->mainContent;
} else {
try {
if (!$markupPath->isSlot()) {
$requestedContextPathForMain = $this->getRequestedContextPath();
} else {
try {
$markupContextPath = SlotSystem::getContextPath();
SlotSystem::sendContextPathMessage($markupContextPath);
$requestedContextPathForMain = $markupContextPath->toWikiPath();
} catch (ExceptionNotFound|ExceptionCast $e) {
$requestedContextPathForMain = $this->getRequestedContextPath();
}
}
$model["main-content-html"] = FetcherMarkup::confRoot()
->setRequestedMimeToXhtml()
->setRequestedContextPath($requestedContextPathForMain)
->setRequestedExecutingPath($this->getRequestedContextPath())
->build()
->getFetchString();
} catch (ExceptionCompile|ExceptionNotExists|ExceptionNotExists $e) {
LogUtility::error("Error while rendering the page content.", self::CANONICAL, $e);
$model["main-content-html"] = "An error has occured. " . $e->getMessage();
}
}
/**
* Toc (after main execution please)
*/
$model['toc-class'] = Toc::getClass();
$model['toc-html'] = $this->getTocOrDefault()->toXhtml();
/**
* Slots
*/
foreach ($this->getSlots() as $slot) {
$elementId = $slot->getElementId();
try {
$model["$elementId-html"] = $slot->getMarkupFetcher()->getFetchString();
} catch (ExceptionNotFound|ExceptionNotExists $e) {
// no slot found
} catch (ExceptionCompile $e) {
LogUtility::error("Error while rendering the slot $elementId for the template ($this)", self::CANONICAL, $e);
$model["$elementId-html"] = LogUtility::wrapInRedForHtml("Error: " . $e->getMessage());
}
}
/**
* Found in {@link tpl_content()}
* Used to add html such as {@link \action_plugin_combo_routermessage}
* Not sure if this is the right place to add it.
*/
ob_start();
global $ACT;
\dokuwiki\Extension\Event::createAndTrigger('TPL_ACT_RENDER', $ACT);
$tplActRenderOutput = ob_get_clean();
if (!empty($tplActRenderOutput)) {
$model["main-content-afterbegin-html"] = $tplActRenderOutput;
$this->hadMessages = true;
}
} catch (ExceptionNotFound $e) {
// no context path
if (isset($this->mainContent)) {
$model["main-content-html"] = $this->mainContent;
}
}
/**
* Head Html
* Snippet, Css and Js from the layout if any
*
* Note that head tag may be added during rendering and must be then called after rendering and toc
* (ie at last then)
*/
$model['head-html'] = $this->getHeadHtml();
/**
* Preloaded Css
* (It must come after the head processing as this is where the preloaded script are defined)
* (Not really useful but legacy)
* We add it just before the end of the body tag
*/
try {
$model['preloaded-stylesheet-html'] = $this->getHtmlForPreloadedStyleSheets();
} catch (ExceptionNotFound $e) {
// no preloaded stylesheet resources
}
/**
* Powered by
*/
$model['powered-by'] = self::getPoweredBy();
/**
* Messages
* (Should come just before the page creation
* due to the $MSG_shown mechanism in {@link html_msgarea()}
* We may also get messages in the head
*/
try {
$model['messages-html'] = $this->getMessages();
/**
* Because they must be problem and message with the {@link self::getHeadHtml()}
* We process the messages at the end
* It means that the needed script needs to be added manually
*/
$model['head-html'] .= Snippet::getOrCreateFromComponentId("toast", Snippet::EXTENSION_JS)->toXhtml();
} catch (ExceptionNotFound $e) {
// no messages
} catch (ExceptionBadState $e) {
throw ExceptionRuntimeInternal::withMessageAndError("The toast snippet should have been found", $e);
}
/**
* Task runner needs the id
*/
if ($this->requestedEnableTaskRunner && isset($this->requestedContextPath)) {
$model['task-runner-html'] = $this->getTaskRunnerImg();
}
return $model;
}
private
function getRequestedTitleOrDefault(): string
{
if (isset($this->requestedTitle)) {
return $this->requestedTitle;
}
try {
$path = $this->getRequestedContextPath();
$markupPath = MarkupPath::createPageFromPathObject($path);
return PageTitle::createForMarkup($markupPath)->getValueOrDefault();
} catch (ExceptionNotFound $e) {
//
}
throw new ExceptionBadSyntaxRuntime("A title is mandatory");
}
/**
* @throws ExceptionNotFound
*/
private
function getTocOrDefault(): Toc
{
if (isset($this->toc)) {
/**
* The {@link FetcherPageBundler}
* bundle pages can create a toc for multiples pages
*/
return $this->toc;
}
$wikiPath = $this->getRequestedContextPath();
if (FileSystems::isDirectory($wikiPath)) {
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.");
}
$markup = MarkupPath::createPageFromPathObject($wikiPath);
return Toc::createForPage($markup);
}
public
function setMainContent(string $mainContent): TemplateForWebPage
{
$this->mainContent = $mainContent;
return $this;
}
/**
* @throws ExceptionBadSyntax
*/
public
function renderAsDom(): XmlDocument
{
return XmlDocument::createHtmlDocFromMarkup($this->render());
}
/**
* Add the preloaded CSS resources
* at the end
* @throws ExceptionNotFound
*/
private
function getHtmlForPreloadedStyleSheets(): string
{
// For the preload if any
try {
$executionContext = ExecutionContext::getActualOrCreateFromEnv();
$preloadedCss = $executionContext->getRuntimeObject(self::PRELOAD_TAG);
} catch (ExceptionNotFound $e) {
throw new ExceptionNotFound("No preloaded resources found");
}
//
// Note: Adding this css in an animationFrame
// such as https://github.com/jakearchibald/svgomg/blob/master/src/index.html#L183
// would be difficult to test
$class = StyleAttribute::addComboStrapSuffix(self::PRELOAD_TAG);
$preloadHtml = "