This commit is contained in:
2026-01-20 20:33:59 +01:00
commit b16a40e431
583 changed files with 87339 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
{{- if site.Params.banner }}
<div class="hextra-banner hx:max-md:sticky hx:top-0 hx:z-20 hx:px-6 hx:text-center hx:text-slate-50 hx:dark:text-white hx:bg-neutral-900 hx:dark:bg-neutral-800 hx:print:[display:none]">
<div class="hx:relative hx:flex hx:items-center hx:justify-center hx:font-medium hx:text-sm hx:py-2.5">
{{- with partial "custom/banner.html" . -}}
{{- . -}}
{{- else -}}
<div style="white-space: pre-wrap" class="hx:px-8">
{{- site.Params.banner.message | default "🎉 Welcome! This is a banner message." | .RenderString -}}
</div>
{{- end -}}
<button
class="hextra-banner-close-button hx:cursor-pointer hx:absolute hx:right-0 hx:text-white hx:font-bold hx:leading-none hx:hover:opacity-75 hx:transition hx:w-10 hx:h-10 hx:-mr-2 hx:md:mr-0 hx:flex hx:items-center hx:justify-center"
>
{{- partial "utils/icon.html" (dict "name" "x" "attributes" "height=16") -}}
</button>
</div>
</div>
{{- end -}}

View File

@@ -0,0 +1,17 @@
{{- $page := .page -}}
{{- $enable := .enable -}}
{{- if (default $enable $page.Params.breadcrumbs) -}}
<div class="hx:mt-1.5 hx:flex hx:items-center hx:gap-1 hx:overflow-hidden hx:text-sm hx:text-gray-500 hx:dark:text-gray-400 hx:contrast-more:text-current">
{{- range $page.Ancestors.Reverse }}
{{- if not .IsHome }}
<div class="hx:whitespace-nowrap hx:transition-colors hx:min-w-[24px] hx:overflow-hidden hx:text-ellipsis hx:hover:text-gray-900 hx:dark:hover:text-gray-100">
<a href="{{ .RelPermalink }}">{{- partial "utils/title" . -}}</a>
</div>
{{- partial "utils/icon.html" (dict "name" "chevron-right" "attributes" "class=\"hx:w-3.5 hx:shrink-0 hx:rtl:-rotate-180\"") -}}
{{ end -}}
{{ end -}}
<div class="hx:whitespace-nowrap hx:transition-colors hx:font-medium hx:text-gray-700 hx:contrast-more:font-bold hx:contrast-more:text-current hx:dark:text-gray-100 hx:contrast-more:dark:text-current">
{{- partial "utils/title" $page -}}
</div>
</div>
{{ end -}}

View File

@@ -0,0 +1,24 @@
{{- if hugo.IsProduction -}}
<!-- Google Analytics -->
{{- if .Site.Config.Services.GoogleAnalytics.ID }}
<link rel="preconnect" href="https://www.googletagmanager.com" crossorigin />
{{ partial "google-analytics.html" . -}}
{{- end }}
<!-- Umami -->
{{- if .Site.Params.analytics.umami -}}
{{ partial "components/analytics/umami.html" . }}
{{- end }}
<!-- Matomo -->
{{- if .Site.Params.analytics.matomo -}}
{{ partial "components/analytics/matomo.html" . }}
{{- end }}
<!-- GoatCounter -->
{{- if .Site.Params.analytics.goatCounter -}}
{{ partial "components/analytics/goat-counter.html" . }}
{{- end -}}
{{- end }}

View File

@@ -0,0 +1,17 @@
{{- with .Site.Params.analytics.goatCounter -}}
{{- if not .code -}}
{{- errorf "Missing GoatCounter 'code' configuration. See https://imfing.github.io/hextra/versions/latest/docs/guide/configuration/#goatcounter-analytics" -}}
{{- end -}}
<script
data-goatcounter="https://{{ .code }}.goatcounter.com/count"
data-goatcounter-settings='
{
"no_onload":{{ .noOnload | default false }},
"no_events":{{ .noEvents | default false }},
"allow_local":{{ .allowLocal | default false }},
"allow_frame":{{ .allowFrame | default false }}
}
'
async src="//gc.zgo.at/count.js"></script>
{{- end -}}

View File

@@ -0,0 +1,13 @@
{{- with site.Config.Services.GoogleAnalytics.ID }}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ . }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "{{ . }}");
</script>
{{ end -}}

View File

@@ -0,0 +1,31 @@
{{- /*
Matomo Analytics.
https://developer.matomo.org/guides/tracking-javascript-guide
*/ -}}
{{- with .Site.Params.analytics.matomo -}}
{{- if not .serverURL }}
{{- errorf "Missing Matomo 'serverURL' configuration. See https://imfing.github.io/hextra/versions/latest/docs/guide/configuration/#matomo-analytics" -}}
{{- end -}}
{{- if not .websiteID }}
{{- errorf "Missing Matomo 'websiteID' configuration. See https://imfing.github.io/hextra/versions/latest/docs/guide/configuration/#matomo-analytics" -}}
{{- end -}}
<!-- Matomo -->
<script type="text/javascript">
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//{{ .serverURL }}/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', {{ .websiteID }}]);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
{{- end -}}

View File

@@ -0,0 +1,57 @@
{{- /*
Umami Analytics
https://umami.is/docs/tracker-configuration
*/ -}}
{{- with .Site.Params.analytics.umami -}}
{{- if not .serverURL }}
{{- errorf "Missing Umami 'serverURL' configuration. See https://imfing.github.io/hextra/versions/latest/docs/guide/configuration/#umami-analytics" -}}
{{- end -}}
{{- if not .websiteID }}
{{- errorf "Missing Umami 'websiteID' configuration. See https://imfing.github.io/hextra/versions/latest/docs/guide/configuration/#umami-analytics" -}}
{{- end -}}
{{- $attributes := newScratch -}}
{{- $attributes.SetInMap "umami" "src" (printf "%s/%s" .serverURL (.scriptName | default "script.js")) -}}
{{- $attributes.SetInMap "umami" "data-website-id" .websiteID -}}
{{- if .hostURL -}}
{{- /* https://umami.is/docs/tracker-configuration#data-host-url */ -}}
{{- $attributes.SetInMap "umami" "data-host-url" .hostURL -}}
{{- end -}}
{{- if .autoTrack -}}
{{- /* https://umami.is/docs/tracker-configuration#data-auto-track */ -}}
{{- $attributes.SetInMap "umami" "data-auto-track" .autoTrack -}}
{{- end -}}
{{- if .tag -}}
{{- /* https://umami.is/docs/tracker-configuration#data-tag */ -}}
{{- $attributes.SetInMap "umami" "data-tag" .tag -}}
{{- end -}}
{{- if .excludeSearch -}}
{{- /* https://umami.is/docs/tracker-configuration#data-exclude-search */ -}}
{{- $attributes.SetInMap "umami" "data-exclude-search" .excludeSearch -}}
{{- end -}}
{{- if .excludeHash -}}
{{- /* https://umami.is/docs/tracker-configuration#data-exclude-hash */ -}}
{{- $attributes.SetInMap "umami" "data-exclude-hash" .excludeHash -}}
{{- end -}}
{{- if .doNotTrack -}}
{{- /* https://umami.is/docs/tracker-configuration#data-do-not-track */ -}}
{{- $attributes.SetInMap "umami" "data-do-not-track" .doNotTrack -}}
{{- end -}}
{{- if .domains -}}
{{- /* https://umami.is/docs/tracker-configuration#data-domains */ -}}
{{- $attributes.SetInMap "umami" "data-domains" .domains -}}
{{- end -}}
<script async defer {{ range $k, $v := ($attributes.Get "umami" ) }} {{ (printf `%s=%q` $k $v) | safeHTMLAttr }}{{- end -}}></script>
{{- end -}}

View File

@@ -0,0 +1,39 @@
{{/*
Blog pagination component for list pages (e.g., blog list, category list)
Usage: {{ partial "components/blog-pager.html" $paginator }}
Parameters:
- . (context): Hugo paginator object
*/}}
{{- $paginator := . -}}
{{- $prevText := (T "previous") | default "Prev" -}}
{{- $nextText := (T "next") | default "Next" -}}
{{- $prevLabel := printf "%s %d/%d" $prevText (sub $paginator.PageNumber 1) $paginator.TotalPages -}}
{{- $nextLabel := printf "%s %d/%d" $nextText (add $paginator.PageNumber 1) $paginator.TotalPages -}}
{{- if or $paginator.HasPrev $paginator.HasNext -}}
<div class="hx:mb-8 hx:flex hx:items-center hx:border-t hx:pt-8 hx:border-gray-200 hx:dark:border-neutral-800 hx:contrast-more:border-neutral-400 hx:dark:contrast-more:border-neutral-400 hx:print:hidden">
{{- if $paginator.HasPrev -}}
<a
href="{{ $paginator.Prev.URL }}"
title="{{ $prevLabel }}"
class="hx:flex hx:max-w-[50%] hx:items-center hx:gap-1 hx:py-4 hx:text-base hx:font-medium hx:text-gray-600 hx:transition-colors [word-break:break-word] hx:hover:text-primary-600 hx:dark:text-gray-300 hx:md:text-lg hx:ltr:pr-4 hx:rtl:pl-4"
>
{{- partial "utils/icon.html" (dict "name" "chevron-right" "attributes" "class=\"hx:inline hx:h-5 hx:shrink-0 hx:ltr:rotate-180\"") -}}
{{ $prevLabel }}
</a>
{{- end -}}
{{- if $paginator.HasNext -}}
<a
href="{{ $paginator.Next.URL }}"
title="{{ $nextLabel }}"
class="hx:flex hx:max-w-[50%] hx:items-center hx:gap-1 hx:py-4 hx:text-base hx:font-medium hx:text-gray-600 hx:transition-colors [word-break:break-word] hx:hover:text-primary-600 hx:dark:text-gray-300 hx:md:text-lg hx:ltr:ml-auto hx:ltr:pl-4 hx:ltr:text-right hx:rtl:mr-auto hx:rtl:pr-4 hx:rtl:text-left"
>
{{ $nextLabel }}
{{- partial "utils/icon.html" (dict "name" "chevron-right" "attributes" "class=\"hx:inline hx:h-5 hx:shrink-0 hx:rtl:-rotate-180\"") -}}
</a>
{{- end -}}
</div>
{{- end -}}

View File

@@ -0,0 +1,15 @@
{{/* TODO: remove filename variable */}}
{{- $filename := .filename | default "" -}}
{{- $display := site.Params.highlight.copy.display | default "hover" -}}
{{- $copyCode := (T "copyCode") | default "Copy code" -}}
<div class="hextra-code-copy-btn-container {{ if eq $display `hover` }}hx:opacity-0{{ end }} hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 {{ if $filename }}hx:top-8{{ else }}hx:top-0{{ end }}">
<button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="{{ $copyCode }}"
>
<div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
</button>
</div>

View File

@@ -0,0 +1,29 @@
{{ $filename := .filename | default "" -}}
{{ $base_url := .base_url | default "" -}}
{{ $lang := .lang | default "" }}
{{ $content := .content }}
{{ $options := .options | default (dict) }}
{{- if $filename -}}
<div class="hextra-code-filename not-prose" dir="auto">
{{- if $base_url -}}
{{- $base_url = strings.TrimSuffix "/" $base_url -}}
{{- $filename = strings.TrimPrefix "/" $filename -}}
{{- $file_url := urls.JoinPath $base_url $filename -}}
<a class="hx:no-underline hx:inline-flex hx:items-center hx:gap-1" href="{{ $file_url }}" target="_blank" rel="noopener noreferrer">
<span>{{- $filename -}}</span>
{{- partial "utils/icon" (dict "name" "external-link" "attributes" "height=1em") -}}
</a>
{{- else -}}
{{- $filename -}}
{{- end -}}
</div>
{{- end -}}
{{- if transform.CanHighlight $lang -}}
<div>{{- highlight $content $lang $options -}}</div>
{{- else -}}
<div><pre><code>{{ $content }}</code></pre></div>
{{- end -}}

View File

