This commit is contained in:
2025-12-02 07:53:20 +01:00
parent 4096b52244
commit e3aaa1b0f8
14 changed files with 461 additions and 360 deletions

View File

@@ -25,6 +25,7 @@ pub fn new(args FactoryArgs) !&Site {
config: SiteConfig{
name: name
}
root_category: Category{}
}
sites_global[name] = &site
return get(name: name)!

View File

@@ -1,8 +1,111 @@
module meta
@[heap]
struct Category {
pub mut:
path string // e.g. Operations/Daily (means 2 levels deep, first level is Operations)
collapsible bool = true
collapsed bool
items []CategoryItem
}
//return the label of the category (last part of the path)
pub fn (mut c Category) label() !string {
if c.path.count('/') == 0 {
return c.path
}
return c.path.all_after_last('/')
}
type CategoryItem = Page | Link | Category
pub fn (mut self Category) up(path string) !&Category {
// Split the requested path into parts
path_parts := path.split('/')
// Start at current category
mut current := &self
// Navigate through each part of the path
for part in path_parts {
// Skip empty parts (from leading/trailing slashes)
if part.len == 0 {
continue
}
// Check if this part already exists in current category's items
mut found := false
for item in current.items {
match item {
&Category {
item_label := item.label()!
if item_label == part {
current = item
found = true
break
}
}
else {}
}
}
// If not found, create a new category and add it
if !found {
mut new_category := Category{
path: part
collapsible: true
collapsed: true
items: []CategoryItem{}
}
current.items << new_category
current = &new_category
}
}
return current
}
fn (mut self Category) page_get(src string)! &Page {
for item in self.items {
match item {
Page {
if item.src == src {
return it
}
}
else {}
}
}
return error('Page with src="${src}" not found in site.')
}
fn (mut self Category) link_get(href string)! &Link {
for item in self.items {
match item {
Link {
if item.href == href {
return it
}
}
else {}
}
}
return error('Link with href="${href}" not found in site.')
}
fn (mut self Category) category_get(path string)! &Category {
for item in self.items {
match item {
Category {
if item.path == path {
return it
}
}
else {}
}
}
return error('Category with path="${path}" not found in site.')
}

View File

@@ -0,0 +1,76 @@
module meta
pub fn (mut self Category) str() string {
mut result := []string{}
if self.items.len == 0 {
return 'Sidebar is empty\n'
}
result << '📑 SIDEBAR STRUCTURE'
result << '━'.repeat(60)
for i, item in self.items {
is_last := i == self.items.len - 1
prefix := if is_last { ' ' } else { ' ' }
match item {
NavDoc {
result << '${prefix}📄 ${item.label}'
result << ' path: ${item.path}'
}
NavCat {
// Category header
collapse_icon := if item.collapsed { ' ' } else { ' ' }
result << '${prefix}${collapse_icon}📁 ${item.label}'
// Category metadata
if !item.collapsed {
result << ' collapsible: ${item.collapsible}'
result << ' items: ${item.items.len}'
// Sub-items
for j, sub_item in item.items {
is_last_sub := j == item.items.len - 1
sub_prefix := if is_last_sub { ' ' } else { ' ' }
match sub_item {
NavDoc {
result << '${sub_prefix}📄 ${sub_item.label} [${sub_item.src_path}]'
}
NavCat {
// Nested categories
sub_collapse_icon := if sub_item.collapsed { ' ' } else { ' ' }
result << '${sub_prefix}${sub_collapse_icon}📁 ${sub_item.label}'
}
NavLink {
result << '${sub_prefix}🔗 ${sub_item.label}'
if sub_item.description.len > 0 {
result << ' ${sub_item.description}'
}
}
}
}
}
}
NavLink {
result << '${prefix}🔗 ${item.label}'
result << ' href: ${item.href}'
if item.description.len > 0 {
result << ' desc: ${item.description}'
}
}
}
// Add spacing between root items
if i < self.items.len - 1 {
result << ''
}
}
result << '━'.repeat(60)
result << '📊 SUMMARY'
result << ' Total items: ${self.items.len}'
return result.join('\n') + '\n'
}

View File

