psturtle.com/layout.ps1
<# .SYNOPSIS Layout script .DESCRIPTION This script is used to layout a page with a consistent style and structure. If a file generates HTML but does not include a `<html>` tag, it's output should be piped to this script. Any directories without a layout should use the nearest `layout.ps1` file in a parent directory. Layout parameters can be provided by the site or page. #> param( # The name of the palette to use. [Alias('Palette')] [string] $PaletteName = $( if ($Site -and $Site['PaletteName']) { $Site['PaletteName'] } else { 'Konsolas' } ), # The Google Font name [Alias('FontName')] [string] $Font = $( if ($Site -and $Site['FontName']) { $Site['FontName'] } else { 'Roboto' } ), # The Google Code Font name [string] $CodeFont = $( if ($Site -and $Site['CodeFontName']) { $Site['CodeFontName'] } else { 'Inconsolata' } ), # The urls for any fav icons. [string[]] $FavIcon, # The taskbar icons. # The key should be the icon name or content, and the value should be the URL. # SVG icons should be included inline so they may be stylized. [Collections.IDictionary] $Taskbar = $( if ($page -and $page['Taskbar']) { $page.Taskbar } elseif ($Site -and $site['Taskbar']) { $site['Taskbar'] } else { [Ordered]@{} } ), # The header menu. [Collections.IDictionary] $HeaderMenu = $( if ($page -and $page.'HeaderMenu' -is [Collections.IDictionary]) { $page.'HeaderMenu' } elseif ($Site -and $site.'HeaderMenu' -is [Collections.IDictionary]) { $site.'HeaderMenu' } else { [Ordered]@{} } ), # The footer menu. [Collections.IDictionary] $FooterMenu = $( if ($page -and $page.'FooterMenu' -is [Collections.IDictionary]) { $page.'FooterMenu' } elseif ($Site -and $site.'FooterMenu' -is [Collections.IDictionary]) { $site.'FooterMenu' } else { [Ordered]@{} } ) ) # The literal first thing we do is to capture the arguments and input. # This is important beecause `$input` can only be read once. $allInput = @($input) $allArguments = @($args) $argsAndinput = @($args) + @($allInput) #region Initialize Site and Page if (-not $Site) { $Site = [Ordered]@{} } if (-not $page) { $page = [Ordered]@{} } if (-not $page.MetaData) { $page.MetaData = [Ordered]@{} } #endregion Initialize Site and Page #region Initialize Metadata $page.MetaData['og:title'] = if ($title) { $title } elseif ($Page.title) { $Page.title } elseif ($site.title) { $site.title } $page.MetaData['og:description'] = if ($description) { $description } elseif ($page.description) { $page.description } elseif ($site.description) { $site.description } $page.MetaData['og:image'] = if ($image) { $image } elseif ($page.image) { $page.image } elseif ($site.image) { $site.image } if ($page.Date -is [DateTime]) { $page.MetaData['article:published_time'] = $page.Date.ToString('o') } if ($page.MetaData['og:image']) { $page.MetaData['og:image'] = $page.MetaData['og:image'] -replace '^/', '' -replace '^[^h]', '/' } #endregion Initialize Metadata filter outputHtml { $outputItem = $_ switch ($outputItem) { {$outputItem -is [string]} { return $outputItem } {$outputItem -is [xml]} { return $outputItem.OuterXml } {$outputItem.HTML} { return $outputItem.HTML } {$outputItem.Markdown} { return (ConvertFrom-Markdown -InputObject $outputItem.Markdown).HTML } default { "$outputItem" } } } $outputHtml = @($argsAndinput | outputHtml) -join [Environment]::NewLine #region Declare global styles $style = @" body { max-width: 100vw; height: 100vh; font-family: '$Font', sans-serif; margin: 3em; } header, footer { text-align: center; margin: 2em; } @media (orientation: landscape) { .logo { height: 7em; } } @media (orientation: portrait) { .logo { height: 3em; } .site-title, .page-title { font-size: 0.84em; } } pre, code { font-family: '$CodeFont', monospace; } a, a:visited { text-decoration: none; } a:hover, a:focus { text-decoration: underline; } .main { $(if ($page.FontSize) { "font-size: $($page.FontSize);" } elseif ($site.FontSize) { "font-size: $($site.FontSize);" } else { "font-size: 1.21em;" }) } .taskbar { position: fixed; top: 0; right: 0; z-index: 10; text-align: right; display: flex; flex-direction: row-reverse; align-content: right; align-items: center; margin: 1em; gap: 0.5em; } .taskbar * { vertical-align: middle; } .background { position: fixed; top: 0; left: 0; min-width: 100%; height:100%; } .backdrop-svg { z-index: -100; } .backdrop-canvas { z-index: -99; } "@ # $style = @($StyleTable | outputCss) -join [Environment]::NewLine #endregion Declare global styles #region Page Header # Set up all of the header elements $headerElements = @( # * Google Analytics if ($site.analyticsID) { "<!-- Google tag (gtag.js) --> <script async src='https://www.googletagmanager.com/gtag/js?id=$($site.AnalyticsID)'></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '$($site.AnalyticsID)'); </script>" } # * Viewport metadata "<meta name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1.0' />" # * Open Graph metadata if ($Page.MetaData -is [Collections.IDictionary] -and $Page.MetaData.Count) { foreach ($og in $Page.MetaData.GetEnumerator()) { "<meta name='$([Web.HttpUtility]::HtmlAttributeEncode($og.Key))' content='$([Web.HttpUtility]::HtmlAttributeEncode($og.Value))' />" } } # * RSS autodiscovery if (-not $site.NoRss) { "<link rel='alternate' type='application/rss+xml' title='$($site.Title)' href='/RSS/index.rss' />" } # * Color palette if ($PaletteName) { "<link rel='stylesheet' href='https://cdn.jsdelivr.net/gh/2bitdesigns/4bitcss@latest/css/$PaletteName.css' id='palette' />" } # * Google Font if ($Font) { "<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=$Font' id='font' />" } # * Code font if ($CodeFont) { "<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=$CodeFont' id='codeFont' />" } # * highlightjs css ( if using highlight ) if ($Site.HighlightJS -or $page.HighlightJS) { "<link rel='stylesheet' href='https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/styles/default.min.css' id='highlight' />" '<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/highlight.min.js"></script>' foreach ($language in $Site.HighlightJS.Languages) { "<script src='https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@latest/build/languages/$language.min.js'></script>" } } # * favicons if ($FavIcon) { switch -regex ($FavIcon) { '\.(?>svg|png)$' { $contentType = $matches.0 -replace 'svg', 'svg+xml' -replace '^', 'image' # (try to match the size, if ($_ -match '\d+x\d+') { "<link rel='icon' href='$_' type='$contentType' sizes='$($matches.0)' />" } else { # otherwise, use 'any' size) "<link rel='icon' href='$_' type='$contentType' sizes='any' />" } } } } # * HTMX if (-not $Site.NoHtmx -or $page.NoHtmx) { "<script src='https://unpkg.com/htmx.org@latest'></script>" } $ImportMap # * Our styles "<style>$style</style>" ) # Now we declare the body elements $bodyElements = @( # * The background layers "<svg class='background backdrop-svg' id='background-svg' width='100%' height='100%'>" if ($page.Background -is [xml]) { $page.Background.OuterXml } elseif ($site.Background -is [xml]) { $site.Background.OuterXml } "</svg>" "<canvas id='background backdrop-canvas' width='0' height='0'></canvas>" # * The header "<header>" if ($page.Header) { $page.Header -join [Environment]::NewLine } elseif ($site.Header) { $site.Header -join [Environment]::NewLine } else { "<a href='/'>" @( "<svg xmlns='http://www.w3.org/2000/svg' class='logo'>" + $( if ($site.Logo) { if ($site.Logo -match '<svg') { $site.Logo -replace '<\?.+>' } else { "<image src='$($site.Logo)' class='logoImage' />" } } ) + "</svg>" if ($site.Title) { "<h1 class='site-title'>$([Web.HttpUtility]::HtmlEncode($site.Title))</h1>" } elseif ($site.CNAME) { "<h1 class='site-title'>$([Web.HttpUtility]::HtmlEncode($site.CNAME))</h1>" } ) -join ( [Environment]::NewLine + "<br/>" + [Environment]::NewLine ) "</a>" if ($page.Title -and $page.Title -ne $site.Title) { "<h2 class='page-title'>$([Web.HttpUtility]::HtmlEncode($page.Title))</h2>" } } if ($headerMenu) { "<style>" # If the device is in landscape mode, use larger padding and gaps "@media (orientation: landscape) {" ".header-menu { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 1em }" ".header-menu-item { text-align: center; padding: 1em; }" "}" # If the device is in portrait mode, use smaller padding and gaps "@media (orientation: portrait) {" ".header-menu { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 0.25em }" ".header-menu-item { text-align: center; padding: 0.25em; }" "}" "</style>" "<nav class='header-menu'>" foreach ($menuItem in $headerMenu.GetEnumerator()) { "<a href='$($menuItem.Value)' class='header-menu-item'>$([Web.HttpUtility]::HtmlEncode($menuItem.Key))</a>" } "</nav>" } "</header>" # * The main content "<div class='main'>$outputHtml</div>" if ($taskbar) { # * Our taskbar "<div class='taskbar'>" foreach ($taskbarItem in $taskbar.GetEnumerator()) { "<a href='$($taskbarItem.Value)' class='icon-link' target='_blank'>" if ($page -and $page.Icon."$($taskbarItem.Key)") { $page.Icon[$taskbarItem.Key] if ($site.ShowTaskbarIconText -or $page.ShowTaskbarIconText) { $taskbarItem.Key } } elseif ($site -and $site.Icon."$($taskbarItem.Key)") { $site.Icon[$taskbarItem.Key] if ($site.ShowTaskbarIconText -or $page.ShowTaskbarIconText) { $taskbarItem.Key } } else { $taskbarItem.Key } "</a>" } "</div>" } # * The footer "<footer>" if ($FooterMenu) { "<style>" "@media (orientation: landscape) {" ".footer-menu { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 1em }" ".footer-menu-item { text-align: center; padding: 1em; }" "}" "@media (orientation: portrait) {" ".footer-menu { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 0.5em }" ".footer-menu-item { text-align: center; padding: 0.5em; }" "}" "</style>" "<nav class='footer-menu'>" foreach ($menuItem in $FooterMenu.GetEnumerator()) { "<a href='$($menuItem.Value)' class='footer-menu-item'>$([Web.HttpUtility]::HtmlEncode($menuItem.Key))</a>" } "</nav>" } if ($Page.Footer) { $page.Footer -join [Environment]::NewLine } if ($Site.Footer) { $site.Footer -join [Environment]::NewLine } "</footer>" if ($site.HighlightJS -or $page.HighlightJS) { "<script>hljs.highlightAll();</script>" } ) "<html> <head> <title>$(if ($page['Title']) { $page['Title'] } else { $Title })</title> $($headerElements -join [Environment]::NewLine) </head> <body>$($bodyElements -join [Environment]::NewLine)</body> </html>" |