count,
})).sort((a, b) => a.displayName.localeCompare(b.displayName));
-// ── About section: static tree matching Hugo content/about/ structure ──────────
-// Sorted by weight (Hugo default), then by title.
-// isNode = true means the page is a section index (has children in Hugo).
+// ── Shared nav helpers ────────────────────────────────────────────────────────
+
+// Generic tree traversal — works for both NavPage and ProjectNavNode
+function inSubtree<T extends { url: string; children?: T[] }>(node: T, url: string): boolean {
+ if (node.url === url) return true;
+ return (node.children ?? []).some(c => inSubtree(c, url));
+}
+
+// A nav item is visible (not off) when it is: current, ancestor of current,
+// sibling of current (only when current is not a section-index node), or child of current.
+// `inPath` = inSubtree(node, currentUrl), pre-computed by the caller to avoid double traversal.
+function nodeOff(
+ nodeUrl: string,
+ parentUrl: string,
+ inPath: boolean,
+ currentParentUrl: string,
+ currentIsNode: boolean,
+): boolean {
+ const isCurrent = nodeUrl === currentUrl;
+ const isAncestor = inPath && !isCurrent;
+ const isSibling = parentUrl === currentParentUrl && !currentIsNode;
+ const isChild = parentUrl === currentUrl;
+ return !(isCurrent || isAncestor || isSibling || isChild);
+}
+
+// ── About section: static tree matching content/about/ structure ──────────────
+// isNode = true means the page is a section index (has children).
interface NavPage { url: string; title: string; isNode?: boolean; children?: NavPage[] }
const aboutSection: NavPage = {
url: '/about.html', title: 'About', isNode: true,
children: [
- { url: '/contact.html', title: 'Contact' }, // weight 10
- { // weight 10 (alpha: C < I)
+ { url: '/contact.html', title: 'Contact' }, // weight 10
+ { // weight 10 (alpha: C < I)
url: '/impressum.html', title: 'Impressum', isNode: true,
children: [
- { url: '/urheberrechte.html', title: 'Urheberrecht' }, // weight 10
- { url: '/datenschutz.html', title: 'Datenschutz' }, // weight 20
- { url: '/haftung-inhalte.html', title: 'Haftung für Inhalte' }, // weight 30
- { url: '/haftung-links.html', title: 'Haftung für Links' }, // weight 40
- { url: '/google-analytics.html', title: 'Google Analytics' }, // no weight → last
+ { url: '/urheberrechte.html', title: 'Urheberrecht' }, // weight 10
+ { url: '/datenschutz.html', title: 'Datenschutz' }, // weight 20
+ { url: '/haftung-inhalte.html', title: 'Haftung für Inhalte' }, // weight 30
+ { url: '/haftung-links.html', title: 'Haftung für Links' }, // weight 40
+ { url: '/google-analytics.html', title: 'Google Analytics' }, // no weight → last
]
},
- { url: '/agb.html', title: 'AGB' }, // weight 30
+ { url: '/agb.html', title: 'AGB' }, // weight 30
]
};
-// True if `url` is anywhere inside `node`'s subtree (inclusive)
-function inSubtree(node: NavPage, url: string): boolean {
- if (node.url === url) return true;
- return (node.children ?? []).some(c => inSubtree(c, url));
+// Walk the About tree to find currentUrl — returns parent URL + isNode flag.
+// Replaces the separate aboutParentOf Map and aboutSectionIndexUrls Set.
+function findInTree(node: NavPage, url: string, parentUrl = ''): { parentUrl: string; isNode: boolean } | null {
+ if (node.url === url) return { parentUrl, isNode: node.isNode ?? false };
+ for (const c of node.children ?? []) {
+ const r = findInTree(c, url, node.url);
+ if (r) return r;
+ }
+ return null;
}
-const onAboutSection = inSubtree(aboutSection, currentUrl);
-
-// Parent URL of each About-section page (needed for sibling/child off-logic)
-const aboutParentOf = new Map<string, string>([
- ['/about.html', ''],
- ['/contact.html', '/about.html'],
- ['/impressum.html', '/about.html'],
- ['/agb.html', '/about.html'],
- ['/urheberrechte.html', '/impressum.html'],
- ['/datenschutz.html', '/impressum.html'],
- ['/haftung-inhalte.html', '/impressum.html'],
- ['/haftung-links.html', '/impressum.html'],
- ['/google-analytics.html','/impressum.html'],
-]);
+const _currentAbout = findInTree(aboutSection, currentUrl);
+const onAboutSection = _currentAbout !== null;
+const currentAboutParent = _currentAbout?.parentUrl ?? '';
+const currentAboutIsNode = _currentAbout?.isNode ?? false;
-// Section-index pages within About (= have children in Hugo)
-const aboutSectionIndexUrls = new Set(['/about.html', '/impressum.html']);
-const currentAboutIsNode = aboutSectionIndexUrls.has(currentUrl);
-const currentAboutParent = aboutParentOf.get(currentUrl) ?? '';
-
-// off-logic mirroring Hugo tree.html for a page inside the About subtree.
-// A page is visible (not off) if it is current, ancestor-of-current,
-// sibling-of-current (only when current is not a section-index), or child-of-current.
-function aboutPageOff(page: NavPage, parentUrl: string): boolean {
- const isCurrent = page.url === currentUrl;
- const isAncestor = !isCurrent && inSubtree(page, currentUrl);
- const isSibling = parentUrl === currentAboutParent && !currentAboutIsNode;
- const isChild = parentUrl === currentUrl;
- return !(isCurrent || isAncestor || isSibling || isChild);
-}
+// Partial application of nodeOff bound to the About subtree's current-page context
+const aboutNodeOff = (nodeUrl: string, parentUrl: string, inPath: boolean) =>
+ nodeOff(nodeUrl, parentUrl, inPath, currentAboutParent, currentAboutIsNode);
// ── Projects section ──────────────────────────────────────────────────────────
-const activeProject = await getActiveProject(currentUrl);
-const onProjectsSection = currentUrl === '/projects/' || activeProject !== null;
+const activeProject = await getActiveProject(currentUrl);
+const onProjectsSection = currentUrl === '/projects/' || activeProject !== null;
// When on a SNAPSHOT page, render that version's tree; otherwise all visible trees.
const projectTrees = onProjectsSection && activeProject && !activeProject.isCurrent
? [activeProject]
: await getVisibleProjectNavTrees();
-// Nav helpers for project trees
-function inProjectSubtree(node: ProjectNavNode, url: string): boolean {
- if (node.url === url) return true;
- return node.children.some(c => inProjectSubtree(c, url));
-}
-
-// Find the parent node of a URL within a tree
-function findProjectParent(node: ProjectNavNode, url: string): ProjectNavNode | null {
- for (const c of node.children) {
- if (c.url === url) return node;
- const found = findProjectParent(c, url);
- if (found) return found;
- }
- return null;
-}
-
-// Find a node by URL
-function findProjectNode(node: ProjectNavNode, url: string): ProjectNavNode | null {
- if (node.url === url) return node;
+// Single DFS pass: finds both the current node and its parent in a project tree
+function findNodeAndParent(
+ node: ProjectNavNode,
+ url: string,
+ parent: ProjectNavNode | null = null,
+): { node: ProjectNavNode; parent: ProjectNavNode | null } | null {
+ if (node.url === url) return { node, parent };
for (const c of node.children) {
- const found = findProjectNode(c, url);
- if (found) return found;
+ const r = findNodeAndParent(c, url, node);
+ if (r) return r;
}
return null;
}
-// Determine the "active" project URL — either from visible page or SNAPSHOT page
-const activeProjectUrl = activeProject?.navRoot.url ?? null;
-// The active project's current node and its parent (for off-logic)
-const _activeProjectRoot = activeProject?.navRoot ?? null;
-const activeProjectCurrentNode = _activeProjectRoot ? findProjectNode(_activeProjectRoot, currentUrl) : null;
-const activeProjectCurrentParent = _activeProjectRoot ? findProjectParent(_activeProjectRoot, currentUrl) : null;
+const _found = activeProject?.navRoot ? findNodeAndParent(activeProject.navRoot, currentUrl) : null;
+const activeProjectCurrentNode = _found?.node ?? null;
+const activeProjectCurrentParent = _found?.parent ?? null;
const activeProjectCurrentIsNode = activeProjectCurrentNode?.isNode ?? false;
-// off-logic mirroring tree.html for a node within a project tree
-function projectNodeOff(node: ProjectNavNode, parentUrl: string): boolean {
- const isCurrent = node.url === currentUrl;
- const isAncestor = !isCurrent && inProjectSubtree(node, currentUrl);
- // Sibling: same parent as current page, and current is NOT a section node
- const isSibling = parentUrl === (activeProjectCurrentParent?.url ?? '') && !activeProjectCurrentIsNode;
- // Child: this node's parent is the current page
- const isChild = parentUrl === currentUrl;
- return !(isCurrent || isAncestor || isSibling || isChild);
-}
+// Partial application of nodeOff bound to the Projects subtree's current-page context
+const _projCurrentParentUrl = activeProjectCurrentParent?.url ?? '';
+const projNodeOff = (nodeUrl: string, parentUrl: string, inPath: boolean) =>
+ nodeOff(nodeUrl, parentUrl, inPath, _projCurrentParentUrl, activeProjectCurrentIsNode);
// ── Blog URL classification ────────────────────────────────────────────────────
const mainSections = [
</a>
<ul class={`s${onProjectsSection ? ' active' : ''}`}>
{projectTrees.map(info => {
- const root = info.navRoot;
- const rootOff = projectNodeOff(root, '/projects/');
- const rootInPath = inProjectSubtree(root, currentUrl);
+ const root = info.navRoot;
+ const rootInPath = inSubtree(root, currentUrl);
+ const rootOff = projNodeOff(root.url, '/projects/', rootInPath);
return (
<li class={`s sub${rootOff ? ' off' : ''}`}>
<a href={root.url} class={rootInPath ? 's selected' : 's'}>
</a>
<ul class={`s${rootInPath ? ' active' : ''}`}>
{root.children.map(child => {
- const childOff = projectNodeOff(child, root.url);
- const childInPath = inProjectSubtree(child, currentUrl);
- const childIsNode = child.isNode;
+ const childInPath = inSubtree(child, currentUrl);
+ const childOff = projNodeOff(child.url, root.url, childInPath);
+ const childIsNode = child.isNode;
return (
<li class={`s${childIsNode ? ' sub' : ''}${childOff ? ' off' : ''}`}>
<a href={child.url} class={childInPath ? 's selected' : 's'}>
{childIsNode && child.children.length > 0 && (
<ul class={`s${childInPath ? ' active' : ''}`}>
{child.children.map(grand => {
- const grandOff = projectNodeOff(grand, child.url);
- const grandInPath = inProjectSubtree(grand, currentUrl);
+ const grandInPath = inSubtree(grand, currentUrl);
+ const grandOff = projNodeOff(grand.url, child.url, grandInPath);
const grandIsNode = grand.isNode;
return (
<li class={`s${grandIsNode ? ' sub' : ''}${grandOff ? ' off' : ''}`}>
{grandIsNode && grand.children.length > 0 && (
<ul class={`s${grandInPath ? ' active' : ''}`}>
{grand.children.map(ggrand => {
- const ggOff = projectNodeOff(ggrand, grand.url);
- const ggActive = ggrand.url === currentUrl;
+ const ggInPath = ggrand.url === currentUrl;
+ const ggOff = projNodeOff(ggrand.url, grand.url, ggInPath);
return (
<li class={`s${ggOff ? ' off' : ''}`}>
- <a href={ggrand.url} class={ggActive ? 's selected' : 's'}>
- {ggActive ? <strong>{ggrand.title}</strong> : ggrand.title}
+ <a href={ggrand.url} class={ggInPath ? 's selected' : 's'}>
+ {ggInPath ? <strong>{ggrand.title}</strong> : ggrand.title}
</a>
</li>
);
</a>
<ul class={`s${onAboutSection ? ' active' : ''}`}>
{aboutSection.children!.map(child => {
- const childOff = aboutPageOff(child, '/about.html');
- const childInPath = inSubtree(child, currentUrl);
- const childHasSubs = (child.children ?? []).length > 0;
+ const childInPath = inSubtree(child, currentUrl);
+ const childOff = aboutNodeOff(child.url, '/about.html', childInPath);
+ const childHasSubs = (child.children ?? []).length > 0;
return (
<li class={`s${childHasSubs ? ' sub' : ''}${childOff ? ' off' : ''}`}>
<a href={child.url} class={childInPath ? 's selected' : 's'}>
{childHasSubs && (
<ul class={`s${childInPath ? ' active' : ''}`}>
{child.children!.map(grand => {
- const grandOff = aboutPageOff(grand, child.url);
const grandActive = grand.url === currentUrl;
+ const grandOff = aboutNodeOff(grand.url, child.url, grandActive);
return (
<li class={`s${grandOff ? ' off' : ''}`}>
<a href={grand.url} class={grandActive ? 's selected' : 's'}>