@@ -11,4 +11,5 @@ pub mut:
hide_title bool // Should the title be hidden on the page?
hide bool // Should the page be hidden from navigation?
category_id int // Optional category ID this page belongs to, if 0 it means its at root level
nav_path string // navigation path e.g. "Operations/Daily"
}

View File

@@ -1,34 +0,0 @@
module meta
// ============================================================================
// Sidebar Navigation Models (Domain Types)
// is the result of walking through the pages, links and categories to build the sidebar structure
// ============================================================================
pub struct SideBar {
pub mut:
my_sidebar []NavItem
}
pub type NavItem = NavDoc | NavCat | NavLink
pub struct NavDoc {
pub:
path string // path is $collection/$name without .md, this is a subdir of the doctree export dir
label string
}
pub struct NavCat {
pub mut:
label string
collapsible bool = true
collapsed bool
items []NavItem
}
pub struct NavLink {
pub:
label string
href string
description string
}

View File

@@ -5,316 +5,28 @@ pub struct Site {
pub mut:
doctree_path string // path to the export of the doctree site
config SiteConfig // Full site configuration
pages []Page
links []Link
categories []Category
root // The root category containing all top-level items
announcements []Announcement // there can be more than 1 announcement
imports []ImportItem
build_dest []BuildDest // Production build destinations (from !!site.build_dest)
build_dest_dev []BuildDest // Development build destinations (from !!site.build_dest_dev)
}
pub fn (mut s Site) sidebar() SideBar {
mut result := SideBar{
my_sidebar: []NavItem{}
}
// If no pages, return empty sidebar
if s.pages.len == 0 {
return result
}
// Build a map of category_id -> pages for efficient lookup
mut category_pages := map[int][]Page{}
mut uncategorized_pages := []Page{}
// Group pages by category
eprintln('DEBUG: Grouping ${s.pages.len} pages into categories')
for page in s.pages {
if page.category_id == 0 {
// Page at root level (no category)
uncategorized_pages << page
eprintln(' Page "${page.src}": UNCATEGORIZED')
} else {
// Page belongs to a category
if page.category_id !in category_pages {
category_pages[page.category_id] = []Page{}
}
category_pages[page.category_id] << page
if page.category_id < s.categories.len {
eprintln(' Page "${page.src}": category_id=${page.category_id} -> "${s.categories[page.category_id].path}"')
} else {
eprintln(' Page "${page.src}": category_id=${page.category_id} -> INVALID INDEX!')
}
}
}
eprintln('DEBUG: Grouped into ${category_pages.len} categories + ${uncategorized_pages.len} uncategorized')
// Sort pages within each category by their order in the pages array
for category_id in category_pages.keys() {
category_pages[category_id].sort(a.src < b.src)
}
// Sort uncategorized pages
uncategorized_pages.sort(a.src < b.src)
// ============================================================
// Build nested category structure from path
// ============================================================
mut category_tree := map[string]&NavCat{}
mut parent_map := map[string]string{} // Map of path -> parent_path
// PASS 1: Create ALL category nodes first
// Collect all paths first, then sort by depth (shallow first)
mut all_paths := []string{}
for i, category in s.categories {
path_parts := if category.path.contains('/') {
category.path.split('/')
} else {
[category.path]
}
mut current_path := ''
for part_idx, part in path_parts {
if current_path.len > 0 {
current_path += '/'
}
current_path += part
// Add this path if not already added
if current_path !in category_tree {
all_paths << current_path
}
}
}
// Sort paths by depth (number of '/') so we create parents before children
all_paths.sort(a.count('/') < b.count('/'))
// Now create all nodes in order of depth
for path in all_paths {
if path !in category_tree {
path_parts := path.split('/')
part := path_parts[path_parts.len - 1]
// Find the category with this path to get collapsible/collapsed settings
mut collapsible := true
mut collapsed := false
for category in s.categories {
if category.path == path {
collapsible = category.collapsible
collapsed = category.collapsed
break
}
}
// Create new category node
mut new_cat := &NavCat{
label: part
collapsible: collapsible
collapsed: collapsed
items: []NavItem{}
}
category_tree[path] = new_cat
// Record parent for later linking
if path.contains('/') {
last_slash := path.last_index('/') or { 0 }
parent_path := path[0..last_slash]
parent_map[path] = parent_path
}
}
}
// PASS 2: Link all parent-child relationships
// Process these in order of depth to ensure parents are linked first
mut sorted_paths := parent_map.keys()
sorted_paths.sort(a.count('/') < b.count('/'))
for path in sorted_paths {
parent_path := parent_map[path]
if parent_path in category_tree && path in category_tree {
mut parent_cat := category_tree[parent_path]
child_cat := category_tree[path]
// Only add if not already added
mut already_added := false
for item in parent_cat.items {
if item is NavCat && item.label == child_cat.label {
already_added = true
break
}
}
if !already_added {
parent_cat.items << child_cat
}
}
}
// PASS 3: Add pages to their designated categories
eprintln('DEBUG PASS 3: Adding pages to categories')
for i, category in s.categories {
category_id := i // categories are 0-indexed in the page assignment
// Skip if no pages in this category
if category_id !in category_pages {
eprintln(' Category ${category_id} ("${category.path}"): no pages')
continue
}
// Build the full path for this category
full_path := category.path
eprintln(' Category ${category_id} ("${full_path}"): ${category_pages[category_id].len} pages')
// Add pages to this category
if full_path in category_tree {
mut leaf_cat := category_tree[full_path]
for page in category_pages[category_id] {
if !page.hide {
// Convert page src format "collection:name" to path "collection/name"
path := page.src.replace(':', '/')
eprintln(' Adding page: ${page.src} -> ${path}')
nav_doc := NavDoc{
path: path
label: if page.label.len > 0 { page.label } else { page.title }
}
leaf_cat.items << nav_doc
}
}
} else {
eprintln(' ERROR: Category path "${full_path}" not in category_tree!')
}
}
// ============================================================
// PASS 4: Add root-level categories to sidebar
// ============================================================
// Find all root-level categories (those without '/') and add them once
mut added_roots := map[string]bool{}
for i, category in s.categories {
// Only process top-level categories
if !category.path.contains('/') && category.path.len > 0 {
root_path := category.path
// Only add each root once
if root_path !in added_roots {
if root_path in category_tree {
result.my_sidebar << category_tree[root_path]
added_roots[root_path] = true
}
}
}
}
// ============================================================
// PASS 5: Add uncategorized pages at root level
// ============================================================
for page in uncategorized_pages {
if !page.hide {
// Convert page src format "collection:name" to path "collection/name"
path := page.src.replace(':', '/')
nav_doc := NavDoc{
path: path
label: if page.label.len > 0 { page.label } else { page.title }
}
result.my_sidebar << nav_doc
}
}
// ============================================================
// PASS 6: Add standalone links (if needed)
// ============================================================
for link in s.links {
nav_link := NavLink{
label: link.label
href: link.href
description: link.description
}
result.my_sidebar << nav_link
}
return result
pub fn (mut self Site) page_get(src string)! &Page {
return self.root.page_get(src)!
}
pub fn (mut self Site) link_get(href string)! &Link {
return self.root.link_get(href)!
}
pub fn (mut s Site) sidebar_str() string {
mut result := []string{}
mut sidebar := s.sidebar()
if sidebar.my_sidebar.len == 0 {
return 'Sidebar is empty\n'
}
result << '📑 SIDEBAR STRUCTURE'
result << '━'.repeat(60)
for i, item in sidebar.my_sidebar {
is_last := i == sidebar.my_sidebar.len - 1
prefix := if is_last { ' ' } else { ' ' }
match item {
NavDoc {
result << '${prefix}📄 ${item.label}'
result << ' path: ${item.path}'
}
NavCat {
// Category header
collapse_icon := if item.collapsed { ' ' } else { ' ' }
result << '${prefix}${collapse_icon}📁 ${item.label}'
// Category metadata
if !item.collapsed {
result << ' collapsible: ${item.collapsible}'
result << ' items: ${item.items.len}'
// Sub-items
for j, sub_item in item.items {
is_last_sub := j == item.items.len - 1
sub_prefix := if is_last_sub { ' ' } else { ' ' }
match sub_item {
NavDoc {
result << '${sub_prefix}📄 ${sub_item.label} [${sub_item.path}]'
}
NavCat {
// Nested categories
sub_collapse_icon := if sub_item.collapsed { ' ' } else { ' ' }
result << '${sub_prefix}${sub_collapse_icon}📁 ${sub_item.label}'
}
NavLink {
result << '${sub_prefix}🔗 ${sub_item.label}'
if sub_item.description.len > 0 {
result << ' ${sub_item.description}'
}
}
}
}
}
}
NavLink {
result << '${prefix}🔗 ${item.label}'
result << ' href: ${item.href}'
if item.description.len > 0 {
result << ' desc: ${item.description}'
}
}
}
// Add spacing between root items
if i < sidebar.my_sidebar.len - 1 {
result << ''
}
}
result << '━'.repeat(60)
result << '📊 SUMMARY'
result << ' Total items: ${sidebar.my_sidebar.len}'
result << ' Pages: ${s.pages.len}'
result << ' Categories: ${s.categories.len}'
result << ' Links: ${s.links.len}'
return result.join('\n') + '\n'
pub fn (mut self Site) category_get(path string)! &Category {
return self.root.category_get(path)!
}
//sidebar returns the root category for building the sidebar navigation
pub fn (mut self Site) sidebar()! &Category {
return self.root
}

View File

@@ -9,7 +9,8 @@ import incubaid.herolib.ui.console
// ============================================================
fn play_pages(mut plbook PlayBook, mut website Site) ! {
mut collection_current := ''
mut category_current := 0
mut category_current := &website.root_category // start at root category, this is basically the navigation tree root
// ============================================================
// PASS 1: Process all page and category actions
@@ -26,16 +27,18 @@ fn play_pages(mut plbook PlayBook, mut website Site) ! {
mut p := action.params
// label is empty when not specified (we support label & path for flexibility)
mut category_path := p.get_default('path', '')!
category_current = category_current.up(category_path)!
category_current.collapsible = p.get_default_true('collapsible')
category_current.collapsed = p.get_default_true('collapsed')
mut category := Category{
path: p.get_default('path', p.get_default('label', '')!)!
collapsible: p.get_default_true('collapsible')
collapsed: p.get_default_true('collapsed')
}
website.categories << category
category_current = website.categories.len - 1
console.print_item('Created page category: "${category.path}" ')
console.print_item('Created page category: "${category_current.path}" ')
action.done = true
println(category_current)
website.categories << category_current
$dbg();
continue
}
@@ -76,6 +79,8 @@ fn play_pages(mut plbook PlayBook, mut website Site) ! {
page_title := p.get_default('title', '')! // is title shown on the page, if not from the page content, if empty then will be brought in from the content
page_description := p.get_default('description', '')!
// Create page
mut page := Page{
src: '${page_collection}:${page_name}'

View File

@@ -127,7 +127,7 @@ pub mut:
}
// Generate sidebar navigation
sidebar := mysite.sidebar() // Returns SideBar
sidebar := mysite.sidebar()! // Returns SideBar
// Sidebar structure
pub struct SideBar {
@@ -159,7 +159,7 @@ pub:
}
// Example: iterate navigation
sidebar := mysite.sidebar()
sidebar := mysite.sidebar()!
for item in sidebar.my_sidebar {
match item {
NavDoc {

View File

@@ -560,7 +560,7 @@ pub fn test_navigation_depth() ! {
// ========================================================
console.print_header('TEST 3: Navigation Structure Analysis')
mut sidebar := nav_site.sidebar()
mut sidebar := nav_site.sidebar()!
console.print_item('Sidebar root items: ${sidebar.my_sidebar.len}')
console.lf()

View File

@@ -403,7 +403,7 @@ fn test_site2() ! {
// ========================================================
console.print_header('Validating Navigation Structure (Sidebar)')
mut sidebar := test_site.sidebar()
mut sidebar := test_site.sidebar()!
console.print_item('Sidebar has ${sidebar.my_sidebar.len} root items')
assert sidebar.my_sidebar.len > 0, 'Sidebar should not be empty'