@@ -0,0 +1,11 @@
{{- $enableComments := site.Params.comments.enable | default false -}}
{{ if not (eq .Params.comments nil) }}
{{ $enableComments = .Params.comments }}
{{ end }}
{{- if $enableComments -}}
{{- if eq site.Params.comments.type "giscus" -}}
{{ partial "components/giscus.html" . }}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,85 @@
{{- $lang := site.Language.Lang | default `en` -}}
{{- if hasPrefix $lang "zh" -}}
{{- /* See: https://github.com/giscus/giscus/tree/main/locales */}}
{{- $lang = site.Language.LanguageCode | default `zh-CN` -}}
{{- end -}}
{{- with site.Params.comments.giscus -}}
<script>
function getGiscusTheme() {
const giscusTheme = '{{ .theme }}';
if (giscusTheme === 'light' || giscusTheme === 'dark') {
return giscusTheme;
}
const hugoTheme = localStorage.getItem("color-theme");
if (hugoTheme === 'light' || hugoTheme === 'dark') {
return hugoTheme;
}
if (hugoTheme === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const defaultTheme = '{{ site.Params.theme.default }}';
if (defaultTheme === 'light' || defaultTheme === 'dark') {
return defaultTheme;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function setGiscusTheme() {
const iframe = document.querySelector('iframe.giscus-frame');
if (!iframe) return;
const msg = {
giscus: {
setConfig: {
theme: getGiscusTheme(),
},
},
}
iframe.contentWindow.postMessage(msg, 'https://giscus.app');
}
document.addEventListener('DOMContentLoaded', function () {
const giscusAttributes = {
"src": "https://giscus.app/client.js",
"data-repo": "{{ .repo }}",
"data-repo-id": "{{ .repoId }}",
"data-category": "{{ .category }}",
"data-category-id": "{{ .categoryId }}",
"data-mapping": "{{ .mapping | default `pathname` }}",
"data-strict": "{{ (string .strict) | default 0 }}",
"data-reactions-enabled": "{{ (string .reactionsEnabled) | default 1 }}",
"data-emit-metadata": "{{ (string .emitMetadata) | default 0 }}",
"data-input-position": "{{ .inputPosition | default `top` }}",
"data-theme": getGiscusTheme(),
"data-lang": "{{ .lang | default $lang }}",
"crossorigin": "anonymous",
"async": "",
};
// Dynamically create script tag
const giscusScript = document.createElement("script");
Object.entries(giscusAttributes).forEach(([key, value]) => giscusScript.setAttribute(key, value));
// Random hash id to avoid conflicts with titles inside pages.
document.getElementById('giscus-hextra-bb112b9f807c37c1752e5da6a1652a29').appendChild(giscusScript);
// Listen for system theme changes
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", setGiscusTheme);
// Update giscus theme when theme switcher is clicked
const themeToggleOptions = document.querySelectorAll(".hextra-theme-toggle-options p");
if (themeToggleOptions) {
themeToggleOptions.forEach(toggle => toggle.addEventListener('click', setGiscusTheme));
}
});
</script>
<div id="giscus-hextra-bb112b9f807c37c1752e5da6a1652a29"></div>
{{- else -}}
{{ warnf "giscus is not configured" }}
{{- end -}}

View File

@@ -0,0 +1,53 @@
{{- $content := .content -}}
{{- $alertType := .alertType -}}
{{- $alertTitle := .alertTitle -}}
{{- $styles := newScratch -}}
{{- $styles.Set "default" (dict
"icon" "light-bulb"
"style" "hx:border-green-200 hx:bg-green-100 hx:text-green-900 hx:dark:border-green-200/30 hx:dark:bg-green-900/30 hx:dark:text-green-200"
)
-}}
{{- $styles.Set "note" (dict
"icon" "information-circle"
"style" "hx:border-blue-200 hx:bg-blue-100 hx:text-blue-900 hx:dark:border-blue-200/30 hx:dark:bg-blue-900/30 hx:dark:text-blue-200"
)
-}}
{{- $styles.Set "tip" (dict
"icon" "light-bulb"
"style" "hx:border-green-200 hx:bg-green-100 hx:text-green-900 hx:dark:border-green-200/30 hx:dark:bg-green-900/30 hx:dark:text-green-200"
)
-}}
{{- $styles.Set "important" (dict
"icon" "information-circle"
"style" "hx:border-purple-200 hx:bg-purple-100 hx:text-purple-900 hx:dark:border-purple-200/30 hx:dark:bg-purple-900/30 hx:dark:text-purple-200"
)
-}}
{{- $styles.Set "warning" (dict
"icon" "exclamation"
"style" "hx:border-amber-200 hx:bg-amber-100 hx:text-amber-900 hx:dark:border-amber-200/30 hx:dark:bg-amber-900/30 hx:dark:text-amber-200"
)
-}}
{{- $styles.Set "caution" (dict
"icon" "exclamation-circle"
"style" "hx:border-red-200 hx:bg-red-100 hx:text-red-900 hx:dark:border-red-200/30 hx:dark:bg-red-900/30 hx:dark:text-red-200"
)
-}}
{{- $style := or ($styles.Get $alertType) ($styles.Get "default") -}}
{{- $title := or $alertTitle (or (i18n $alertType) (title $alertType)) -}}
<div class="hx:overflow-x-auto hx:mt-6 hx:flex hx:flex-col hx:rounded-lg hx:border hx:py-4 hx:px-4 hx:border-gray-200 hx:contrast-more:border-current hx:contrast-more:dark:border-current {{ $style.style }}">
<p class="hx:flex hx:items-center hx:font-medium">
{{- with $style.icon -}}
{{- partial "utils/icon.html" (dict "name" . "attributes" `height=16px class="hx:inline-block hx:align-middle hx:mr-2"`) -}}
{{- end -}}
{{- $title -}}
</p>
<div class="hx:w-full hx:min-w-0 hx:leading-7">
<div class="hx:mt-6 hx:leading-7 hx:first:mt-0">
{{- $content -}}
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
{{- $lastUpdated := (T "lastUpdated") | default "Last updated on" -}}
{{- if site.Params.displayUpdatedDate -}}
{{- with .Lastmod -}}
{{ $datetime := (time.Format "2006-01-02T15:04:05.000Z" .) }}
<div class="hx:mt-12 hx:mb-8 hx:block hx:text-xs hx:text-gray-500 hx:ltr:text-right hx:rtl:text-left hx:dark:text-gray-400">{{ $lastUpdated }} <time datetime="{{ $datetime }}">{{ partial "utils/format-date" . }}</time></div>
{{- else -}}
<div class="hx:mt-16"></div>
{{- end -}}
{{- else -}}
<div class="hx:mt-16"></div>
{{- end -}}

View File

@@ -0,0 +1,53 @@
{{/* Article navigation on the footer of the article */}}
{{- $reversePagination := .Store.Get "reversePagination" | default false -}}
{{- $prev := cond $reversePagination .PrevInSection .NextInSection -}}
{{- $next := cond $reversePagination .NextInSection .PrevInSection -}}
{{- if eq .Params.prev false }}
{{- if $reversePagination }}{{ $next = false }}{{ else }}{{ $prev = false }}{{ end -}}
{{ else }}
{{- with .Params.prev -}}
{{- with $.Site.GetPage . -}}
{{- if $reversePagination }}{{ $next = . }}{{ else }}{{ $prev = . }}{{ end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if eq .Params.next false }}
{{- if $reversePagination }}{{ $prev = false }}{{ else }}{{ $next = false }}{{ end -}}
{{ else }}
{{- with .Params.next -}}
{{- with $.Site.GetPage . -}}
{{- if $reversePagination }}{{ $prev = . }}{{ else }}{{ $next = . }}{{ end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if or $prev $next -}}
<div class="hx:mb-8 hx:flex hx:items-center hx:border-t hx:pt-8 hx:border-gray-200 hx:dark:border-neutral-800 hx:contrast-more:border-neutral-400 hx:dark:contrast-more:border-neutral-400 hx:print:hidden">
{{- if $prev -}}
{{- $linkTitle := partial "utils/title" $prev -}}
<a
href="{{ $prev.RelPermalink }}"
title="{{ $linkTitle }}"
class="hx:flex hx:max-w-[50%] hx:items-center hx:gap-1 hx:py-4 hx:text-base hx:font-medium hx:text-gray-600 hx:transition-colors [word-break:break-word] hx:hover:text-primary-600 hx:dark:text-gray-300 hx:md:text-lg hx:ltr:pr-4 hx:rtl:pl-4"
>
{{- partial "utils/icon.html" (dict "name" "chevron-right" "attributes" "class=\"hx:inline hx:h-5 hx:shrink-0 hx:ltr:rotate-180\"") -}}
{{- $linkTitle -}}
</a>
{{- end -}}
{{- if $next -}}
{{- $linkTitle := partial "utils/title" $next -}}
<a
href="{{ $next.RelPermalink }}"
title="{{ $linkTitle }}"
class="hx:flex hx:max-w-[50%] hx:items-center hx:gap-1 hx:py-4 hx:text-base hx:font-medium hx:text-gray-600 hx:transition-colors [word-break:break-word] hx:hover:text-primary-600 hx:dark:text-gray-300 hx:md:text-lg hx:ltr:ml-auto hx:ltr:pl-4 hx:ltr:text-right hx:rtl:mr-auto hx:rtl:pr-4 hx:rtl:text-left"
>
{{- $linkTitle -}}
{{- partial "utils/icon.html" (dict "name" "chevron-right" "attributes" "class=\"hx:inline hx:h-5 hx:shrink-0 hx:rtl:-rotate-180\"") -}}
</a>
{{- end -}}
</div>
{{- end -}}

View File

@@ -0,0 +1,6 @@
<link rel="icon shortcut" href="{{ "favicon.ico" | relURL }}" sizes="32x32" />
<link rel="icon" href="{{ "favicon.svg" | relURL }}" type="image/svg+xml" id="favicon-svg" />
<link rel="icon" href="{{ "favicon-16x16.png" | relURL }}" type="image/png" sizes="16x16" />
<link rel="icon" href="{{ "favicon-32x32.png" | relURL }}" type="image/png" sizes="32x32" />
<link rel="apple-touch-icon" href="{{ "apple-touch-icon.png" | relURL }}" sizes="180x180" />
<link fetchpriority="low" href="{{ "site.webmanifest" | relURL }}" rel="manifest" />

View File

@@ -0,0 +1,44 @@
{{- $enableFooterSwitches := .Store.Get "enableFooterSwitches" | default false -}}
{{- $displayThemeToggle := site.Params.theme.displayToggle | default true -}}
{{- $footerSwitchesVisible := and $enableFooterSwitches (or hugo.IsMultilingual $displayThemeToggle) -}}
{{- $copyrightSectionVisible := or (.Site.Params.footer.displayPoweredBy | default true) .Site.Params.footer.displayCopyright -}}
{{- $copyright := (T "copyright") | default "© 2024 Hextra." -}}
{{- $poweredBy := (T "poweredBy") | default "Powered by Hextra" -}}
<footer class="hextra-footer hx:bg-gray-100 hx:pb-[env(safe-area-inset-bottom)] hx:dark:bg-neutral-900 hx:print:bg-transparent">
{{- if $footerSwitchesVisible -}}
<div class="hx:mx-auto hx:flex hx:gap-2 hx:py-2 hx:px-4 hextra-max-footer-width">
{{- partial "language-switch.html" (dict "context" .) -}}
{{- with $displayThemeToggle }}{{ partial "theme-toggle.html" }}{{ end -}}
</div>
{{- if or hugo.IsMultilingual $displayThemeToggle -}}
<hr class="hx:border-gray-200 hx:dark:border-neutral-800" />
{{- end -}}
{{- end -}}
<div class="hextra-custom-footer hextra-max-footer-width hx:pl-[max(env(safe-area-inset-left),1.5rem)] hx:pr-[max(env(safe-area-inset-right),1.5rem)] hx:text-gray-600 hx:dark:text-gray-400">
{{- partial "custom/footer.html" (dict "context" . "switchesVisible" $footerSwitchesVisible "copyrightVisible" $copyrightSectionVisible) -}}
</div>
{{- if $copyrightSectionVisible -}}
<div
class="hextra-max-footer-width hx:mx-auto hx:flex hx:justify-center hx:py-12 hx:pl-[max(env(safe-area-inset-left),1.5rem)] hx:pr-[max(env(safe-area-inset-right),1.5rem)] hx:text-gray-600 hx:dark:text-gray-400 hx:md:justify-start"
>
<div class="hx:flex hx:w-full hx:flex-col hx:items-center hx:sm:items-start">
{{- if (.Site.Params.footer.displayPoweredBy | default true) }}<div class="hx:font-semibold">{{ template "theme-credit" $poweredBy }}</div>{{- end -}}
{{- if .Site.Params.footer.displayCopyright }}<div class="hx:mt-6 hx:text-xs">{{ $copyright | markdownify }}</div>{{- end -}}
</div>
</div>
{{- end -}}
</footer>
{{- define "theme-credit" -}}
<a class="hx:flex hx:text-sm hx:items-center hx:gap-1 hx:text-current" target="_blank" rel="noopener noreferrer" title="Hextra GitHub Homepage" href="https://github.com/imfing/hextra">
<span>
{{- . | markdownify -}}
{{- if strings.Contains . "Hextra" -}}
{{- partial "utils/icon.html" (dict "name" "hextra" "attributes" `height=1em class="hx:inline-block hx:ltr:ml-1 hx:rtl:mr-1 hx:align-[-2.5px]"`) -}}
{{- end -}}
</span>
</a>
{{- end -}}

View File

@@ -0,0 +1,2 @@
{{- /* Only for compatibility. */ -}}
{{- partial "components/analytics/google-analytics.html" . -}}

View File

@@ -0,0 +1,81 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{- $noindex := .Params.noindex | default false -}}
{{ if and (hugo.IsProduction) (not $noindex) -}}
<meta name="robots" content="index, follow" />
{{ else -}}
<meta name="robots" content="noindex, nofollow" />
{{ end -}}
{{ partialCached "favicons.html" . -}}
<title>
{{- if .IsHome -}}
{{ .Site.Title -}}
{{ else -}}
{{ with .Title }}{{ . }} {{ end -}}
{{ .Site.Title -}}
{{ end -}}
</title>
<meta name="description" content="{{ partial "utils/page-description.html" . }}" />
{{- with .Params.canonical -}}
<link rel="canonical" href="{{ . }}" itemprop="url" />
{{- else -}}
<link rel="canonical" href="{{ .Permalink }}" itemprop="url" />
{{- end -}}
{{- partial "opengraph.html" . -}}
{{- partial "schema.html" . -}}
{{- partial "twitter_cards.html" . -}}
{{- $mainCss := resources.Get "css/compiled/main.css" -}}
{{- $customCss := resources.Get "css/custom.css" -}}
{{- $variablesCss := resources.Get "css/variables.css" | resources.ExecuteAsTemplate "css/variables.css" . -}}
{{- /* Production build */ -}}
{{- if hugo.IsProduction }}
{{- $styles := slice $variablesCss $mainCss $customCss | resources.Concat "css/compiled/main.css" | minify | fingerprint }}
<link rel="preload" href="{{ $styles.RelPermalink }}" as="style" integrity="{{ $styles.Data.Integrity }}" />
<link href="{{ $styles.RelPermalink }}" rel="stylesheet" integrity="{{ $styles.Data.Integrity }}" />
{{- /* Theme development mode (non-production + theme environment) */ -}}
{{- else if eq hugo.Environment "theme" }}
{{- $devStyles := resources.Get "css/styles.css" | postCSS (dict "inlineImports" true) }}
<link href="{{ $devStyles.RelPermalink }}" rel="stylesheet" />
<link href="{{ $variablesCss.RelPermalink }}" rel="stylesheet" />
<link href="{{ $customCss.RelPermalink }}" rel="stylesheet" />
{{- /* User local development */ -}}
{{- else }}
{{- $styles := resources.Get "css/compiled/main.css" -}}
<link href="{{ $styles.RelPermalink }}" rel="stylesheet" />
<link href="{{ $variablesCss.RelPermalink }}" rel="stylesheet" />
<link href="{{ $customCss.RelPermalink }}" rel="stylesheet" />
{{- end }}
{{ partial "components/analytics/analytics.html" . }}
{{- $scriptsHead := slice -}}
{{- range resources.Match "js/head/*.js" -}}
{{ $scriptsHead = $scriptsHead | append (resources.ExecuteAsTemplate .Name $ .) }}
{{- end -}}
{{- $scripts := $scriptsHead | resources.Concat "js/main-head.js" -}}
{{- if hugo.IsProduction -}}
{{- $scripts = $scripts | minify | fingerprint -}}
{{- end -}}
<script src="{{ $scripts.RelPermalink }}" integrity="{{ $scripts.Data.Integrity }}"></script>
<!-- Math engine -->
{{ $noop := .WordCount -}}
{{- $engine := site.Params.math.engine | default "katex" -}}
{{ if and (.Page.Store.Get "hasMath") (eq $engine "katex") -}}
{{ partialCached "scripts/katex.html" . -}}
{{ else if and (.Page.Store.Get "hasMath") (eq $engine "mathjax") -}}
{{ partialCached "scripts/mathjax.html" . -}}
{{ end -}}
{{ partial "utils/page-width-override.html" . }}
{{ partial "custom/head-end.html" . -}}
</head>

View File

@@ -0,0 +1,50 @@
{{- $page := .context -}}
{{- $iconName := .iconName | default "globe-alt" -}}
{{- $iconHeight := .iconHeight | default 12 -}}
{{- $location := .location -}}
{{- $class := .class | default "hx:h-7 hx:px-2 hx:text-xs hx:text-gray-600 hx:transition-colors hx:dark:text-gray-400 hx:hover:bg-gray-100 hx:hover:text-gray-900 hx:dark:hover:bg-primary-100/5 hx:dark:hover:text-gray-50" -}}
{{- $grow := .grow -}}
{{- $hideLabel := .hideLabel | default false -}}
{{- $changeLanguage := (T "changeLanguage") | default "Change language" -}}
{{- if hugo.IsMultilingual -}}
<div class="hx:flex hx:justify-items-start {{ if $grow }}hx:grow{{ end }}">
<button
title="{{ $changeLanguage }}"
data-state="closed"
data-location="{{ $location }}"
class="hextra-language-switcher hx:cursor-pointer hx:rounded-md hx:text-left hx:font-medium {{ $class }} hx:grow"
type="button"
aria-label="{{ $changeLanguage }}"
>
<div class="hx:flex hx:items-center hx:gap-2 hx:capitalize">
{{- partial "utils/icon" (dict "name" $iconName "attributes" (printf "height=%d" $iconHeight)) -}}
{{- if not $hideLabel }}<span>{{ site.Language.LanguageName }}</span>{{ end -}}
</div>
</button>
<ul
class="hextra-language-options hx:hidden hx:z-20 hx:max-h-64 hx:overflow-auto hx:rounded-md hx:ring-1 hx:ring-black/5 hx:bg-white hx:py-1 hx:text-sm hx:shadow-lg hx:dark:ring-white/20 hx:dark:bg-neutral-800"
style="position: fixed; inset: auto auto 0px 0px; margin: 0px; min-width: 100px;"
>
{{ range site.Languages }}
{{ $link := partial "utils/lang-link" (dict "lang" .Lang "context" $page) }}
<li class="hx:flex hx:flex-col">
<a
href="{{ $link }}"
class="hx:text-gray-800 hx:dark:text-gray-100 hx:hover:bg-primary-50 hx:hover:text-primary-600 hx:hover:dark:bg-primary-500/10 hx:hover:dark:text-primary-600 hx:relative hx:cursor-pointer hx:whitespace-nowrap hx:py-1.5 hx:transition-colors hx:ltr:pl-3 hx:ltr:pr-9 hx:rtl:pr-3 hx:rtl:pl-9"
>
{{- .LanguageName -}}
{{- if eq .LanguageName site.Language.LanguageName -}}
<span class="hx:absolute hx:inset-y-0 hx:flex hx:items-center hx:ltr:right-3 hx:rtl:left-3">
{{- partial "utils/icon" (dict "name" "check" "attributes" "height=1em width=1em") -}}
</span>
{{- end -}}
</a>
</li>
{{ end -}}
</ul>
</div>
{{- end -}}

View File

@@ -0,0 +1,76 @@
{{- $currentPage := .currentPage -}}
{{- $link := .link -}}
{{- $item := .item -}}
{{- $icon := .icon -}}
{{- $external := .external -}}
{{- $active := or ($currentPage.HasMenuCurrent "main" $item) ($currentPage.IsMenuCurrent "main" $item) -}}
{{- $activeClass := cond $active "hx:font-medium" "hx:text-gray-600 hx:hover:text-gray-800 hx:dark:text-gray-400 hx:dark:hover:text-gray-200" -}}
{{- if $item.HasChildren -}}
{{- /* Dropdown menu for items with children */ -}}
<div class="hx:relative hx:hidden hx:md:inline-block">
<button
title="{{ or (T $item.Identifier) $item.Name | safeHTML }}"
data-state="closed"
class="hextra-nav-menu-toggle hx:cursor-pointer hx:text-sm hx:contrast-more:text-gray-700 hx:contrast-more:dark:text-gray-100 hx:relative hx:-ml-2 hx:whitespace-nowrap hx:p-2 hx:flex hx:items-center hx:gap-1 {{ $activeClass }}"
type="button"
aria-label="{{ or (T $item.Identifier) $item.Name | safeHTML }}"
>
{{- if $icon -}}
<span class="hx:inline-flex hx:items-center">
{{- partial "utils/icon" (dict "name" $icon "attributes" `height="1em" class="hx:inline-block"`) -}}
</span>
{{- end -}}
<span class="hx:text-center">
{{- or (T $item.Identifier) $item.Name | safeHTML -}}
</span>
{{- partial "utils/icon.html" (dict "name" "chevron-down" "attributes" "height=12 class=\"hx:transition-transform hx:duration-200 hx:ease-in-out\"") -}}
</button>
<ul
class="hextra-nav-menu-items hx:hidden hx:z-20 hx:max-h-64 hx:overflow-auto hx:rounded-md hx:ring-1 hx:ring-black/5 hx:bg-white hx:py-1 hx:text-sm hx:shadow-lg hx:dark:ring-white/20 hx:dark:bg-neutral-800"
style="min-width: 100px;"
>
{{ range $item.Children }}
{{- $link := .URL -}}
{{- $external := strings.HasPrefix $link "http" -}}
{{- with .PageRef -}}
{{- if hasPrefix . "/" -}}
{{- $link = relLangURL (strings.TrimPrefix "/" .) -}}
{{- end -}}
{{- end -}}
<li class="hextra-nav-menu-item hx:flex hx:flex-col">
<a
href="{{ $link }}"
{{ if $external }}target="_blank" rel="noreferrer"{{ end }}
class="hx:text-gray-600 hx:hover:text-gray-800 hx:dark:text-gray-400 hx:dark:hover:text-gray-200 hx:relative hx:cursor-pointer hx:whitespace-nowrap hx:py-1.5 hx:transition-colors hx:ltr:pl-3 hx:ltr:pr-9 hx:rtl:pr-3 hx:rtl:pl-9 hx:flex hx:items-center hx:gap-1"
>
{{- if and (eq .Params.type "link") .Params.icon -}}
<span class="hx:inline-flex hx:items-center">
{{- partial "utils/icon" (dict "name" .Params.icon "attributes" `height="1em" class="hx:inline-block"`) -}}
</span>
{{- end -}}
{{- or (T .Identifier) .Name | safeHTML -}}
</a>
</li>
{{- end -}}
</ul>
</div>
{{- else -}}
{{- /* Regular menu item without children */ -}}
<a
title="{{ or (T .Identifier) .Name | safeHTML }}"
href="{{ $link }}"
{{ if $external }}target="_blank" rel="noreferrer"{{ end }}
class="hx:text-sm hx:contrast-more:text-gray-700 hx:contrast-more:dark:text-gray-100 hx:relative hx:-ml-2 hx:hidden hx:whitespace-nowrap hx:p-2 hx:md:inline-flex hx:items-center hx:gap-1 {{ $activeClass }}"
>
{{- if $icon -}}
<span class="hx:inline-flex hx:items-center">
{{- partial "utils/icon" (dict "name" $icon "attributes" `height="1em" class="hx:inline-block"`) -}}
</span>
{{- end -}}
<span class="hx:text-center">
{{- or (T $item.Identifier) $item.Name | safeHTML -}}
</span>
</a>
{{- end -}}

View File

@@ -0,0 +1,16 @@
{{- $logoPath := .Site.Params.navbar.logo.path | default "images/logo.svg" -}}
{{- $logoLink := .Site.Params.navbar.logo.link | default .Site.Home.RelPermalink -}}
{{- $logoWidth := .Site.Params.navbar.logo.width | default "20" -}}
{{- $logoHeight := .Site.Params.navbar.logo.height | default "20" -}}
{{- $logoDarkPath := .Site.Params.navbar.logo.dark | default $logoPath -}}
<a class="hx:flex hx:items-center hx:hover:opacity-75 hx:ltr:mr-auto hx:rtl:ml-auto" href="{{ $logoLink }}">
{{- $displayTitle := (.Site.Params.navbar.displayTitle | default true) }}
{{- if (.Site.Params.navbar.displayLogo | default true) }}
<img class="hx:mr-2 hx:block hx:dark:hidden" src="{{ $logoPath | relURL }}" alt="{{ cond $displayTitle `Logo` .Site.Title }}" height="{{ $logoHeight }}" width="{{ $logoWidth }}" />
<img class="hx:mr-2 hx:hidden hx:dark:block" src="{{ $logoDarkPath | relURL }}" alt="{{ cond $displayTitle `Dark Logo` .Site.Title }}" height="{{ $logoHeight }}" width="{{ $logoWidth }}" />
{{- end }}
{{- if $displayTitle }}
<span class="hx:mr-2 hx:font-extrabold hx:inline hx:select-none" title="{{ .Site.Title }}">{{- .Site.Title -}}</span>
{{- end }}
</a>

View File

@@ -0,0 +1,59 @@
{{- $navWidth := "hx:max-w-[90rem]" -}}
{{- with .Site.Params.navbar.width -}}
{{ if eq . "normal" -}}
{{ $navWidth = "hx:max-w-screen-xl" -}}
{{ else if eq . "full" -}}
{{ $navWidth = "max-w-full" -}}
{{ end -}}
{{- end -}}
{{- $page := . -}}
{{- $iconHeight := 24 -}}
<div class="hextra-nav-container hx:sticky hx:top-0 hx:z-20 hx:w-full hx:bg-transparent hx:print:hidden">
<div
class="hextra-nav-container-blur hx:pointer-events-none hx:absolute hx:z-[-1] hx:h-full hx:w-full hx:bg-white hx:dark:bg-dark hx:shadow-[0_2px_4px_rgba(0,0,0,.02),0_1px_0_rgba(0,0,0,.06)] hx:contrast-more:shadow-[0_0_0_1px_#000] hx:dark:shadow-[0_-1px_0_rgba(255,255,255,.1)_inset] hx:contrast-more:dark:shadow-[0_0_0_1px_#fff]"
></div>
<nav class="hextra-max-navbar-width hx:mx-auto hx:flex hx:items-center hx:justify-end hx:gap-2 hx:h-16 hx:px-6">
{{ partial "navbar-title.html" . }}
{{- $currentPage := . -}}
{{- range .Site.Menus.main -}}
{{- if eq .Params.type "search" -}}
{{- partial "search.html" (dict "params" .Params) -}}
{{- else -}}
{{- $link := .URL -}}
{{- $external := strings.HasPrefix $link "http" -}}
{{- with .PageRef -}}
{{- if hasPrefix . "/" -}}
{{- $link = relLangURL (strings.TrimPrefix "/" .) -}}
{{- end -}}
{{- end -}}
{{- if eq .Params.type "link" -}}
{{- partial "navbar-link.html" (dict "currentPage" $currentPage "link" $link "external" $external "item" . "icon" .Params.icon) -}}
{{- else if eq .Params.type "theme-toggle" -}}
{{- partial "theme-toggle.html" (dict "iconHeight" $iconHeight "hideLabel" (not .Params.label) "iconHeight" $iconHeight "location" "top" "class" "hx:p-2") -}}
{{- else if eq .Params.type "language-switch" -}}
{{- partial "language-switch" (dict "context" $page "grow" false "hideLabel" (not .Params.label) "iconName" (.Params.icon | default "translate") "iconHeight" $iconHeight "location" "top" "class" "hx:p-2") -}}
{{- else if .Params.icon -}}
{{- /* Display icon menu item */ -}}
{{- if not $link -}}{{ warnf "Icon menu item '%s' has no URL" .Name }}{{- end -}}
{{- $rel := cond (eq .Params.icon "mastodon") "noreferrer me" "noreferrer" }}
<a class="hx:p-2 hx:text-current" {{ if $external }}target="_blank" rel="{{ $rel }}"{{ end }} href="{{ $link }}" title="{{ or (T .Identifier) .Name | safeHTML }}">
{{- partial "utils/icon.html" (dict "name" .Params.icon "attributes" (printf "height=%d" $iconHeight)) -}}
<span class="hx:sr-only">{{ or (T .Identifier) .Name | safeHTML }}</span>
</a>
{{- else -}}
{{- partial "navbar-link.html" (dict "currentPage" $currentPage "link" $link "external" $external "item" .) -}}
{{- end -}}
{{- end -}}
{{- end -}}
<button type="button" aria-label="Menu" class="hextra-hamburger-menu hx:cursor-pointer hx:-mr-2 hx:rounded-sm hx:p-2 hx:active:bg-gray-400/20 hx:md:hidden">
{{- partial "utils/icon.html" (dict "name" "hamburger-menu" "attributes" (printf "height=%d" $iconHeight)) -}}
</button>
</nav>
</div>

View File

@@ -0,0 +1,88 @@
{{/* Adapted from https://github.com/gohugoio/hugo/blob/v0.149.0/docs/layouts/_partials/opengraph/opengraph.html */}}
<meta property="og:title" content="{{ .Title }}">
<meta
property="og:description"
content="{{ with .Description }}
{{ . }}
{{ else }}
{{ if .IsPage }}
{{ .Summary }}
{{ else }}
{{ with .Site.Params.description }}{{ . }}{{ end }}
{{ end }}
{{ end }}">
<meta
property="og:type"
content="{{ if .IsPage }}
article
{{ else }}
website
{{ end }}">
<meta property="og:url" content="{{ .Permalink }}">
{{- with $.Params.images -}}
{{- range first 6 . }}
{{- with $.Resources.GetMatch . }}
<!-- If the string matches a page resource, use that -->
<meta property="og:image" content="{{ .Permalink }}">
{{- else }}
<!-- Otherwise treat it as a site/global path -->
<meta property="og:image" content="{{ . | absURL }}">
{{- end }}
{{- end }}
{{- else -}}
{{- with $.Site.Params.images }}
<meta property="og:image" content="{{ index . 0 | absURL }}">
{{- end }}
{{- end -}}
{{- if .IsPage }}
{{- $iso8601 := "2006-01-02T15:04:05-07:00" -}}
<meta property="article:section" content="{{ .Section }}">
{{ with .PublishDate }}
<meta
property="article:published_time"
{{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }}>
{{ end }}
{{ with .Lastmod }}
<meta
property="article:modified_time"
{{ .Format $iso8601 | printf "content=%q" | safeHTMLAttr }}>
{{ end }}
{{- end -}}
{{- with .Params.audio }}<meta property="og:audio" content="{{ . }}">{{ end }}
{{- with .Params.locale }}
<meta property="og:locale" content="{{ . }}">
{{ end }}
{{- with .Site.Params.title }}
<meta property="og:site_name" content="{{ . }}">
{{ end }}
{{- with .Params.videos }}
{{- range . }}
<meta property="og:video" content="{{ . | absURL }}">
{{ end }}
{{ end }}
{{- /* If it is part of a series, link to related articles */}}
{{- $permalink := .Permalink }}
{{- $siteSeries := .Site.Taxonomies.series }}
{{ with .Params.series }}
{{- range $name := . }}
{{- $series := index $siteSeries ($name | urlize) }}
{{- range $page := first 6 $series.Pages }}
{{- if ne $page.Permalink $permalink }}
<meta property="og:see_also" content="{{ $page.Permalink }}">
{{ end }}
{{- end }}
{{ end }}
{{ end }}
{{- /* Facebook Page Admin ID for Domain Insights */}}
{{- with site.Params.social.facebook_admin }}
<meta property="fb:admins" content="{{ . }}">
{{ end }}

View File

@@ -0,0 +1,15 @@
{{/* Core scripts (theme, menu, tabs, etc.) */}}
{{- partial "scripts/core.html" . -}}
{{/* Search */}}
{{- partial "scripts/search.html" . -}}
{{/* Mermaid */}}
{{- if (.Store.Get "hasMermaid") -}}
{{- partial "scripts/mermaid.html" . -}}
{{- end -}}
{{/* Asciinema */}}
{{- if (.Store.Get "hasAsciinema") -}}
{{- partial "scripts/asciinema.html" . -}}
{{- end -}}

View File

@@ -0,0 +1,114 @@
{{- /* Asciinema */ -}}
{{- $asciinemaBase := "" -}}
{{- $useDefaultCdn := true -}}
{{- with site.Params.asciinema.base -}}
{{- $asciinemaBase = . -}}
{{- $useDefaultCdn = false -}}
{{- end -}}
{{- $asciinemaJsAsset := "" -}}
{{- with site.Params.asciinema.js -}}
{{- $asciinemaJsAsset = . -}}
{{- end -}}
{{- $asciinemaCssAsset := "" -}}
{{- with site.Params.asciinema.css -}}
{{- $asciinemaCssAsset = . -}}
{{- end -}}
{{- /* If only js/css is set without base, use local asset loading */ -}}
{{- if and $useDefaultCdn (or (ne $asciinemaJsAsset "") (ne $asciinemaCssAsset "")) -}}
{{- $useDefaultCdn = false -}}
{{- end -}}
{{- /* Set default CDN base if needed */ -}}
{{- if $useDefaultCdn -}}
{{- $asciinemaBase = "https://cdn.jsdelivr.net/npm/asciinema-player@latest/dist/bundle" -}}
{{- end -}}
{{- $isRemoteBase := or (strings.HasPrefix $asciinemaBase "http://") (strings.HasPrefix $asciinemaBase "https://") -}}
{{- $minSuffix := cond hugo.IsProduction ".min" "" -}}
{{- /* CSS retrieval: get raw CSS from either local asset or remote, then process */ -}}
{{- if $isRemoteBase -}}
{{- $cssPath := cond (ne $asciinemaCssAsset "") $asciinemaCssAsset "asciinema-player.css" -}}
{{- $asciinemaCssUrl := printf "%s/%s" $asciinemaBase $cssPath -}}
{{- with try (resources.GetRemote $asciinemaCssUrl) -}}
{{- with .Err -}}
{{- errorf "Could not retrieve Asciinema css file from %s. Reason: %s." $asciinemaCssUrl . -}}
{{- else with .Value -}}
{{- with resources.Copy "css/asciinema-player.css" . -}}
{{- $asciinemaCss := . | fingerprint -}}
<link rel="stylesheet" href="{{ $asciinemaCss.RelPermalink }}" integrity="{{ $asciinemaCss.Data.Integrity }}" crossorigin="anonymous" />
{{- end -}}
{{- end -}}
{{- end -}}
{{- else if $asciinemaCssAsset -}}
{{- with resources.Get $asciinemaCssAsset -}}
{{- $asciinemaCss := . | fingerprint -}}
<link rel="stylesheet" href="{{ $asciinemaCss.RelPermalink }}" integrity="{{ $asciinemaCss.Data.Integrity }}" crossorigin="anonymous" />
{{- else -}}
{{- errorf "Asciinema css asset not found at %q" $asciinemaCssAsset -}}
{{- end -}}
{{- end -}}
{{- /* JS retrieval: get raw JS from either local asset or remote, then process */ -}}
{{- if $isRemoteBase -}}
{{- $jsPath := cond (ne $asciinemaJsAsset "") $asciinemaJsAsset (printf "asciinema-player%s.js" $minSuffix) -}}
{{- $asciinemaJsUrl := printf "%s/%s" $asciinemaBase $jsPath -}}
{{- with try (resources.GetRemote $asciinemaJsUrl) -}}
{{- with .Err -}}
{{- errorf "Could not retrieve Asciinema js file from %s. Reason: %s." $asciinemaJsUrl . -}}
{{- else with .Value -}}
{{- with resources.Copy (printf "js/asciinema-player%s.js" $minSuffix) . -}}
{{- $asciinemaJs := . | fingerprint -}}
<script defer src="{{ $asciinemaJs.RelPermalink }}" integrity="{{ $asciinemaJs.Data.Integrity }}" crossorigin="anonymous"></script>
{{- end -}}
{{- end -}}
{{- end -}}
{{- else if $asciinemaJsAsset -}}
{{- with resources.Get $asciinemaJsAsset -}}
{{- $asciinemaJs := . | fingerprint -}}
<script defer src="{{ $asciinemaJs.RelPermalink }}" integrity="{{ $asciinemaJs.Data.Integrity }}" crossorigin="anonymous"></script>
{{- else -}}
{{- errorf "Asciinema js asset not found at %q" $asciinemaJsAsset -}}
{{- end -}}
{{- end -}}
<script>
document.addEventListener("DOMContentLoaded", () => {
// Fix play button position issue
const style = document.createElement("style");
style.textContent = `
.ap-player .ap-overlay-start .ap-play-button span > svg {
display: inline;
}
`;
document.head.appendChild(style);
// Initialize asciinema players
document.querySelectorAll(".asciinema-player").forEach((el) => {
const castFile = el.dataset.castFile;
const theme = el.dataset.theme || "asciinema";
const speed = parseFloat(el.dataset.speed) || 1;
const autoplay = el.dataset.autoplay === "true";
const loop = el.dataset.loop === "true";
const poster = el.dataset.poster || "";
const markers = el.dataset.markers ? JSON.parse(el.dataset.markers) : [];
// Create asciinema player
if (window.AsciinemaPlayer) {
window.AsciinemaPlayer.create(castFile, el, {
theme: theme,
speed: speed,
autoplay: autoplay,
loop: loop,
poster: poster || undefined,
markers: markers.length > 0 ? markers : undefined,
controls: true, // Always show user controls (bottom control bar)
idleTimeLimit: 2, // Limit terminal inactivity to 2 seconds (compress pauses longer than 2s)
});
}
});
});
</script>

View File

@@ -0,0 +1,10 @@
{{- $scriptsBody := slice }}
{{- range resources.Match "js/core/*.js" -}}
{{ $scriptsBody = $scriptsBody | append (resources.ExecuteAsTemplate .Name $ .) }}
{{- end -}}
{{- $scripts := $scriptsBody | resources.Concat "js/main.js" -}}
{{- if hugo.IsProduction -}}
{{- $scripts = $scripts | minify | fingerprint -}}
{{- end -}}
<script defer src="{{ $scripts.RelPermalink }}" integrity="{{ $scripts.Data.Integrity }}"></script>

View File

@@ -0,0 +1,92 @@
{{- /* KaTeX CSS loader
Behavior (driven by site.params.math.katex):
- base (remote URL) + optional css:
- Construct remote CSS URL: "{{ base }}/{{ css | default "katex[.min].css" }}".
- Fetch via resources.GetRemote, rewrite font URLs to "{{ base }}/fonts/...".
- Build and fingerprint; emit <link rel="stylesheet" integrity>.
- base (local path or not set) + css (asset path):
- Read CSS from Hugo assets via resources.Get; DO NOT rewrite font URLs.
- Build and fingerprint; emit <link rel="stylesheet" integrity>.
- base (local path) only (no css):
- Link directly to "{{ base }}/katex[.min].css" (no processing).
- Nothing set:
- Default to CDN latest base; same as remote path above.
Additional:
- assets: optional list to publish extra assets. CSS/JS get tags with integrity (JS loads async).
*/ -}}
{{- $noop := .WordCount -}}
{{- $katexBase := "" -}}
{{- with site.Params.math.katex.base -}}
{{- $katexBase = . -}}
{{- else -}}
{{- if not site.Params.math.katex.css -}}
{{- $katexBase = "https://cdn.jsdelivr.net/npm/katex@latest/dist" -}}
{{- end -}}
{{- end -}}
{{- $katexCssAsset := "" -}}
{{- with site.Params.math.katex.css -}}
{{- $katexCssAsset = . -}}
{{- end -}}
{{- $s := newScratch -}}
{{- $isRemoteBase := or (strings.HasPrefix $katexBase "http://") (strings.HasPrefix $katexBase "https://") -}}
{{- /* CSS retrieval consolidated: get raw CSS from either local asset or remote, then process once */ -}}
{{- $minSuffix := cond hugo.IsProduction ".min" "" -}}
{{- if $isRemoteBase -}}
{{- $cssPath := cond (ne $katexCssAsset "") $katexCssAsset (printf "katex%s.css" $minSuffix) -}}
{{- $katexCssUrl := printf "%s/%s" $katexBase $cssPath -}}
{{- with try (resources.GetRemote $katexCssUrl) -}}
{{- with .Err -}}
{{- errorf "Could not retrieve KaTeX css file from %s. Reason: %s." $katexCssUrl . -}}
{{- else with .Value -}}
{{- $s.Set "katexCssValue" .Content -}}
{{- end -}}
{{- end -}}
{{- else if $katexCssAsset -}}
{{- with resources.Get $katexCssAsset -}}
{{- $s.Set "katexCssValue" .Content -}}
{{- else -}}
{{- errorf "KaTeX css asset not found at %q" $katexCssAsset -}}
{{- end -}}
{{- end -}}
{{- with $s.Get "katexCssValue" -}}
{{- $cssContent := . -}}
{{- if $isRemoteBase -}}
{{- $fontPattern := "url(fonts/" -}}
{{- $fontSub := printf "url(%s/fonts/" $katexBase -}}
{{- $cssContent = strings.Replace $cssContent $fontPattern $fontSub -}}
{{- end -}}
{{- with resources.FromString (printf "css/katex%s.css" $minSuffix) $cssContent -}}
{{- $css := . | fingerprint "sha512" -}}
<link rel="stylesheet" href="{{ $css.RelPermalink }}" integrity="{{ $css.Data.Integrity }}" />
{{- end -}}
{{- else -}}
{{- if not $isRemoteBase -}}
{{- $cssPath := cond (ne $katexCssAsset "") $katexCssAsset (printf "katex%s.css" $minSuffix) -}}
<link rel="stylesheet" href="{{ printf "%s/%s" $katexBase $cssPath }}" />
{{- end -}}
{{- end -}}
{{- /* Optionally publish files (fonts, css, js, etc.) from assets and emit tags for css/js with integrity and crossorigin */ -}}
{{- with site.Params.math.katex.assets -}}
{{- range . -}}
{{- with resources.Get . -}}
{{- $name := .Name | lower -}}
{{- if strings.HasSuffix $name ".css" -}}
{{- $built := . | fingerprint "sha512" -}}
<link rel="stylesheet" href="{{ $built.RelPermalink }}" integrity="{{ $built.Data.Integrity }}" crossorigin="anonymous" />
{{- else if or (strings.HasSuffix $name ".js") (strings.HasSuffix $name ".mjs") -}}
{{- $built := . | fingerprint "sha512" -}}
<script src="{{ $built.RelPermalink }}" async integrity="{{ $built.Data.Integrity }}" crossorigin="anonymous"></script>
{{- else -}}
{{- .Publish -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,20 @@
{{/* MathJax */}}
{{ $mathjaxJsUrl := "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" -}}
<script defer id="MathJax-script" src="{{ $mathjaxJsUrl }}" crossorigin="anonymous" async></script>
<script>
MathJax = {
loader: {
load: ["ui/safe"],
},
tex: {
displayMath: [
["\\[", "\\]"],
["$$", "$$"],
],
inlineMath: [
["\\(", "\\)"],
["$", "$"],
],
},
};
</script>

View File

@@ -0,0 +1,79 @@
{{- /* Mermaid */ -}}
{{- $mermaidBase := "" -}}
{{- $useDefaultCdn := true -}}
{{- with site.Params.mermaid.base -}}
{{- $mermaidBase = . -}}
{{- $useDefaultCdn = false -}}
{{- end -}}
{{- $mermaidJsAsset := "" -}}
{{- with site.Params.mermaid.js -}}
{{- $mermaidJsAsset = . -}}
{{- end -}}
{{- /* If only js is set without base, use local asset loading */ -}}
{{- if and $useDefaultCdn (ne $mermaidJsAsset "") -}}
{{- $useDefaultCdn = false -}}
{{- end -}}
{{- /* Set default CDN base if needed */ -}}
{{- if $useDefaultCdn -}}
{{- $mermaidBase = "https://cdn.jsdelivr.net/npm/mermaid@latest/dist" -}}
{{- end -}}
{{- $isRemoteBase := or (strings.HasPrefix $mermaidBase "http://") (strings.HasPrefix $mermaidBase "https://") -}}
{{- $minSuffix := cond hugo.IsProduction ".min" "" -}}
{{- /* JS retrieval: get raw JS from either local asset or remote, then process */ -}}
{{- if $isRemoteBase -}}
{{- $jsPath := cond (ne $mermaidJsAsset "") $mermaidJsAsset (printf "mermaid%s.js" $minSuffix) -}}
{{- $mermaidJsUrl := printf "%s/%s" $mermaidBase $jsPath -}}
{{- with try (resources.GetRemote $mermaidJsUrl) -}}
{{- with .Err -}}
{{- errorf "Could not retrieve Mermaid js file from %s. Reason: %s." $mermaidJsUrl . -}}
{{- else with .Value -}}
{{- with resources.Copy (printf "js/mermaid%s.js" $minSuffix) . -}}
{{- $mermaidJs := . | fingerprint -}}
<script defer src="{{ $mermaidJs.RelPermalink }}" integrity="{{ $mermaidJs.Data.Integrity }}" crossorigin="anonymous"></script>
{{- end -}}
{{- end -}}
{{- end -}}
{{- else if $mermaidJsAsset -}}
{{- with resources.Get $mermaidJsAsset -}}
{{- $mermaidJs := . | fingerprint -}}
<script defer src="{{ $mermaidJs.RelPermalink }}" integrity="{{ $mermaidJs.Data.Integrity }}" crossorigin="anonymous"></script>
{{- else -}}
{{- errorf "Mermaid js asset not found at %q" $mermaidJsAsset -}}
{{- end -}}
{{- end -}}
<script>
document.addEventListener("DOMContentLoaded", () => {
// Store original mermaid code for each diagram
document.querySelectorAll(".mermaid").forEach((el) => {
el.dataset.original = el.innerHTML;
});
const theme = document.documentElement.classList.contains("dark") ? "dark" : "default";
mermaid.initialize({ startOnLoad: true, theme: theme });
let timeout;
new MutationObserver(() => {
clearTimeout(timeout);
timeout = setTimeout(() => {
const theme = document.documentElement.classList.contains("dark") ? "dark" : "default";
document.querySelectorAll(".mermaid").forEach((el) => {
// Reset to original content, preserving HTML
el.innerHTML = el.dataset.original;
el.removeAttribute("data-processed");
});
mermaid.initialize({ startOnLoad: true, theme: theme });
mermaid.init();
}, 150);
}).observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
});
</script>

View File

@@ -0,0 +1,26 @@
{{/* Search */}}
{{- if (site.Params.search.enable | default true) -}}
{{- $searchType := site.Params.search.type | default "flexsearch" -}}
{{- if eq $searchType "flexsearch" -}}
{{- $jsSearchScript := printf "%s.search.js" .Language.Lang -}}
{{- $jsSearch := resources.Get "js/flexsearch.js" | resources.ExecuteAsTemplate $jsSearchScript . -}}
{{- if hugo.IsProduction -}}
{{- $jsSearch = $jsSearch | minify | fingerprint -}}
{{- end -}}
{{- $flexSearchVersion := site.Params.search.flexsearch.version | default "0.8.143" -}}
{{- $flexSearchJsUrl := printf "https://cdn.jsdelivr.net/npm/flexsearch@%s/dist/flexsearch.bundle%s.js" $flexSearchVersion (cond hugo.IsProduction ".min" ".debug") -}}
{{ with try (resources.GetRemote $flexSearchJsUrl) -}}
{{ with .Err -}}
{{ errorf "Could not retrieve FlexSearch js file from %s. Reason: %s." $flexSearchJsUrl . -}}
{{ else with.Value -}}
{{ with resources.Copy (printf "js/flexsearch.js") . -}}
{{ $flexSearchJs := . | fingerprint -}}
<script defer src="{{ $flexSearchJs.RelPermalink }}" integrity="{{ $flexSearchJs.Data.Integrity }}" crossorigin="anonymous"></script>
{{ end -}}
{{ end -}}
{{ end -}}
<script defer src="{{ $jsSearch.RelPermalink }}" integrity="{{ $jsSearch.Data.Integrity }}"></script>
{{- else -}}
{{- warnf `search type "%s" is not supported` $searchType -}}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,26 @@
{{- $placeholder := (T "searchPlaceholder") | default "Search..." -}}
<div class="hextra-search-wrapper hx:relative hx:md:w-64">
<div class="hx:relative hx:flex hx:items-center hx:text-gray-900 hx:contrast-more:text-gray-800 hx:dark:text-gray-300 hx:contrast-more:dark:text-gray-300">
<input
placeholder="{{ $placeholder }}"
class="hextra-search-input hx:focus:hextra-focus hx:block hx:w-full hx:appearance-none hx:rounded-lg hx:px-3 hx:py-2 hx:transition-colors hx:text-base hx:leading-tight hx:md:text-sm hx:bg-black/[.05] hx:dark:bg-gray-50/10 hx:focus:bg-white hx:dark:focus:bg-dark hx:placeholder:text-gray-500 hx:dark:placeholder:text-gray-400 hx:contrast-more:border hx:contrast-more:border-current"
type="search"
value=""
spellcheck="false"
/>
<kbd
class="hx:absolute hx:my-1.5 hx:select-none hx:ltr:right-1.5 hx:rtl:left-1.5 hx:h-5 hx:rounded-sm hx:bg-white hx:px-1.5 hx:font-mono hx:text-[10px] hx:font-medium hx:text-gray-500 hx:border hx:border-gray-200 hx:dark:border-gray-100/20 hx:dark:bg-dark/50 hx:contrast-more:border-current hx:contrast-more:text-current hx:contrast-more:dark:border-current hx:items-center hx:gap-1 hx:transition-opacity hx:pointer-events-none hx:hidden hx:sm:flex"
>
CTRL K
</kbd>
</div>
<div>
<ul
class="hextra-search-results hextra-scrollbar hx:hidden hx:border hx:border-gray-200 hx:bg-white hx:text-gray-100 hx:dark:border-neutral-800 hx:dark:bg-neutral-900 hx:absolute hx:top-full hx:z-20 hx:mt-2 hx:overflow-auto hx:overscroll-contain hx:rounded-xl hx:py-2.5 hx:shadow-xl hx:max-h-[min(calc(50vh-11rem-env(safe-area-inset-bottom)),400px)] hx:md:max-h-[min(calc(100vh-5rem-env(safe-area-inset-bottom)),400px)] hx:inset-x-0 hx:ltr:md:left-auto hx:rtl:md:right-auto hx:contrast-more:border hx:contrast-more:border-gray-900 hx:contrast-more:dark:border-gray-50 hx:w-screen hx:min-h-[100px] hx:max-w-[min(calc(100vw-2rem),calc(100%+20rem))]"
style="transition: max-height 0.2s ease 0s;"
></ul>
</div>
</div>

View File

@@ -0,0 +1,38 @@
{{- $content := .content -}}
{{- $color := .color | default .type | default "" -}}{{- /* Compatibility with previous parameter. */ -}}
{{- $class := .class | default "" -}}
{{- $border := .border | default false -}}
{{- $icon := .icon | default "" -}}
{{- /* Compatibility with previous names. */ -}}
{{- $mapping := (dict
"default" "gray"
"tip" "green"
"info" "blue"
"warning" "yellow"
"error" "red"
"important" "purple"
)
-}}
{{- $color = index $mapping $color | default $color | default "gray" -}}
{{- $styleClass := newScratch -}}
{{- $styleClass.Set "gray" "hx:text-gray-600 hx:bg-gray-100 hx:dark:bg-neutral-800 hx:dark:text-neutral-200 hx:border-gray-200 hx:dark:border-neutral-700" -}}
{{- $styleClass.Set "purple" "hx:border-purple-200 hx:bg-purple-100 hx:text-purple-900 hx:dark:border-purple-200/30 hx:dark:bg-purple-900/30 hx:dark:text-purple-200" -}}
{{- $styleClass.Set "indigo" "hx:border-indigo-200 hx:bg-indigo-100 hx:text-indigo-900 hx:dark:border-indigo-200/30 hx:dark:bg-indigo-900/30 hx:dark:text-indigo-200" -}}
{{- $styleClass.Set "blue" "hx:border-blue-200 hx:bg-blue-100 hx:text-blue-900 hx:dark:border-blue-200/30 hx:dark:bg-blue-900/30 hx:dark:text-blue-200" -}}
{{- $styleClass.Set "green" "hx:border-green-200 hx:bg-green-100 hx:text-green-900 hx:dark:border-green-200/30 hx:dark:bg-green-900/30 hx:dark:text-green-200" -}}
{{- $styleClass.Set "yellow" "hx:border-yellow-100 hx:bg-yellow-50 hx:text-yellow-900 hx:dark:border-yellow-200/30 hx:dark:bg-yellow-700/30 hx:dark:text-yellow-200" -}}
{{- $styleClass.Set "orange" "hx:border-orange-100 hx:bg-orange-50 hx:text-orange-800 hx:dark:border-orange-400/30 hx:dark:bg-orange-400/20 hx:dark:text-orange-300" -}}
{{- $styleClass.Set "amber" "hx:border-amber-200 hx:bg-amber-100 hx:text-amber-900 hx:dark:border-amber-200/30 hx:dark:bg-amber-900/30 hx:dark:text-amber-200" -}}
{{- $styleClass.Set "red" "hx:border-red-200 hx:bg-red-100 hx:text-red-900 hx:dark:border-red-200/30 hx:dark:bg-red-900/30 hx:dark:text-red-200" -}}
{{- $borderClass := cond (eq $border true) "hx:border" "" -}}
{{- $badgeClass := or ($styleClass.Get $color) ($styleClass.Get "gray") -}}
<div class="hextra-badge {{ $class }}">
<div class="hx:inline-flex hx:gap-1 hx:items-center hx:rounded-full hx:px-2.5 hx:leading-6 hx:text-[.65rem] {{ $borderClass }} {{ $badgeClass }}">
{{- with $icon -}}{{- partial "utils/icon" (dict "name" . "attributes" "height=12") -}}{{- end -}}
{{- $content -}}
</div>
</div>
{{- /* Strip trailing newline. */ -}}

View File

@@ -0,0 +1,28 @@
{{- $content := .content -}}
{{- $emoji := .emoji -}}
{{- $icon := .icon -}}
{{- $defaultClass := "hx:border-orange-100 hx:bg-orange-50 hx:text-orange-800 hx:dark:border-orange-400/30 hx:dark:bg-orange-400/20 hx:dark:text-orange-300" -}}
{{- $class := .class | default $defaultClass -}}
<div class="hx:overflow-x-auto hx:mt-6 hx:flex hx:rounded-lg hx:border hx:py-2 hx:ltr:pr-4 hx:rtl:pl-4 hx:contrast-more:border-current hx:contrast-more:dark:border-current {{ $class }}">
<div class="hx:ltr:pl-3 hx:ltr:pr-2 hx:rtl:pr-3 hx:rtl:pl-2">
{{- with $emoji -}}
<div class="hx:select-none hx:text-xl" style="font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';">
{{- . -}}
</div>
{{- else -}}
{{- with $icon -}}
{{ partial "utils/icon.html" (dict "name" . "attributes" `height=1.2em class="hx:inline-block hx:align-middle"`) -}}
{{- end -}}
{{- end -}}
</div>
<div class="hx:w-full hx:min-w-0 hx:leading-7">
<div class="hx:mt-6 hx:leading-7 hx:first:mt-0">
{{- $content -}}
</div>
</div>
</div>

View File

@@ -0,0 +1,69 @@
{{- $link := .link -}}
{{- $title := .title -}}
{{- $icon := .icon -}}
{{- $subtitle := .subtitle -}}
{{- $image := .image -}}
{{- $alt := .alt | default $title -}}
{{- $width := .width -}}
{{- $height := .height -}}
{{- $imageStyle := .imageStyle -}}
{{- $tag := .tag -}}
{{- $tagColor := .tagColor | default .tagType | default "" -}}{{- /* Compatibility with previous parameter. */ -}}
{{- $tagBorder := not (eq .tagBorder false) | default true }}
{{- $tagIcon := .tagIcon -}}
{{ $linkClass := "hx:hover:border-gray-300 hx:bg-transparent hx:shadow-xs hx:dark:border-neutral-800 hx:hover:bg-slate-50 hx:hover:shadow-md hx:dark:hover:border-neutral-700 hx:dark:hover:bg-neutral-900" }}
{{- with $image -}}
{{ $linkClass = "hx:hover:border-gray-300 hx:bg-gray-100 hx:shadow-sm hx:dark:border-neutral-700 hx:dark:bg-neutral-800 hx:dark:text-gray-50 hx:hover:shadow-lg hx:dark:hover:border-neutral-500 hx:dark:hover:bg-neutral-700" }}
{{- end -}}
{{- $external := strings.HasPrefix $link "http" -}}
{{- $href := cond (strings.HasPrefix $link "/") ($link | relURL) $link -}}
<a
class="hextra-card hx:group hx:flex hx:flex-col hx:justify-start hx:overflow-hidden hx:rounded-lg hx:border hx:border-gray-200 hx:text-current hx:no-underline hx:dark:shadow-none hx:hover:shadow-gray-100 hx:dark:hover:shadow-none hx:shadow-gray-100 hx:active:shadow-sm hx:active:shadow-gray-200 hx:transition-all hx:duration-200 {{ $linkClass }}"
{{- if $link -}}
href="{{ $href }}"
{{ with $external }}target="_blank" rel="noreferrer"{{ end -}}
{{- end -}}
>
{{- with $image -}}
<img
alt="{{ $alt }}"
class="hextra-card-image"
loading="lazy"
decoding="async"
src="{{ $image | safeURL }}"
{{ with $width }}width="{{ . }}"{{ end }}
{{ with $height }}height="{{ . }}"{{ end }}
{{ with $imageStyle }}style="{{ . | safeCSS }}"{{ end }}
/>
{{- end -}}
{{- $padding := "hx:p-4" -}}
{{- with $subtitle -}}
{{- $padding = "hx:pt-4 hx:px-4" -}}
{{- end -}}
<span class="hextra-card-icon hx:flex hx:font-semibold hx:items-start hx:gap-2 {{ $padding }} hx:text-gray-700 hx:hover:text-gray-900 hx:dark:text-neutral-200 hx:dark:hover:text-neutral-50">
{{- with $icon }}{{ partial "utils/icon.html" (dict "name" $icon) -}}{{- end -}}
{{- $title -}}
</span>
{{- with $subtitle -}}
<div class="hextra-card-subtitle hx:line-clamp-3 hx:text-sm hx:font-normal hx:text-gray-500 hx:dark:text-gray-400 hx:px-4 hx:mb-4 hx:mt-2">{{- $subtitle | markdownify -}}</div>
{{- end -}}
{{- if $tag }}
{{- partial "shortcodes/badge.html" (dict
"content" $tag
"color" $tagColor
"class" "hextra-card-tag"
"border" $tagBorder
"icon" $tagIcon
)
-}}
{{- end -}}
</a>
{{- /* Strip trailing newline. */ -}}

View File

@@ -0,0 +1,6 @@
{{- $cols := .cols | default 3 -}}
{{- $content := .content -}}
<div class="hextra-cards hx:mt-4 hx:gap-4 hx:grid not-prose" style="--hextra-cards-grid-cols: {{ $cols }};">
{{- $content -}}
</div>

View File

@@ -0,0 +1,69 @@
{{- $tabsID := .id }}
{{- /*
The `tabs` parameter is a list of dict with the following keys:
- `id`: (int) the ID of the tab (the Ordinal of the tab shortcode).
- `name`: (string) the name of the tab (the title).
- `content`: (string) the content of the tab.
- `selected`: (bool) whether the tab is selected.
*/ -}}
{{- $tabs := .tabs }}
{{- if eq (len $tabs) 0 -}}
{{ errorf "tabs must have at least one tab" }}
{{- end -}}
{{- $enableSync := .enableSync }}
{{- /* Create group data for syncing and select the first tab if none is selected. */ -}}
{{- $selectedIndex := 0 -}}
{{ $dataTabGroup := slice -}}
{{- range $i, $item := $tabs -}}
{{- $dataTabGroup = $dataTabGroup | append ($item.name) -}}
{{- if $item.selected -}}
{{- $selectedIndex = $i -}}
{{- end -}}
{{- end -}}
{{- /* Generate a unique ID for each tab group. */ -}}
{{- $globalID := printf "tabs-%02v" $tabsID -}}
<div class="hextra-scrollbar hx:overflow-x-auto hx:overflow-y-hidden hx:overscroll-x-contain">
<div
class="hx:mt-4 hx:flex hx:w-max hx:min-w-full hx:border-b hx:border-gray-200 hx:pb-px hx:dark:border-neutral-800"
{{ if $enableSync }} data-tab-group="{{ delimit $dataTabGroup `,` }}"{{ end }}
>
{{- range $i, $item := $tabs -}}
<button
class="hextra-tabs-toggle hx:cursor-pointer hx:data-[state=selected]:border-primary-500 hx:data-[state=selected]:text-primary-600 hx:data-[state=selected]:dark:border-primary-500 hx:data-[state=selected]:dark:text-primary-600 hx:mr-2 hx:rounded-t hx:p-2 hx:font-medium hx:leading-5 hx:transition-colors hx:-mb-0.5 hx:select-none hx:border-b-2 hx:border-transparent hx:text-gray-600 hx:hover:border-gray-200 hx:hover:text-black hx:dark:text-gray-200 hx:dark:hover:border-neutral-800 hx:dark:hover:text-white"
role="tab"
type="button"
aria-controls="tabs-panel-{{ $globalID }}-{{ $item.id }}"
{{- if eq $i $selectedIndex -}}
aria-selected="true"
tabindex="0"
data-state="selected"
{{- end }}
>
{{- $item.name -}}
</button>
{{- end -}}
</div>
</div>
<div>
{{- range $i, $item := $tabs -}}
<div
class="hextra-tabs-panel hx:rounded-sm hx:pt-6 hx:hidden hx:data-[state=selected]:block"
id="tabs-panel-{{ $globalID }}-{{ $item.id }}"
role="tabpanel"
{{- if eq $i $selectedIndex -}}
tabindex="0"
data-state="selected"
{{ end -}}
>
{{- $item.content | markdownify -}}
</div>
{{- end -}}
</div>

View File

@@ -0,0 +1,188 @@
{{- $context := .context -}}
{{- $disableSidebar := .disableSidebar | default false -}}
{{- $displayPlaceholder := .displayPlaceholder | default false -}}
{{- $navRoot := cond (eq site.Home.Type "docs") site.Home $context.FirstSection -}}
{{- $pageURL := $context.RelPermalink -}}
{{- if .context.Params.sidebar.hide -}}
{{- $disableSidebar = true -}}
{{- $displayPlaceholder = false -}}
{{- end -}}
{{- $sidebarClass := "hx:md:sticky" -}}
{{- if $disableSidebar -}}
{{- if $displayPlaceholder -}}
{{- $sidebarClass = "hx:md:hidden hx:xl:block" -}}
{{- else -}}
{{- $sidebarClass = "hx:md:hidden" -}}
{{- end -}}
{{- end -}}
<aside class="hextra-sidebar-container hx:flex hx:flex-col hx:print:hidden hx:md:top-16 hx:md:shrink-0 hx:md:w-64 hx:md:self-start hx:max-md:[transform:translate3d(0,-100%,0)] {{ $sidebarClass }}">
<!-- Search bar on small screen -->
<div class="hx:px-4 hx:pt-4 hx:md:hidden">
{{ partial "search.html" }}
</div>
<div class="hextra-scrollbar hx:overflow-y-auto hx:overflow-x-hidden hx:p-4 hx:grow hx:md:h-[calc(100vh-var(--navbar-height)-var(--menu-height))]">
<ul class="hx:flex hx:flex-col hx:gap-1 hx:md:hidden">
<!-- Nav -->
{{ template "sidebar-main" (dict "context" site.Home "pageURL" $pageURL "page" $context "toc" true) -}}
{{ template "sidebar-footer" }}
</ul>
<!-- Sidebar on large screen -->
{{- if $disableSidebar -}}
{{- if $displayPlaceholder }}<div class="hx:max-xl:hidden hx:h-0 hx:w-64 hx:shrink-0"></div>{{ end -}}
{{ .context.Store.Set "enableFooterSwitches" true }}
{{- else -}}
<ul class="hx:flex hx:flex-col hx:gap-1 hx:max-md:hidden">
{{ template "sidebar-main" (dict "context" $navRoot "page" $context "pageURL" $pageURL) }}
{{ template "sidebar-footer" }}
</ul>
{{ end -}}
</div>
{{/* Hide theme switch when sidebar is disabled */}}
{{ $switchesClass := cond $disableSidebar "hx:md:hidden" "" -}}
{{ $displayThemeToggle := (site.Params.theme.displayToggle | default true) -}}
{{ if or hugo.IsMultilingual $displayThemeToggle }}
<div class="{{ $switchesClass }} {{ with hugo.IsMultilingual }}hx:justify-end{{ end }} hx:sticky hx:bottom-0 hx:max-h-(--menu-height) hx:bg-white hx:dark:bg-dark hx:mx-4 hx:py-4 hx:shadow-[0_-12px_16px_#fff] hx:flex hx:items-center hx:gap-2 hx:border-gray-200 hx:dark:border-neutral-800 hx:dark:shadow-[0_-12px_16px_#111] hx:contrast-more:border-neutral-400 hx:contrast-more:shadow-none hx:contrast-more:dark:shadow-none hx:border-t" data-toggle-animation="show">
{{- with hugo.IsMultilingual -}}
{{- partial "language-switch" (dict "context" $context "grow" true) -}}
{{- with $displayThemeToggle }}{{ partial "theme-toggle" (dict "hideLabel" true "location" "bottom-right") }}{{ end -}}
{{- else -}}
{{- with $displayThemeToggle -}}
<div class="hx:flex hx:grow hx:flex-col">{{ partial "theme-toggle" }}</div>
{{- end -}}
{{- end -}}
</div>
{{- end -}}
</aside>
{{- define "sidebar-main" -}}
{{ template "sidebar-tree" (dict "context" .context "level" 0 "page" .page "pageURL" .pageURL "toc" (.toc | default false)) }}
{{- end -}}
{{- define "sidebar-tree" -}}
{{- if ge .level 4 -}}
{{- return -}}
{{- end -}}
{{- $context := .context -}}
{{- $page := .page }}
{{- $pageURL := .page.RelPermalink -}}
{{- $level := .level -}}
{{- $toc := .toc | default false -}}
{{- with $items := union .context.RegularPages .context.Sections -}}
{{- $items = where $items "Params.sidebar.exclude" "!=" true -}}
{{- if eq $level 0 -}}
{{- range $items.ByWeight }}
{{- if .Params.sidebar.separator -}}
<li class="[word-break:break-word] hx:mt-5 hx:mb-2 hx:px-2 hx:py-1.5 hx:text-sm hx:font-semibold hx:text-gray-900 hx:first:mt-0 hx:dark:text-gray-100">
<span class="hx:cursor-default">{{ partial "utils/title" . }}</span>
</li>
{{- else -}}
{{- $active := eq $pageURL .RelPermalink -}}
{{- $shouldOpen := or (.Params.sidebar.open) (.IsAncestor $page) $active | default true }}
<li class="{{ if $shouldOpen }}open{{ end }}">
{{- $linkTitle := partial "utils/title" . -}}
{{- template "sidebar-item-link" dict "context" . "active" $active "title" $linkTitle "link" .RelPermalink -}}
{{- if and $toc $active -}}
{{- template "sidebar-toc" dict "page" . -}}
{{- end -}}
{{- template "sidebar-tree" dict "context" . "page" $page "pageURL" $pageURL "level" (add $level 1) "toc" $toc -}}
</li>
{{- end -}}
{{- end -}}
{{- else -}}
<div class="hx:ltr:pr-0 hx:overflow-hidden">
<ul class='hx:relative hx:flex hx:flex-col hx:gap-1 hx:before:absolute hx:before:inset-y-1 hx:before:w-px hx:before:bg-gray-200 hx:before:content-[""] hx:ltr:ml-3 hx:ltr:pl-3 hx:ltr:before:left-0 hx:rtl:mr-3 hx:rtl:pr-3 hx:rtl:before:right-0 hx:dark:before:bg-neutral-800'>
{{- range $items.ByWeight }}
{{- $active := eq $pageURL .RelPermalink -}}
{{- $shouldOpen := or (.Params.sidebar.open) (.IsAncestor $page) $active | default true }}
{{- $linkTitle := partial "utils/title" . -}}
<li class="hx:flex hx:flex-col {{ if $shouldOpen }}open{{ end }}">
{{- template "sidebar-item-link" dict "context" . "active" $active "title" $linkTitle "link" .RelPermalink -}}
{{- if and $toc $active -}}
{{ template "sidebar-toc" dict "page" . }}
{{- end }}
{{ template "sidebar-tree" dict "context" . "page" $page "pageURL" $pageURL "level" (add $level 1) "toc" $toc }}
</li>
{{- end -}}
</ul>
</div>
{{- end -}}
{{- end }}
{{- end -}}
{{- define "sidebar-toc" -}}
{{ $page := .page }}
{{ with $page.Fragments.Headings }}
<ul class='hx:flex hx:flex-col hx:gap-1 hx:relative hx:before:absolute hx:before:inset-y-1 hx:before:w-px hx:before:bg-gray-200 hx:before:content-[""] hx:dark:before:bg-neutral-800 hx:ltr:pl-3 hx:ltr:before:left-0 hx:rtl:pr-3 hx:rtl:before:right-0 hx:ltr:ml-3 hx:rtl:mr-3'>
{{- range . }}
{{- with .Headings }}
{{- range . -}}
<li>
<a
href="#{{ anchorize .ID }}"
class="hx:flex hx:rounded-sm hx:px-2 hx:py-1.5 hx:text-sm hx:transition-colors [word-break:break-word] hx:cursor-pointer [-webkit-tap-highlight-color:transparent] [-webkit-touch-callout:none] hx:contrast-more:border hx:gap-2 hx:before:opacity-25 hx:before:content-['#'] hx:text-gray-500 hx:hover:bg-gray-100 hx:hover:text-gray-900 hx:dark:text-neutral-400 hx:dark:hover:bg-primary-100/5 hx:dark:hover:text-gray-50 hx:contrast-more:text-gray-900 hx:contrast-more:dark:text-gray-50 hx:contrast-more:border-transparent hx:contrast-more:hover:border-gray-900 hx:contrast-more:dark:hover:border-gray-50"
>
{{- .Title | safeHTML | plainify | htmlUnescape -}}
</a>
</li>
{{ end -}}
{{ end -}}
{{ end -}}
</ul>
{{ end }}
{{- end -}}
{{- define "sidebar-footer" -}}
{{- range site.Menus.sidebar -}}
{{- $name := or (T .Identifier) .Name -}}
{{ if eq .Params.type "separator" }}
<li class="[word-break:break-word] hx:mt-5 hx:mb-2 hx:px-2 hx:py-1.5 hx:text-sm hx:font-semibold hx:text-gray-900 hx:first:mt-0 hx:dark:text-gray-100">
<span class="hx:cursor-default">{{ $name }}</span>
</li>
{{ else }}
{{- $link := .URL -}}
{{- with .PageRef -}}
{{- if hasPrefix . "/" -}}
{{- $link = relLangURL (strings.TrimPrefix "/" .) -}}
{{- end -}}
{{- end -}}
<li>{{ template "sidebar-item-link" dict "active" false "title" $name "link" $link }}</li>
{{ end }}
{{- end -}}
{{- end -}}
{{- define "sidebar-item-link" -}}
{{- $external := strings.HasPrefix .link "http" -}}
{{- $open := .open | default true -}}
<a
class="hx:flex hx:items-center hx:justify-between hx:gap-2 hx:cursor-pointer hx:rounded-sm hx:px-2 hx:py-1.5 hx:text-sm hx:transition-colors [-webkit-tap-highlight-color:transparent] [-webkit-touch-callout:none] [word-break:break-word]
{{- if .active }}
hextra-sidebar-active-item hx:bg-primary-100 hx:font-semibold hx:text-primary-800 hx:contrast-more:border hx:contrast-more:border-primary-500 hx:dark:bg-primary-400/10 hx:dark:text-primary-600 hx:contrast-more:dark:border-primary-500
{{- else }}
hx:text-gray-500 hx:hover:bg-gray-100 hx:hover:text-gray-900 hx:contrast-more:border hx:contrast-more:border-transparent hx:contrast-more:text-gray-900 hx:contrast-more:hover:border-gray-900 hx:dark:text-neutral-400 hx:dark:hover:bg-primary-100/5 hx:dark:hover:text-gray-50 hx:contrast-more:dark:text-gray-50 hx:contrast-more:dark:hover:border-gray-50
{{- end -}}"
href="{{ .link }}"
{{ if $external }}target="_blank" rel="noreferrer"{{ end }}
>
{{- .title -}}
{{- with .context }}
{{- if or .RegularPages .Sections }}
<span class="hextra-sidebar-collapsible-button">
{{- template "sidebar-collapsible-button" -}}
</span>
{{- end }}
{{ end -}}
</a>
{{- end -}}
{{- define "sidebar-collapsible-button" -}}
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" class="hx:h-[18px] hx:min-w-[18px] hx:rounded-xs hx:p-0.5 hx:hover:bg-gray-800/5 hx:dark:hover:bg-gray-100/5"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" class="hx:origin-center hx:transition-transform hx:rtl:-rotate-180"></path></svg>
{{- end -}}

View File

@@ -0,0 +1,7 @@
{{- $context := .context -}}
{{- range $tag := $context.Params.tags -}}
{{- with $context.Site.GetPage (printf "/tags/%s" $tag) -}}
<a class="hx:inline-block hx:whitespace-nowrap hx:mr-2 hx:text-gray-500 hx:hover:text-gray-900 hx:dark:text-gray-400 hx:dark:hover:text-gray-100 hx:contrast-more:text-gray-800 hx:contrast-more:dark:text-gray-50" href="{{ .RelPermalink }}">#{{ $tag }}</a>
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,68 @@
{{- $hideLabel := .hideLabel -}}
{{- $iconHeight := .iconHeight | default 12 -}}
{{- $class := .class | default "hx:h-7 hx:px-2 hx:text-xs hx:hover:bg-gray-100 hx:hover:text-gray-900 hx:dark:hover:bg-primary-100/5 hx:dark:hover:text-gray-50 hx:font-medium hx:text-gray-600 hx:transition-colors hx:dark:text-gray-400" -}}
{{- $location := .location | default "bottom" -}}
{{- $changeTheme := (T "changeTheme") | default "Change theme" -}}
{{- $light := (T "light") | default "Light" -}}
{{- $dark := (T "dark") | default "Dark" -}}
{{- $system := (T "system") | default "System" -}}
<div class="hx:flex hx:justify-items-start hx:group" data-theme="light">
<button
title="{{ $changeTheme }}"
data-state="closed"
data-location="{{ $location }}"
class="hextra-theme-toggle hx:cursor-pointer hx:rounded-md hx:text-left hx:font-medium {{ $class }} hx:grow"
type="button"
aria-label="{{ $changeTheme }}"
>
<div class="hx:flex hx:items-center hx:gap-2 hx:capitalize">
{{- partial "utils/icon.html" (dict "name" "sun" "attributes" (printf `height=%d class="hx:group-data-[theme=dark]:hidden hx:group-data-[theme=system]:hidden"` $iconHeight)) -}}
{{- if not $hideLabel }}<span class="hx:group-data-[theme=dark]:hidden hx:group-data-[theme=system]:hidden">{{ $light }}</span>{{ end -}}
{{- partial "utils/icon.html" (dict "name" "moon" "attributes" (printf `height=%d class="hx:group-data-[theme=light]:hidden hx:group-data-[theme=system]:hidden"` $iconHeight)) -}}
{{- if not $hideLabel }}<span class="hx:group-data-[theme=light]:hidden hx:group-data-[theme=system]:hidden">{{ $dark }}</span>{{ end -}}
{{- partial "utils/icon.html" (dict "name" "contrast" "attributes" (printf `height=%d class="hx:group-data-[theme=dark]:hidden hx:group-data-[theme=light]:hidden"` $iconHeight)) -}}
{{- if not $hideLabel }}<span class="hx:group-data-[theme=dark]:hidden hx:group-data-[theme=light]:hidden">{{ $system }}</span>{{ end -}}
</div>
</button>
<ul
class="hextra-theme-toggle-options hx:hidden hx:z-20 hx:max-h-64 hx:overflow-auto hx:rounded-md hx:ring-1 hx:ring-black/5 hx:bg-white hx:py-1 hx:text-sm hx:shadow-lg hx:dark:ring-white/20 hx:dark:bg-neutral-800"
style="position: fixed; inset: auto auto 0px 0px; margin: 0px; min-width: 100px;"
data-theme="light"
>
<li class="hx:flex hx:flex-col">
<p
data-item="light"
class="hx:text-gray-800 hx:dark:text-gray-100 hx:hover:bg-primary-50 hx:hover:text-primary-600 hx:hover:dark:bg-primary-500/10 hx:hover:dark:text-primary-600 hx:relative hx:cursor-pointer hx:whitespace-nowrap hx:py-1.5 hx:transition-colors hx:ltr:pl-3 hx:ltr:pr-9 hx:rtl:pr-3 hx:rtl:pl-9"
>
{{ $light }}
<span class="hx:absolute hx:inset-y-0 hx:flex hx:items-center hx:ltr:right-3 hx:rtl:left-3 hx:group-data-[theme=dark]:hidden hx:group-data-[theme=system]:hidden">
{{- partial "utils/icon" (dict "name" "check" "attributes" "height=1em width=1em") -}}
</span>
</p>
</li>
<li class="hx:flex hx:flex-col">
<p
data-item="dark"
class="hx:text-gray-800 hx:dark:text-gray-100 hx:hover:bg-primary-50 hx:hover:text-primary-600 hx:hover:dark:bg-primary-500/10 hx:hover:dark:text-primary-600 hx:relative hx:cursor-pointer hx:whitespace-nowrap hx:py-1.5 hx:transition-colors hx:ltr:pl-3 hx:ltr:pr-9 hx:rtl:pr-3 hx:rtl:pl-9"
>
{{ $dark }}
<span class="hx:absolute hx:inset-y-0 hx:flex hx:items-center hx:ltr:right-3 hx:rtl:left-3 hx:group-data-[theme=light]:hidden hx:group-data-[theme=system]:hidden">
{{- partial "utils/icon" (dict "name" "check" "attributes" "height=1em width=1em") -}}
</span>
</p>
</li>
<li class="hx:flex hx:flex-col">
<p
data-item="system"
class="hx:text-gray-800 hx:dark:text-gray-100 hx:hover:bg-primary-50 hx:hover:text-primary-600 hx:hover:dark:bg-primary-500/10 hx:hover:dark:text-primary-600 hx:relative hx:cursor-pointer hx:whitespace-nowrap hx:py-1.5 hx:transition-colors hx:ltr:pl-3 hx:ltr:pr-9 hx:rtl:pr-3 hx:rtl:pl-9"
>
{{ $system }}
<span class="hx:absolute hx:inset-y-0 hx:flex hx:items-center hx:ltr:right-3 hx:rtl:left-3 hx:group-data-[theme=dark]:hidden hx:group-data-[theme=light]:hidden">
{{- partial "utils/icon" (dict "name" "check" "attributes" "height=1em width=1em") -}}
</span>
</p>
</li>
</ul>
</div>

View File

@@ -0,0 +1,91 @@
{{/* Table of Contents */}}
{{/* TODO: toc bottom part should be able to hide */}}
{{- $toc := .Params.toc | default true -}}
{{- $onThisPage := (T "onThisPage") | default "On this page"}}
{{- $tags := (T "tags") | default "Tags"}}
{{- $editThisPage := (T "editThisPage") | default "Edit this page"}}
{{- $backToTop := (T "backToTop") | default "Scroll to top" -}}
<nav class="hextra-toc hx:order-last hx:hidden hx:w-64 hx:shrink-0 hx:xl:block hx:print:hidden hx:px-4" aria-label="table of contents">
{{- if $toc }}
<div class="hextra-scrollbar hx:sticky hx:top-16 hx:overflow-y-auto hx:pr-4 hx:pt-6 hx:text-sm [hyphens:auto] hx:max-h-[calc(100vh-var(--navbar-height)-env(safe-area-inset-bottom))] hx:ltr:-mr-4 hx:rtl:-ml-4">
{{- with .Fragments.Headings -}}
<p class="hx:mb-4 hx:font-semibold hx:tracking-tight">{{ $onThisPage }}</p>
{{- range . -}}
<ul>
{{- with .Headings -}}{{ template "toc-subheading" (dict "headings" . "level" 0) }}{{- end -}}
</ul>
{{- end -}}
{{- end -}}
{{- $borderClass := "hx:mt-8 hx:border-t hx:bg-white hx:pt-8 hx:shadow-[0_-12px_16px_white] hx:dark:bg-dark hx:dark:shadow-[0_-12px_16px_#111]" -}}
{{- if not .Fragments.Headings -}}
{{- $borderClass = "" -}}
{{- end -}}
{{/* TOC bottom part */}}
<div class="{{ $borderClass }} hx:sticky hx:bottom-0 hx:flex hx:flex-col hx:items-start hx:gap-2 hx:pb-8 hx:border-gray-200 hx:dark:border-neutral-800 hx:contrast-more:border-t hx:contrast-more:border-neutral-400 hx:contrast-more:shadow-none hx:contrast-more:dark:border-neutral-400">
{{- if and site.Params.toc.displayTags .Params.tags -}}
<div class="hx:flex hx:items-start hx:gap-x-2 hx:font-medium hx:text-xs">
<div class="hx:text-gray-500 hx:dark:text-gray-400 hx:contrast-more:text-gray-800 hx:contrast-more:dark:text-gray-50">{{ $tags }}</div>
<div class="hx:flex hx:flex-wrap hx:gap-y-1">
{{ partial "tags.html" (dict "context" .) }}
</div>
</div>
{{- end -}}
{{- if site.Params.editURL.enable -}}
{{- $editURL := site.Params.editURL.base | default "" -}}
{{- with .Params.editURL -}}
{{/* if `editURL` is set in the front matter */}}
{{- $editURL = . -}}
{{- else -}}
{{- with .File -}}
{{/* `.FileInfo.Meta.SourceRoot` is a Hugo internal field, e.g. `/path/to/repo/content/en/` */}}
{{- $sourceDir := replace (strings.TrimPrefix .FileInfo.Meta.BaseDir .FileInfo.Meta.SourceRoot) "\\" "/" -}}
{{- $sourceDir = strings.TrimPrefix "/content" $sourceDir -}}
{{- $path := replace .Path "\\" "/" -}}
{{- $editURL = urls.JoinPath $editURL $sourceDir $path -}}
{{- end -}}
{{- end -}}
<a class="hx:text-xs hx:font-medium hx:text-gray-500 hx:hover:text-gray-900 hx:dark:text-gray-400 hx:dark:hover:text-gray-100 hx:contrast-more:text-gray-800 hx:contrast-more:dark:text-gray-50" href="{{ $editURL }}" target="_blank" rel="noreferrer">{{ $editThisPage }}</a>
{{- end -}}
{{/* Scroll To Top */}}
<button aria-hidden="true" id="backToTop" onClick="scrollUp();" class="hx:cursor-pointer hx:transition-all hx:duration-75 hx:opacity-0 hx:text-xs hx:font-medium hx:text-gray-500 hx:hover:text-gray-900 hx:dark:text-gray-400 hx:dark:hover:text-gray-100 hx:contrast-more:text-gray-800 hx:contrast-more:dark:text-gray-50">
<span>
{{- $backToTop -}}
</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="hx:inline hx:ltr:ml-1 hx:rtl:mr-1 hx:h-3.5 hx:w-3.5 hx:rounded-full hx:border hx:border-gray-500 hx:hover:border-gray-900 hx:dark:border-gray-400 hx:dark:hover:border-gray-100 hx:contrast-more:border-gray-800 hx:contrast-more:dark:border-gray-50">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
</svg>
</button>
</div>
</div>
{{ end -}}
</nav>
{{/* TOC subheadings component. This is a recursive component that renders a list of headings. */}}
{{- define "toc-subheading" -}}
{{- $headings := .headings -}}
{{- $level := .level -}}
{{- if ge $level 6 -}}
{{ return }}
{{- end -}}
{{- $padding := (mul $level 4) -}}
{{- $class := cond (eq $level 0) "hx:font-medium" (printf "hx:ltr:pl-%d hx:rtl:pr-%d" $padding $padding) -}}
{{- range $headings }}
{{- if .Title }}
<li class="hx:my-2 hx:scroll-my-6 hx:scroll-py-6">
<a class="{{ $class }} hx:inline-block hx:text-gray-500 hx:hover:text-gray-900 hx:dark:text-gray-400 hx:dark:hover:text-gray-300 hx:contrast-more:text-gray-900 hx:contrast-more:underline hx:contrast-more:dark:text-gray-50 hx:w-full hx:break-words" href="#{{ anchorize .ID }}">
{{- .Title | safeHTML | plainify | htmlUnescape }}
</a>
</li>
{{- end -}}
{{- with .Headings -}}
{{ template "toc-subheading" (dict "headings" . "level" (add $level 1)) }}
{{- end -}}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,31 @@
{{- /*
Extracts all headings from a page and adds them to the scratchpad.
The keys can be obtained from the scratchpad by using the "keys" key.
The titles can be obtained from the scratchpad by using the "titles" key.
The scratchpad must be initialized with empty slices before calling this function for the keys "keys" and "titles"
@param {any} target The element to extract headings from.
@param {any} scratch The scratchpad to add the keys and titles to.
@example {{ partial "utils/extract-headings.html" (dict "target" $h1 "scratch" $s) }}
*/ -}}
{{- range $heading := index .target.Headings -}}
{{- if and (eq $heading.Level 0) (not $heading.Title) -}}
{{- $.scratch.Add "keys" (slice $heading.Title) -}}
{{- else -}}
{{- $key := (printf "%s#%s" $heading.ID $heading.Title) -}}
{{- $.scratch.Add "keys" (slice $key) -}}
{{- end -}}
{{- $title := (printf "<h%d>%s" $heading.Level $heading.Title) | htmlUnescape -}}
{{- $.scratch.Add "titles" (slice $title) -}}
{{- partial "utils/extract-headings.html" (dict
"target" $heading
"scratch" $.scratch
)
}}
{{- end -}}

View File

@@ -0,0 +1,21 @@
{{/* This utility is used to get the file path from absolute, relative path or URL. */}}
{{- $path := .path -}}
{{- $page := .page -}}
{{- $isLocal := not (urls.Parse $path).Scheme -}}
{{- $isPage := and (eq $page.Kind "page") (not $page.BundleType) -}}
{{- $startsWithSlash := hasPrefix $path "/" -}}
{{- $startsWithRelative := hasPrefix $path "../" -}}
{{- if and $path $isLocal -}}
{{- if $startsWithSlash -}}
{{/* File under static directory */}}
{{- $path = (relURL (strings.TrimPrefix "/" $path)) -}}
{{- else if and $isPage (not $startsWithRelative) -}}
{{/* File is a sibling to the individual page file */}}
{{ $path = (printf "../%s" $path) }}
{{- end -}}
{{- end -}}
{{- return $path -}}

View File

@@ -0,0 +1,3 @@
{{- with . -}}
{{- . | time.Format (site.Params.dateFormat | default ":date_long") -}}
{{- end -}}

View File

@@ -0,0 +1,93 @@
{{- /*
fragments.html - Split page content into searchable fragments
This partial processes a Hugo page and splits its content into fragments based on headings,
creating a data structure suitable for search indexing. It supports different fragment types
and handles hierarchical heading structures (h1, h2).
Parameters:
- .context (Page): The Hugo page to process
- .type (string): Fragment type - "content" (default), "heading", "title", or "summary"
Returns:
- dict: Map of heading keys to content fragments
Example:
Input page with content:
# Introduction
This is the intro text.
## Setup
Setup instructions here.
# Configuration
Config details here.
Output (type "content"):
{
"": "This is the intro text.",
"intro#Introduction": "This is the intro text. Setup instructions here.",
"setup#Setup": "Setup instructions here.",
"config#Configuration": "Config details here."
}
Fragment types:
- "content": Splits page content by headings (default)
- "heading": Returns heading keys with empty content
- "title": Returns empty content (title handled elsewhere)
- "summary": Returns page summary only
*/ -}}
{{- /* Extract page context and fragment type */ -}}
{{- $page := .context -}}
{{- $type := .type | default "content" -}}
{{- /* Process all headings */ -}}
{{- $s := newScratch -}}
{{- $s.Set "keys" slice -}}
{{- $s.Set "titles" slice -}}
{{- partial "utils/extract-headings.html" (dict "target" $page.Fragments "scratch" $s) -}}
{{- $headingKeys := $s.Get "keys" -}}
{{- $headingTitles := $s.Get "titles" -}}
{{- $content := $page.Content | htmlUnescape -}}
{{- $len := len $headingKeys -}}
{{- $data := dict -}}
{{ if eq $type "content" }}
{{/* Include full content of the page */}}
{{ if eq $len 0 }}
{{ $data = $data | merge (dict "" ($page.Plain | htmlUnescape | strings.TrimSpace)) }}
{{ else }}
{{/* Split the raw content from bottom to top */}}
{{ range seq $len }}
{{ $i := sub $len . }}
{{ $headingKey := index $headingKeys $i }}
{{ $headingTitle := index $headingTitles $i }}
{{ if eq $i 0 }}
{{ $data = $data | merge (dict $headingKey ($content | plainify | htmlUnescape | strings.TrimSpace)) }}
{{ else }}
{{ $parts := split $content (printf "%s" $headingTitle) }}
{{ $lastPart := index $parts (sub (len $parts) 1) }}
{{ $data = $data | merge (dict $headingKey ($lastPart | plainify | htmlUnescape | strings.TrimSpace)) }}
{{ $content = strings.TrimSuffix $lastPart $content }}
{{ $content = strings.TrimSuffix (printf "%s" $headingTitle) $content }}
{{ end }}
{{ end }}
{{ end }}
{{ else if (eq $type "heading" ) }}
{{/* Put heading keys with empty content to the data object */}}
{{ $data = dict "" "" }}
{{ range $headingKeys }}
{{ $data = $data | merge (dict . "") }}
{{ end }}
{{ else if (eq $type "title") }}
{{/* Use empty data object since title is included in search-data.json */}}
{{ $data = $data | merge (dict "" "") }}
{{ else if (eq $type "summary" ) }}
{{ $data = $data | merge (dict "" ($page.Summary | plainify | htmlUnescape | strings.TrimSpace)) }}
{{ end }}
{{ return $data }}

View File

@@ -0,0 +1,14 @@
{{/* Render raw svg icon from .Site.Data */}}
{{- $icon := index site.Data.icons .name -}}
{{- if not $icon -}}
{{ errorf "icon %q not found" .name }}
{{- end -}}
{{- $icon = $icon | safeHTML -}}
{{- if .attributes -}}
{{- $icon = replaceRE "<svg" (printf "<svg %s" .attributes) $icon -}}
{{- end -}}
{{- return ($icon | safeHTML) -}}

View File

@@ -0,0 +1,25 @@
{{/* Get relative link of a page for given language */}}
{{/* If not found, return the homepage of the language page */}}
{{ $page := .context }}
{{ $lang := .lang }}
{{ $link := false }}
{{ range $page.AllTranslations }}
{{ if eq .Language.Lang $lang }}
{{ $link = .RelPermalink }}
{{ end }}
{{ end }}
{{ if not $link }}
{{ range where $page.Sites ".Language.Lang" $lang }}
{{ $link = .Home.RelPermalink }}
{{ end }}
{{ end }}
{{ if not $link }}
{{ $link = site.Home.RelPermalink }}
{{ end }}
{{ return $link }}

View File

@@ -0,0 +1,11 @@
{{ with .Description | plainify | htmlUnescape -}}
{{ . -}}
{{ else -}}
{{ if .IsHome -}}
{{ with .Site.Params.description | plainify | htmlUnescape -}}
{{ . -}}
{{ end -}}
{{ else -}}
{{ .Summary | plainify | htmlUnescape | chomp -}}
{{ end -}}
{{ end -}}

View File

@@ -0,0 +1,7 @@
{{- with .Params.width -}}
<style>
:root {
--hextra-max-page-width: {{ cond (eq . "wide") "90rem" (cond (eq . "full") "100%" "80rem") }};
}
</style>
{{- end -}}

View File

@@ -0,0 +1,32 @@
{{- $page := .page -}}
{{- $by := .by | default "weight" -}}
{{- $order := .order | default "asc" -}}
{{- $pages := slice }}
{{- if eq $by "weight" }}
{{- $pages = $page.Pages.ByWeight }}
{{- else if eq $by "date" }}
{{- $pages = $page.Pages.ByDate }}
{{- else if eq $by "title" }}
{{- $pages = $page.Pages.ByTitle }}
{{- else if eq $by "expiryDate" }}
{{- $pages = $page.Pages.ByExpiryDate }}
{{- else if eq $by "publishDate" }}
{{- $pages = $page.Pages.ByPublishDate }}
{{- else if eq $by "lastmod" }}
{{- $pages = $page.Pages.ByLastmod }}
{{- else if eq $by "linkTitle" }}
{{- $pages = $page.Pages.ByLinkTitle }}
{{- else if eq $by "length" }}
{{- $pages = $page.Pages.ByLength }}
{{- else }}
{{- warnf "sort-pages: unknown sort field %q" $by -}}
{{- $pages = $page.Pages }}
{{ end -}}
{{- if eq $order "desc" }}
{{- $pages = $pages.Reverse }}
{{- end -}}
{{- return $pages -}}

View File

@@ -0,0 +1,19 @@
{{/*
This utility is used to retrieve the title of a page or section.
If no title is set, it falls back to using the directory or file name.
Based on https://github.com/thegeeklab/hugo-geekdoc/blob/v0.44.0/layouts/partials/utils/title.html
*/}}
{{- $title := "" }}
{{ if .LinkTitle }}
{{ $title = .LinkTitle }}
{{ else if .Title }}
{{ $title = .Title }}
{{ else if and .IsSection .File }}
{{ $title = path.Base .File.Dir | humanize | title }}
{{ else if and .IsPage .File }}
{{ $title = .File.BaseFileName | humanize | title }}
{{ end }}
{{ return $title -}}