diff --git a/lib/web/docusaurus/dsite.v b/lib/web/docusaurus/dsite.v index fb8dcf4f..80f48226 100644 --- a/lib/web/docusaurus/dsite.v +++ b/lib/web/docusaurus/dsite.v @@ -71,9 +71,9 @@ pub struct DevArgs { pub mut: host string = 'localhost' port int = 3000 - open bool = true // whether to open the browser automatically - watch_changes bool // whether to watch for changes in docs and rebuild automatically - skip_generate bool // whether to skip generation (useful when docs are pre-generated, e.g., from atlas) + open bool = true // whether to open the browser automatically + watch_changes bool = false // whether to watch for changes in docs and rebuild automatically + skip_generate bool = false // whether to skip generation (useful when docs are pre-generated, e.g., from atlas) } pub fn (mut s DocSite) open(args DevArgs) ! { diff --git a/lib/web/docusaurus/dsite_generate_docs.v b/lib/web/docusaurus/dsite_generate_docs.v index b5f20d25..3fb548d7 100644 --- a/lib/web/docusaurus/dsite_generate_docs.v +++ b/lib/web/docusaurus/dsite_generate_docs.v @@ -142,10 +142,6 @@ fn (mut generator SiteGenerator) page_generate(args_ Page) ! { pagefile.write(c)! - generator.client.copy_pages(collection_name, page_name, pagefile.path_dir()) or { - generator.error("Couldn't copy pages for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")! - return - } generator.client.copy_images(collection_name, page_name, pagefile.path_dir()) or { generator.error("Couldn't copy images for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")! return diff --git a/lib/web/docusaurus/interface_atlas_client.v b/lib/web/docusaurus/interface_atlas_client.v new file mode 100644 index 00000000..c687d884 --- /dev/null +++ b/lib/web/docusaurus/interface_atlas_client.v @@ -0,0 +1,30 @@ +module docusaurus + +pub interface IDocClient { +mut: + // Path methods - get absolute paths to resources + get_page_path(collection_name string, page_name string) !string + get_file_path(collection_name string, file_name string) !string + get_image_path(collection_name string, image_name string) !string + + // Existence checks - verify if resources exist + page_exists(collection_name string, page_name string) bool + file_exists(collection_name string, file_name string) bool + image_exists(collection_name string, image_name string) bool + + // Content retrieval + get_page_content(collection_name string, page_name string) !string + + // Listing methods - enumerate resources + list_collections() ![]string + list_pages(collection_name string) ![]string + list_files(collection_name string) ![]string + list_images(collection_name string) ![]string + list_pages_map() !map[string][]string + list_markdown() !string + + // Image operations + // get_page_paths(collection_name string, page_name string) !(string, []string) + copy_images(collection_name string, page_name string, destination_path string) ! + copy_files(collection_name string, page_name string, destination_path string) ! +} diff --git a/lib/web/docusaurus/play.v b/lib/web/docusaurus/play.v index 14b6f0f1..37609f9a 100644 --- a/lib/web/docusaurus/play.v +++ b/lib/web/docusaurus/play.v @@ -1,7 +1,6 @@ module docusaurus import incubaid.herolib.core.playbook { PlayBook } -import os pub fn play(mut plbook PlayBook) ! { if !plbook.exists(filter: 'docusaurus.') { @@ -18,7 +17,7 @@ pub fn play(mut plbook PlayBook) ! { reset: param_define.get_default_false('reset') template_update: param_define.get_default_false('template_update') install: param_define.get_default_false('install') - atlas_dir: param_define.get_default('atlas_dir', '${os.home_dir()}/hero/var/atlas_export')! + atlas_dir: param_define.get_default('atlas_dir', '/tmp/atlas_export')! use_atlas: param_define.get_default_false('use_atlas') )! diff --git a/lib/web/site/ai_instructions.md b/lib/web/site/ai_instructions.md new file mode 100644 index 00000000..8db0c377 --- /dev/null +++ b/lib/web/site/ai_instructions.md @@ -0,0 +1,536 @@ +# AI Instructions for Site Module HeroScript + +This document provides comprehensive instructions for AI agents working with the Site module's HeroScript format. + +## HeroScript Format Overview + +HeroScript is a declarative configuration language with the following characteristics: + +### Basic Syntax + +```heroscript +!!actor.action + param1: "value1" + param2: "value2" + multiline_param: " + This is a multiline value. + It can span multiple lines. + " + arg1 arg2 // Arguments without keys +``` + +**Key Rules:** +1. Actions start with `!!` followed by `actor.action` format +2. Parameters are indented and use `key: "value"` or `key: value` format +3. Values with spaces must be quoted +4. Multiline values are supported with quotes +5. Arguments without keys are space-separated +6. Comments start with `//` + +## Site Module Actions + +### 1. Site Configuration (`!!site.config`) + +**Purpose:** Define the main site configuration including title, description, and metadata. + +**Required Parameters:** +- `name`: Site identifier (will be normalized to snake_case) + +**Optional Parameters:** +- `title`: Site title (default: "Documentation Site") +- `description`: Site description +- `tagline`: Site tagline +- `favicon`: Path to favicon (default: "img/favicon.png") +- `image`: Default site image (default: "img/tf_graph.png") +- `copyright`: Copyright text +- `url`: Main site URL +- `base_url`: Base URL path (default: "/") +- `url_home`: Home page path + +**Example:** +```heroscript +!!site.config + name: "my_documentation" + title: "My Documentation Site" + description: "Comprehensive technical documentation" + tagline: "Learn everything you need" + url: "https://docs.example.com" + base_url: "/" +``` + +**AI Guidelines:** +- Always include `name` parameter +- Use descriptive titles and descriptions +- Ensure URLs are properly formatted with protocol + +### 2. Metadata Configuration (`!!site.config_meta`) + +**Purpose:** Override specific metadata for SEO purposes. + +**Optional Parameters:** +- `title`: SEO-specific title (overrides site.config title for meta tags) +- `image`: SEO-specific image (overrides site.config image for og:image) +- `description`: SEO-specific description + +**Example:** +```heroscript +!!site.config_meta + title: "My Docs - Complete Guide" + image: "img/social-preview.png" + description: "The ultimate guide to using our platform" +``` + +**AI Guidelines:** +- Use only when SEO metadata needs to differ from main config +- Keep titles concise for social media sharing +- Use high-quality images for social previews + +### 3. Navigation Bar (`!!site.navbar` or `!!site.menu`) + +**Purpose:** Configure the main navigation bar. + +**Optional Parameters:** +- `title`: Navigation title (defaults to site.config title) +- `logo_alt`: Logo alt text +- `logo_src`: Logo image path +- `logo_src_dark`: Dark mode logo path + +**Example:** +```heroscript +!!site.navbar + title: "My Site" + logo_alt: "My Site Logo" + logo_src: "img/logo.svg" + logo_src_dark: "img/logo-dark.svg" +``` + +**AI Guidelines:** +- Use `!!site.navbar` for modern syntax (preferred) +- `!!site.menu` is supported for backward compatibility +- Provide both light and dark logos when possible + +### 4. Navigation Items (`!!site.navbar_item` or `!!site.menu_item`) + +**Purpose:** Add items to the navigation bar. + +**Required Parameters (one of):** +- `to`: Internal link path +- `href`: External URL + +**Optional Parameters:** +- `label`: Display text (required in practice) +- `position`: "left" or "right" (default: "right") + +**Example:** +```heroscript +!!site.navbar_item + label: "Documentation" + to: "docs/intro" + position: "left" + +!!site.navbar_item + label: "GitHub" + href: "https://github.com/myorg/repo" + position: "right" +``` + +**AI Guidelines:** +- Use `to` for internal navigation +- Use `href` for external links +- Position important items on the left, secondary items on the right + +### 5. Footer Configuration (`!!site.footer`) + +**Purpose:** Configure footer styling. + +**Optional Parameters:** +- `style`: "dark" or "light" (default: "dark") + +**Example:** +```heroscript +!!site.footer + style: "dark" +``` + +### 6. Footer Items (`!!site.footer_item`) + +**Purpose:** Add links to the footer, grouped by title. + +**Required Parameters:** +- `title`: Group title (items with same title are grouped together) +- `label`: Link text + +**Required Parameters (one of):** +- `to`: Internal link path +- `href`: External URL + +**Example:** +```heroscript +!!site.footer_item + title: "Docs" + label: "Introduction" + to: "intro" + +!!site.footer_item + title: "Docs" + label: "API Reference" + to: "api" + +!!site.footer_item + title: "Community" + label: "Discord" + href: "https://discord.gg/example" +``` + +**AI Guidelines:** +- Group related links under the same title +- Use consistent title names across related items +- Provide both internal and external links as appropriate + +### 7. Page Categories (`!!site.page_category`) + +**Purpose:** Create a section/category to organize pages. + +**Required Parameters:** +- `name`: Category identifier (snake_case) + +**Optional Parameters:** +- `label`: Display name (auto-generated from name if not provided) +- `position`: Manual sort order (auto-incremented if not specified) +- `path`: URL path segment (defaults to normalized label) + +**Example:** +```heroscript +!!site.page_category + name: "getting_started" + label: "Getting Started" + position: 100 + +!!site.page_category + name: "advanced_topics" + label: "Advanced Topics" +``` + +**AI Guidelines:** +- Use descriptive snake_case names +- Let label be auto-generated when possible (name_fix converts to Title Case) +- Categories persist for all subsequent pages until a new category is declared +- Position values should leave gaps (100, 200, 300) for future insertions + +### 8. Pages (`!!site.page`) + +**Purpose:** Define individual pages in the site. + +**Required Parameters:** +- `src`: Source reference as `collection:page_name` (required for first page in a collection) + +**Optional Parameters:** +- `name`: Page identifier (extracted from src if not provided) +- `title`: Page title (extracted from markdown if not provided) +- `description`: Page description for metadata +- `slug`: Custom URL slug +- `position`: Manual sort order (auto-incremented if not specified) +- `draft`: Mark as draft (default: false) +- `hide_title`: Hide title in rendering (default: false) +- `path`: Custom path (defaults to current category name) +- `category`: Override current category +- `title_nr`: Title numbering level + +**Example:** +```heroscript +!!site.page src: "docs:introduction" + description: "Introduction to the platform" + slug: "/" + +!!site.page src: "quickstart" + description: "Get started in 5 minutes" + +!!site.page src: "installation" + title: "Installation Guide" + description: "How to install and configure" + position: 10 +``` + +**AI Guidelines:** +- **Collection Persistence:** Specify collection once (e.g., `docs:introduction`), then subsequent pages only need page name (e.g., `quickstart`) +- **Category Persistence:** Pages belong to the most recently declared category +- **Title Extraction:** Prefer extracting titles from markdown files +- **Position Management:** Use automatic positioning unless specific order is required +- **Description Required:** Always provide descriptions for SEO +- **Slug Usage:** Use slug for special pages like homepage (`slug: "/"`) + +### 9. Import External Content (`!!site.import`) + +**Purpose:** Import content from external sources. + +**Optional Parameters:** +- `name`: Import identifier +- `url`: Git URL or HTTP URL +- `path`: Local file system path +- `dest`: Destination path in site +- `replace`: Comma-separated key:value pairs for variable replacement +- `visible`: Whether imported content is visible (default: true) + +**Example:** +```heroscript +!!site.import + url: "https://github.com/example/docs" + dest: "external" + replace: "VERSION:1.0.0,PROJECT:MyProject" + visible: true +``` + +**AI Guidelines:** +- Use for shared documentation across multiple sites +- Replace variables using `${VARIABLE}` syntax in source content +- Set `visible: false` for imported templates or partials + +### 10. Publish Destinations (`!!site.publish` and `!!site.publish_dev`) + +**Purpose:** Define where to publish the built site. + +**Optional Parameters:** +- `path`: File system path or URL +- `ssh_name`: SSH connection name for remote deployment + +**Example:** +```heroscript +!!site.publish + path: "/var/www/html/docs" + ssh_name: "production_server" + +!!site.publish_dev + path: "/tmp/docs-preview" +``` + +**AI Guidelines:** +- Use `!!site.publish` for production deployments +- Use `!!site.publish_dev` for development/preview deployments +- Can specify multiple destinations + +## File Organization Best Practices + +### Naming Convention + +Use numeric prefixes to control execution order: + +``` +0_config.heroscript # Site configuration +1_navigation.heroscript # Menu and footer +2_intro.heroscript # Introduction pages +3_guides.heroscript # User guides +4_reference.heroscript # API reference +``` + +**AI Guidelines:** +- Always use numeric prefixes (0_, 1_, 2_, etc.) +- Leave gaps in numbering (0, 10, 20) for future insertions +- Group related configurations in the same file +- Process order matters: config → navigation → pages + +### Execution Order Rules + +1. **Configuration First:** `!!site.config` must be processed before other actions +2. **Categories Before Pages:** Declare `!!site.page_category` before pages in that category +3. **Collection Persistence:** First page in a collection must specify `collection:page_name` +4. **Category Persistence:** Pages inherit the most recent category declaration + +## Common Patterns + +### Pattern 1: Simple Documentation Site + +```heroscript +!!site.config + name: "simple_docs" + title: "Simple Documentation" + +!!site.navbar + title: "Simple Docs" + +!!site.page src: "docs:index" + description: "Welcome page" + slug: "/" + +!!site.page src: "getting-started" + description: "Getting started guide" + +!!site.page src: "api" + description: "API reference" +``` + +### Pattern 2: Multi-Section Documentation + +```heroscript +!!site.config + name: "multi_section_docs" + title: "Complete Documentation" + +!!site.page_category + name: "introduction" + label: "Introduction" + +!!site.page src: "docs:welcome" + description: "Welcome to our documentation" + +!!site.page src: "overview" + description: "Platform overview" + +!!site.page_category + name: "tutorials" + label: "Tutorials" + +!!site.page src: "tutorial_basics" + description: "Basic tutorial" + +!!site.page src: "tutorial_advanced" + description: "Advanced tutorial" +``` + +### Pattern 3: Complex Site with External Links + +```heroscript +!!site.config + name: "complex_site" + title: "Complex Documentation Site" + url: "https://docs.example.com" + +!!site.navbar + title: "My Platform" + logo_src: "img/logo.svg" + +!!site.navbar_item + label: "Docs" + to: "docs/intro" + position: "left" + +!!site.navbar_item + label: "API" + to: "api" + position: "left" + +!!site.navbar_item + label: "GitHub" + href: "https://github.com/example/repo" + position: "right" + +!!site.footer + style: "dark" + +!!site.footer_item + title: "Documentation" + label: "Getting Started" + to: "docs/intro" + +!!site.footer_item + title: "Community" + label: "Discord" + href: "https://discord.gg/example" + +!!site.page_category + name: "getting_started" + +!!site.page src: "docs:introduction" + description: "Introduction to the platform" + slug: "/" + +!!site.page src: "installation" + description: "Installation guide" +``` + +## Error Prevention + +### Common Mistakes to Avoid + +1. **Missing Collection on First Page:** + ```heroscript + # WRONG - no collection specified + !!site.page src: "introduction" + + # CORRECT + !!site.page src: "docs:introduction" + ``` + +2. **Category Without Name:** + ```heroscript + # WRONG - missing name + !!site.page_category + label: "Getting Started" + + # CORRECT + !!site.page_category + name: "getting_started" + label: "Getting Started" + ``` + +3. **Missing Description:** + ```heroscript + # WRONG - no description + !!site.page src: "docs:intro" + + # CORRECT + !!site.page src: "docs:intro" + description: "Introduction to the platform" + ``` + +4. **Incorrect File Ordering:** + ``` + # WRONG - pages before config + pages.heroscript + config.heroscript + + # CORRECT - config first + 0_config.heroscript + 1_pages.heroscript + ``` + +## Validation Checklist + +When generating HeroScript for the Site module, verify: + +- [ ] `!!site.config` includes `name` parameter +- [ ] All pages have `description` parameter +- [ ] First page in each collection specifies `collection:page_name` +- [ ] Categories are declared before their pages +- [ ] Files use numeric prefixes for ordering +- [ ] Navigation items have either `to` or `href` +- [ ] Footer items are grouped by `title` +- [ ] External URLs include protocol (https://) +- [ ] Paths don't have trailing slashes unless intentional +- [ ] Draft pages are marked with `draft: true` + +## Integration with V Code + +When working with the Site module in V code: + +```v +import incubaid.herolib.web.site +import incubaid.herolib.core.playbook + +// Process HeroScript files +mut plbook := playbook.new(path: '/path/to/heroscripts')! +site.play(mut plbook)! + +// Access configured site +mut mysite := site.get(name: 'my_site')! + +// Iterate through pages +for page in mysite.pages { + println('Page: ${page.name} - ${page.description}') +} + +// Iterate through sections +for section in mysite.sections { + println('Section: ${section.label}') +} +``` + +## Summary + +The Site module's HeroScript format provides a declarative way to configure websites with: +- Clear separation of concerns (config, navigation, content) +- Automatic ordering and organization +- Collection and category persistence for reduced repetition +- Flexible metadata and SEO configuration +- Support for both internal and external content + +Always follow the execution order rules, use numeric file prefixes, and provide complete metadata for best results. \ No newline at end of file diff --git a/lib/web/site/factory.v b/lib/web/site/factory.v index d5facfe6..6d0cb5fc 100644 --- a/lib/web/site/factory.v +++ b/lib/web/site/factory.v @@ -3,7 +3,7 @@ module site import incubaid.herolib.core.texttools __global ( - mywebsites map[string]&Site + websites map[string]&Site ) @[params] @@ -15,13 +15,7 @@ pub mut: pub fn new(args FactoryArgs) !&Site { name := texttools.name_fix(args.name) - // Check if a site with this name already exists - if name in mywebsites { - // Return the existing site instead of creating a new one - return get(name: name)! - } - - mywebsites[name] = &Site{ + websites[name] = &Site{ siteconfig: SiteConfig{ name: name } @@ -31,17 +25,18 @@ pub fn new(args FactoryArgs) !&Site { pub fn get(args FactoryArgs) !&Site { name := texttools.name_fix(args.name) - mut sc := mywebsites[name] or { return error('siteconfig with name "${name}" does not exist') } + mut sc := websites[name] or { return error('siteconfig with name "${name}" does not exist') } return sc } pub fn exists(args FactoryArgs) bool { name := texttools.name_fix(args.name) - return name in mywebsites + mut sc := websites[name] or { return false } + return true } pub fn default() !&Site { - if mywebsites.len == 0 { + if websites.len == 0 { return new(name: 'default')! } return get()! @@ -49,5 +44,5 @@ pub fn default() !&Site { // list returns all site names that have been created pub fn list() []string { - return mywebsites.keys() + return websites.keys() } diff --git a/lib/web/site/model_nav.v b/lib/web/site/model_nav.v deleted file mode 100644 index 28c23a05..00000000 --- a/lib/web/site/model_nav.v +++ /dev/null @@ -1,143 +0,0 @@ -module site - -import json - -// Top-level config -pub struct NavConfig { -pub mut: - my_sidebar []NavItem - // myTopbar []NavItem //not used yet - // myFooter []NavItem //not used yet -} - -// -------- Variant Type -------- -pub type NavItem = NavDoc | NavCat | NavLink - -// --------- DOC ITEM ---------- -pub struct NavDoc { -pub: - id string // is the page id - label string -} - -// --------- CATEGORY ---------- -pub struct NavCat { -pub mut: - label string - collapsible bool - collapsed bool - items []NavItem -} - -// --------- LINK ---------- -pub struct NavLink { -pub: - label string - href string - description string -} - -// -------- JSON SERIALIZATION -------- - -// NavItemJson is used for JSON export with type discrimination -pub struct NavItemJson { -pub mut: - type_field string @[json: 'type'] - // For doc - id string @[omitempty] - label string @[omitempty] - // For link - href string @[omitempty] - description string @[omitempty] - // For category - collapsible bool - collapsed bool - items []NavItemJson @[omitempty] -} - -// Convert a single NavItem to JSON-serializable format -fn nav_item_to_json(item NavItem) !NavItemJson { - return match item { - NavDoc { - NavItemJson{ - type_field: 'doc' - id: item.id - label: item.label - collapsible: false - collapsed: false - } - } - NavLink { - NavItemJson{ - type_field: 'link' - label: item.label - href: item.href - description: item.description - collapsible: false - collapsed: false - } - } - NavCat { - mut json_items := []NavItemJson{} - for sub_item in item.items { - json_items << nav_item_to_json(sub_item)! - } - NavItemJson{ - type_field: 'category' - label: item.label - collapsible: item.collapsible - collapsed: item.collapsed - items: json_items - } - } - } -} - -// Convert entire NavConfig sidebar to JSON string -fn (nc NavConfig) sidebar_to_json() !string { - mut result := []NavItemJson{} - for item in nc.my_sidebar { - result << nav_item_to_json(item)! - } - return json.encode_pretty(result) -} - -// // Convert entire NavConfig topbar to JSON-serializable array -// fn (nc NavConfig) topbar_to_json() ![]NavItemJson { -// mut result := []NavItemJson{} -// for item in nc.myTopbar { -// result << nav_item_to_json(item)! -// } -// return result -// } - -// // Convert entire NavConfig footer to JSON-serializable array -// fn (nc NavConfig) footer_to_json() ![]NavItemJson { -// mut result := []NavItemJson{} -// for item in nc.myFooter { -// result << nav_item_to_json(item)! -// } -// return result -// } - -// port topbar as formatted JSON string -// pub fn (nc NavConfig) jsondump_topbar() !string { -// items := nc.topbar_to_json()! -// return json.encode_pretty(items) -// } - -// // Export footer as formatted JSON string -// pub fn (nc NavConfig) jsondump_footer() !string { -// items := nc.footer_to_json()! -// return json.encode_pretty(items) -// } - -// // Export all navigation as object with sidebar, topbar, footer -// pub fn (nc NavConfig) jsondump_all() !string { -// all_nav := map[string][]NavItemJson{ -// 'sidebar': nc.sidebar_to_json()! -// 'topbar': nc.topbar_to_json()! -// 'footer': nc.footer_to_json()! -// } -// return json.encode_pretty(all_nav) -// } diff --git a/lib/web/site/model_page.v b/lib/web/site/model_page.v index c13a67bf..30bfeaed 100644 --- a/lib/web/site/model_page.v +++ b/lib/web/site/model_page.v @@ -1,12 +1,16 @@ module site -// Page represents a single documentation page pub struct Page { pub mut: - id string // Unique identifier: "collection:page_name" - title string // Display title (optional, extracted from markdown if empty) - description string // Brief description for metadata - draft bool // Mark as draft (hidden from navigation) - hide_title bool // Hide the title when rendering - src string // Source reference (same as id in this format) + name string + title string + description string + draft bool + position int + hide_title bool + src string @[required] // always in format collection:page_name, can use the default collection if no : specified + path string @[required] // is without the page name, so just the path to the folder where the page is in + section_name string + title_nr int + slug string } diff --git a/lib/web/site/model_site.v b/lib/web/site/model_site.v deleted file mode 100644 index 4e4fd723..00000000 --- a/lib/web/site/model_site.v +++ /dev/null @@ -1,9 +0,0 @@ -module site - -@[heap] -pub struct Site { -pub mut: - pages map[string]Page // key: "collection:page_name" - nav NavConfig // Navigation sidebar configuration - siteconfig SiteConfig // Full site configuration -} diff --git a/lib/web/site/model_site_section.v b/lib/web/site/model_site_section.v new file mode 100644 index 00000000..df491fa0 --- /dev/null +++ b/lib/web/site/model_site_section.v @@ -0,0 +1,18 @@ +module site + +@[heap] +pub struct Site { +pub mut: + pages []Page + sections []Section + siteconfig SiteConfig +} + +pub struct Section { +pub mut: + name string + position int + path string + label string + description string +} diff --git a/lib/web/site/play.v b/lib/web/site/play.v index 016c0035..907185ad 100644 --- a/lib/web/site/play.v +++ b/lib/web/site/play.v @@ -4,93 +4,222 @@ import os import incubaid.herolib.core.playbook { PlayBook } import incubaid.herolib.core.texttools import time -import incubaid.herolib.ui.console -// Main entry point for processing site HeroScript pub fn play(mut plbook PlayBook) ! { if !plbook.exists(filter: 'site.') { return } - console.print_header('Processing Site Configuration') - - // ============================================================ - // STEP 1: Initialize core site configuration - // ============================================================ - console.print_item('Step 1: Loading site configuration') mut config_action := plbook.ensure_once(filter: 'site.config')! - mut p := config_action.params - name := p.get_default('name', 'default')! + mut p := config_action.params + name := p.get_default('name', 'default')! // Use 'default' as fallback name + + // configure the website mut website := new(name: name)! mut config := &website.siteconfig - // Load core configuration config.name = texttools.name_fix(name) config.title = p.get_default('title', 'Documentation Site')! config.description = p.get_default('description', 'Comprehensive documentation built with Docusaurus.')! config.tagline = p.get_default('tagline', 'Your awesome documentation')! config.favicon = p.get_default('favicon', 'img/favicon.png')! config.image = p.get_default('image', 'img/tf_graph.png')! - config.copyright = p.get_default('copyright', '© ${time.now().year} Example Organization')! + config.copyright = p.get_default('copyright', '© ' + time.now().year.str() + + ' Example Organization')! config.url = p.get_default('url', '')! config.base_url = p.get_default('base_url', '/')! config.url_home = p.get_default('url_home', '')! - config_action.done = true + // Process !!site.config_meta for specific metadata overrides + mut meta_action := plbook.ensure_once(filter: 'site.config_meta')! + mut p_meta := meta_action.params - // ============================================================ - // STEP 2: Apply optional metadata overrides - // ============================================================ - console.print_item('Step 2: Applying metadata overrides') - if plbook.exists_once(filter: 'site.config_meta') { - mut meta_action := plbook.get(filter: 'site.config_meta')! - mut p_meta := meta_action.params - - config.meta_title = p_meta.get_default('title', config.title)! - config.meta_image = p_meta.get_default('image', config.image)! - if p_meta.exists('description') { - config.description = p_meta.get('description')! - } - - meta_action.done = true + // If 'title' is present in site.config_meta, it overrides. Otherwise, meta_title remains empty or uses site.config.title logic in docusaurus model. + config.meta_title = p_meta.get_default('title', config.title)! + // If 'image' is present in site.config_meta, it overrides. Otherwise, meta_image remains empty or uses site.config.image logic. + config.meta_image = p_meta.get_default('image', config.image)! + // If 'description' is present in site.config_meta, it overrides the main description + if p_meta.exists('description') { + config.description = p_meta.get('description')! } - // ============================================================ - // STEP 3: Configure content imports - // ============================================================ - console.print_item('Step 3: Configuring content imports') - play_imports(mut plbook, mut config)! + config_action.done = true // Mark the action as done + meta_action.done = true - // ============================================================ - // STEP 4: Configure navigation menu - // ============================================================ - console.print_item('Step 4: Configuring navigation menu') - play_navbar(mut plbook, mut config)! - - // ============================================================ - // STEP 5: Configure footer - // ============================================================ - console.print_item('Step 5: Configuring footer') + play_import(mut plbook, mut config)! + play_menu(mut plbook, mut config)! play_footer(mut plbook, mut config)! - - // ============================================================ - // STEP 6: Configure announcement bar (optional) - // ============================================================ - console.print_item('Step 6: Configuring announcement bar (if present)') play_announcement(mut plbook, mut config)! - - // ============================================================ - // STEP 7: Configure publish destinations - // ============================================================ - console.print_item('Step 7: Configuring publish destinations') - play_publishing(mut plbook, mut config)! - - // ============================================================ - // STEP 8: Build pages and navigation structure - // ============================================================ - console.print_item('Step 8: Processing pages and building navigation') + play_publish(mut plbook, mut config)! + play_publish_dev(mut plbook, mut config)! play_pages(mut plbook, mut website)! - - console.print_green('Site configuration complete') +} + +fn play_import(mut plbook PlayBook, mut config SiteConfig) ! { + mut import_actions := plbook.find(filter: 'site.import')! + // println('import_actions: ${import_actions}') + + for mut action in import_actions { + mut p := action.params + mut replace_map := map[string]string{} + if replace_str := p.get_default('replace', '') { + parts := replace_str.split(',') + for part in parts { + kv := part.split(':') + if kv.len == 2 { + replace_map[kv[0].trim_space()] = kv[1].trim_space() + } + } + } + + mut importpath := p.get_default('path', '')! + if importpath != '' { + if !importpath.starts_with('/') { + importpath = os.abs_path('${plbook.path}/${importpath}') + } + } + + mut import_ := ImportItem{ + name: p.get_default('name', '')! + url: p.get_default('url', '')! + path: importpath + dest: p.get_default('dest', '')! + replace: replace_map + visible: p.get_default_false('visible') + } + config.imports << import_ + + action.done = true // Mark the action as done + } +} + +fn play_menu(mut plbook PlayBook, mut config SiteConfig) ! { + mut navbar_actions := plbook.find(filter: 'site.navbar')! + if navbar_actions.len > 0 { + for mut action in navbar_actions { // Should ideally be one, but loop for safety + mut p := action.params + config.menu.title = p.get_default('title', config.title)! // Use existing config.title as ultimate fallback + config.menu.logo_alt = p.get_default('logo_alt', '')! + config.menu.logo_src = p.get_default('logo_src', '')! + config.menu.logo_src_dark = p.get_default('logo_src_dark', '')! + action.done = true // Mark the action as done + } + } else { + // Fallback to site.menu for title if site.navbar is not found + mut menu_actions := plbook.find(filter: 'site.menu')! + for mut action in menu_actions { + mut p := action.params + config.menu.title = p.get_default('title', config.title)! + config.menu.logo_alt = p.get_default('logo_alt', '')! + config.menu.logo_src = p.get_default('logo_src', '')! + config.menu.logo_src_dark = p.get_default('logo_src_dark', '')! + action.done = true // Mark the action as done + } + } + + mut menu_item_actions := plbook.find(filter: 'site.navbar_item')! + if menu_item_actions.len == 0 { + // Fallback to site.menu_item if site.navbar_item is not found + menu_item_actions = plbook.find(filter: 'site.menu_item')! + } + + // Clear existing menu items to prevent duplication + config.menu.items = []MenuItem{} + + for mut action in menu_item_actions { + mut p := action.params + mut item := MenuItem{ + label: p.get_default('label', 'Documentation')! + href: p.get_default('href', '')! + to: p.get_default('to', '')! + position: p.get_default('position', 'right')! + } + config.menu.items << item + action.done = true // Mark the action as done + } +} + +fn play_footer(mut plbook PlayBook, mut config SiteConfig) ! { + mut footer_actions := plbook.find(filter: 'site.footer')! + for mut action in footer_actions { + mut p := action.params + config.footer.style = p.get_default('style', 'dark')! + action.done = true // Mark the action as done + } + + mut footer_item_actions := plbook.find(filter: 'site.footer_item')! + mut links_map := map[string][]FooterItem{} + + // Clear existing footer links to prevent duplication + config.footer.links = []FooterLink{} + + for mut action in footer_item_actions { + mut p := action.params + title := p.get_default('title', 'Docs')! + mut item := FooterItem{ + label: p.get_default('label', 'Introduction')! + href: p.get_default('href', '')! + to: p.get_default('to', '')! + } + + if title !in links_map { + links_map[title] = []FooterItem{} + } + links_map[title] << item + action.done = true // Mark the action as done + } + + // Convert map to footer links array + for title, items in links_map { + config.footer.links << FooterLink{ + title: title + items: items + } + } +} + +fn play_announcement(mut plbook PlayBook, mut config SiteConfig) ! { + mut announcement_actions := plbook.find(filter: 'site.announcement')! + if announcement_actions.len > 0 { + // Only process the first announcement action + mut action := announcement_actions[0] + mut p := action.params + + config.announcement = AnnouncementBar{ + id: p.get_default('id', 'announcement')! + content: p.get_default('content', '')! + background_color: p.get_default('background_color', '#20232a')! + text_color: p.get_default('text_color', '#fff')! + is_closeable: p.get_default_true('is_closeable') + } + + action.done = true // Mark the action as done + } +} + +fn play_publish(mut plbook PlayBook, mut config SiteConfig) ! { + mut build_dest_actions := plbook.find(filter: 'site.publish')! + for mut action in build_dest_actions { + mut p := action.params + mut dest := BuildDest{ + path: p.get_default('path', '')! // can be url + ssh_name: p.get_default('ssh_name', '')! + } + config.build_dest << dest + action.done = true // Mark the action as done + } +} + +fn play_publish_dev(mut plbook PlayBook, mut config SiteConfig) ! { + mut build_dest_actions := plbook.find(filter: 'site.publish_dev')! + for mut action in build_dest_actions { + mut p := action.params + mut dest := BuildDest{ + path: p.get_default('path', '')! // can be url + ssh_name: p.get_default('ssh_name', '')! + } + config.build_dest_dev << dest + action.done = true // Mark the action as done + } } diff --git a/lib/web/site/play_announcement.v b/lib/web/site/play_announcement.v deleted file mode 100644 index f516e7e1..00000000 --- a/lib/web/site/play_announcement.v +++ /dev/null @@ -1,34 +0,0 @@ -module site - -import os -import incubaid.herolib.core.playbook { PlayBook } -import incubaid.herolib.core.texttools -import time -import incubaid.herolib.ui.console - -// ============================================================ -// ANNOUNCEMENT: Process announcement bar (optional) -// ============================================================ -fn play_announcement(mut plbook PlayBook, mut config SiteConfig) ! { - mut announcement_actions := plbook.find(filter: 'site.announcement')! - - if announcement_actions.len > 0 { - // Only process the first announcement action - mut action := announcement_actions[0] - mut p := action.params - - content := p.get('content') or { - return error('!!site.announcement: must specify "content"') - } - - config.announcement = AnnouncementBar{ - id: p.get_default('id', 'announcement')! - content: content - background_color: p.get_default('background_color', '#20232a')! - text_color: p.get_default('text_color', '#fff')! - is_closeable: p.get_default_true('is_closeable') - } - - action.done = true - } -} diff --git a/lib/web/site/play_footer.v b/lib/web/site/play_footer.v deleted file mode 100644 index 0601eb83..00000000 --- a/lib/web/site/play_footer.v +++ /dev/null @@ -1,62 +0,0 @@ -module site - -import os -import incubaid.herolib.core.playbook { PlayBook } -import incubaid.herolib.core.texttools -import time -import incubaid.herolib.ui.console - -// ============================================================ -// FOOTER: Process footer configuration -// ============================================================ -fn play_footer(mut plbook PlayBook, mut config SiteConfig) ! { - // Process footer style (optional) - mut footer_actions := plbook.find(filter: 'site.footer')! - for mut action in footer_actions { - mut p := action.params - config.footer.style = p.get_default('style', 'dark')! - action.done = true - } - - // Process footer items (multiple) - mut footer_item_actions := plbook.find(filter: 'site.footer_item')! - mut links_map := map[string][]FooterItem{} - - // Clear existing links to prevent duplication - config.footer.links = []FooterLink{} - - for mut action in footer_item_actions { - mut p := action.params - - title := p.get_default('title', 'Docs')! - - label := p.get('label') or { - return error('!!site.footer_item: must specify "label"') - } - - mut item := FooterItem{ - label: label - href: p.get_default('href', '')! - to: p.get_default('to', '')! - } - - // Validate that href or to is specified - if item.href.len == 0 && item.to.len == 0 { - return error('!!site.footer_item for "${label}": must specify either "href" or "to"') - } - - if title !in links_map { - links_map[title] = []FooterItem{} - } - links_map[title] << item - action.done = true - } - - // Convert map to footer links array - for title, items in links_map { - config.footer.links << FooterLink{ - title: title - items: items - } - } -} diff --git a/lib/web/site/play_imports.v b/lib/web/site/play_imports.v deleted file mode 100644 index 05039c60..00000000 --- a/lib/web/site/play_imports.v +++ /dev/null @@ -1,51 +0,0 @@ -module site - -import os -import incubaid.herolib.core.playbook { PlayBook } -import incubaid.herolib.core.texttools -import time -import incubaid.herolib.ui.console - -// ============================================================ -// IMPORTS: Process content imports -// ============================================================ -fn play_imports(mut plbook PlayBook, mut config SiteConfig) ! { - mut import_actions := plbook.find(filter: 'site.import')! - - for mut action in import_actions { - mut p := action.params - - // Parse replacement patterns (comma-separated key:value pairs) - mut replace_map := map[string]string{} - if replace_str := p.get_default('replace', '') { - parts := replace_str.split(',') - for part in parts { - kv := part.split(':') - if kv.len == 2 { - replace_map[kv[0].trim_space()] = kv[1].trim_space() - } - } - } - - // Get path (can be relative to playbook path) - mut import_path := p.get_default('path', '')! - if import_path != '' { - if !import_path.starts_with('/') { - import_path = os.abs_path('${plbook.path}/${import_path}') - } - } - - // Create import item - mut import_item := ImportItem{ - name: p.get_default('name', '')! - url: p.get_default('url', '')! - path: import_path - dest: p.get_default('dest', '')! - replace: replace_map - visible: p.get_default_false('visible') - } - - config.imports << import_item - action.done = true - } -} diff --git a/lib/web/site/play_navbar.v b/lib/web/site/play_navbar.v deleted file mode 100644 index 08b95810..00000000 --- a/lib/web/site/play_navbar.v +++ /dev/null @@ -1,60 +0,0 @@ -module site - -import os -import incubaid.herolib.core.playbook { PlayBook } -import incubaid.herolib.core.texttools -import time -import incubaid.herolib.ui.console - -// ============================================================ -// NAVBAR: Process navigation menu -// ============================================================ -fn play_navbar(mut plbook PlayBook, mut config SiteConfig) ! { - // Try 'site.navbar' first, then fallback to deprecated 'site.menu' - mut navbar_actions := plbook.find(filter: 'site.navbar')! - if navbar_actions.len == 0 { - navbar_actions = plbook.find(filter: 'site.menu')! - } - - // Configure navbar metadata - if navbar_actions.len > 0 { - for mut action in navbar_actions { - mut p := action.params - config.menu.title = p.get_default('title', config.title)! - config.menu.logo_alt = p.get_default('logo_alt', '')! - config.menu.logo_src = p.get_default('logo_src', '')! - config.menu.logo_src_dark = p.get_default('logo_src_dark', '')! - action.done = true - } - } - - // Process navbar items - mut navbar_item_actions := plbook.find(filter: 'site.navbar_item')! - if navbar_item_actions.len == 0 { - navbar_item_actions = plbook.find(filter: 'site.menu_item')! - } - - // Clear existing items to prevent duplication - config.menu.items = []MenuItem{} - - for mut action in navbar_item_actions { - mut p := action.params - - label := p.get('label') or { return error('!!site.navbar_item: must specify "label"') } - - mut item := MenuItem{ - label: label - href: p.get_default('href', '')! - to: p.get_default('to', '')! - position: p.get_default('position', 'right')! - } - - // Validate that at least href or to is specified - if item.href.len == 0 && item.to.len == 0 { - return error('!!site.navbar_item: must specify either "href" or "to" for label "${label}"') - } - - config.menu.items << item - action.done = true - } -} diff --git a/lib/web/site/play_page.v b/lib/web/site/play_page.v new file mode 100644 index 00000000..333293df --- /dev/null +++ b/lib/web/site/play_page.v @@ -0,0 +1,135 @@ +module site + +import incubaid.herolib.core.playbook { PlayBook } +import incubaid.herolib.core.texttools + +// plays the sections & pages +fn play_pages(mut plbook PlayBook, mut site Site) ! { + // mut siteconfig := &site.siteconfig + + // if only 1 doctree is specified, then we use that as the default doctree name + // mut doctreename := 'main' // Not used for now, keep commented for future doctree integration + // if plbook.exists(filter: 'site.doctree') { + // if plbook.exists_once(filter: 'site.doctree') { + // mut action := plbook.get(filter: 'site.doctree')! + // mut p := action.params + // doctreename = p.get('name') or { return error('need to specify name in site.doctree') } + // } else { + // return error("can't have more than one site.doctree") + // } + // } + + mut section_current := Section{} // is the category + mut position_section := 1 + mut position_category := 100 // Start categories at position 100 + mut collection_current := '' // current collection we are working on + + mut all_actions := plbook.find(filter: 'site.')! + + for mut action in all_actions { + if action.done { + continue + } + + mut p := action.params + + if action.name == 'page_category' { + mut section := Section{} + section.name = p.get('name') or { + return error('need to specify name in site.page_category. Action: ${action}') + } + position_section = 1 // go back to default position for pages in the category + section.position = p.get_int_default('position', position_category)! + if section.position == position_category { + position_category += 100 // Increment for next category + } + section.label = p.get_default('label', texttools.name_fix_snake_to_pascal(section.name))! + section.path = p.get_default('path', texttools.name_fix(section.label))! + section.description = p.get_default('description', '')! + + site.sections << section + action.done = true // Mark the action as done + section_current = section + continue // next action + } + + if action.name == 'page' { + mut pagesrc := p.get_default('src', '')! + mut pagename := p.get_default('name', '')! + mut pagecollection := '' + + if pagesrc.contains(':') { + pagecollection = pagesrc.split(':')[0] + pagename = pagesrc.split(':')[1] + } else { + if collection_current.len > 0 { + pagecollection = collection_current + pagename = pagesrc // ADD THIS LINE - use pagesrc as the page name + } else { + return error('need to specify collection in page.src path as collection:page_name or make sure someone before you did. Got src="${pagesrc}" with no collection set. Action: ${action}') + } + } + + pagecollection = texttools.name_fix(pagecollection) + collection_current = pagecollection + pagename = texttools.name_fix_keepext(pagename) + if pagename.ends_with('.md') { + pagename = pagename.replace('.md', '') + } + + if pagename == '' { + return error('need to specify name in page.src or specify in path as collection:page_name. Action: ${action}') + } + if pagecollection == '' { + return error('need to specify collection in page.src or specify in path as collection:page_name. Action: ${action}') + } + + // recreate the pagepath + pagesrc = '${pagecollection}:${pagename}' + + // get sectionname from category, page_category or section, if not specified use current section + section_name := p.get_default('category', p.get_default('page_category', p.get_default('section', + section_current.name)!)!)! + mut pagepath := p.get_default('path', section_current.path)! + pagepath = pagepath.trim_space().trim('/') + // Only apply name_fix if it's a simple name (no path separators) + // For paths like 'appendix/internet_today', preserve the structure + if !pagepath.contains('/') { + pagepath = texttools.name_fix(pagepath) + } + // Ensure pagepath ends with / to indicate it's a directory path + if pagepath.len > 0 && !pagepath.ends_with('/') { + pagepath += '/' + } + + mut mypage := Page{ + section_name: section_name + name: pagename + path: pagepath + src: pagesrc + } + + mypage.position = p.get_int_default('position', 0)! + if mypage.position == 0 { + mypage.position = section_current.position + position_section + position_section += 1 + } + mypage.title = p.get_default('title', '')! + + mypage.description = p.get_default('description', '')! + mypage.slug = p.get_default('slug', '')! + mypage.draft = p.get_default_false('draft') + mypage.hide_title = p.get_default_false('hide_title') + mypage.title_nr = p.get_int_default('title_nr', 0)! + + site.pages << mypage + + action.done = true // Mark the action as done + } + + // println(action) + // println(section_current) + // println(site.pages.last()) + // $dbg; + } +} diff --git a/lib/web/site/play_pages.v b/lib/web/site/play_pages.v deleted file mode 100644 index b37debd9..00000000 --- a/lib/web/site/play_pages.v +++ /dev/null @@ -1,203 +0,0 @@ -module site - -import os -import incubaid.herolib.core.playbook { PlayBook } -import incubaid.herolib.core.texttools -import time -import incubaid.herolib.ui.console - -// ============================================================ -// Helper function: normalize name while preserving .md extension handling -// ============================================================ -fn normalize_page_name(name string) string { - mut result := name - // Remove .md extension if present for processing - if result.ends_with('.md') { - result = result[0..result.len - 3] - } - // Apply name fixing - return texttools.name_fix(result) -} - -// ============================================================ -// Internal structure for tracking category information -// ============================================================ -struct CategoryInfo { -pub mut: - name string - label string - position int - nav_items []NavItem -} - -// ============================================================ -// PAGES: Process pages and build navigation structure -// ============================================================ -fn play_pages(mut plbook PlayBook, mut website Site) ! { - mut collection_current := '' // Track current collection for reuse - mut categories := map[string]CategoryInfo{} // Map of category name -> info - mut category_current := '' // Track current active category - mut root_nav_items := []NavItem{} // Root-level items (pages without category) - mut next_category_position := 100 // Auto-increment position for categories - - // ============================================================ - // PASS 1: Process all page and category actions - // ============================================================ - mut all_actions := plbook.find(filter: 'site.')! - - for mut action in all_actions { - if action.done { - continue - } - - // ========== PAGE CATEGORY ========== - if action.name == 'page_category' { - mut p := action.params - - category_name := p.get('name') or { - return error('!!site.page_category: must specify "name"') - } - - category_name_fixed := texttools.name_fix(category_name) - - // Get label (derive from name if not specified) - mut label := p.get_default('label', texttools.name_fix_snake_to_pascal(category_name_fixed))! - mut position := p.get_int_default('position', next_category_position)! - - // Auto-increment position if using default - if position == next_category_position { - next_category_position += 100 - } - - // Create and store category info - categories[category_name_fixed] = CategoryInfo{ - name: category_name_fixed - label: label - position: position - nav_items: []NavItem{} - } - - category_current = category_name_fixed - console.print_item('Created page category: "${label}" (${category_name_fixed})') - action.done = true - continue - } - - // ========== PAGE ========== - if action.name == 'page' { - mut p := action.params - - mut page_src := p.get_default('src', '')! - mut page_collection := '' - mut page_name := '' - - // Parse collection:page format from src - if page_src.contains(':') { - parts := page_src.split(':') - page_collection = texttools.name_fix(parts[0]) - page_name = normalize_page_name(parts[1]) - } else { - // Use previously specified collection if available - if collection_current.len > 0 { - page_collection = collection_current - page_name = normalize_page_name(page_src) - } else { - return error('!!site.page: must specify source as "collection:page_name" in "src".\nGot src="${page_src}" with no collection previously set.\nEither specify "collection:page_name" or define a collection first.') - } - } - - // Validation - if page_name.len == 0 { - return error('!!site.page: could not extract valid page name from src="${page_src}"') - } - if page_collection.len == 0 { - return error('!!site.page: could not determine collection') - } - - // Store collection for subsequent pages - collection_current = page_collection - - // Build page ID - page_id := '${page_collection}:${page_name}' - - // Get optional page metadata - page_title := p.get_default('title', '')! - page_description := p.get_default('description', '')! - page_draft := p.get_default_false('draft') - page_hide_title := p.get_default_false('hide_title') - - // Create page - mut page := Page{ - id: page_id - title: page_title - description: page_description - draft: page_draft - hide_title: page_hide_title - src: page_id - } - - website.pages[page_id] = page - - // Create navigation item - nav_doc := NavDoc{ - id: page_id - label: if page_title.len > 0 { page_title } else { page_name } - } - - // Add to appropriate category or root - if category_current.len > 0 { - if category_current in categories { - mut cat_info := categories[category_current] - cat_info.nav_items << nav_doc - categories[category_current] = cat_info - console.print_debug('Added page "${page_id}" to category "${category_current}"') - } - } else { - root_nav_items << nav_doc - console.print_debug('Added root page "${page_id}"') - } - - action.done = true - continue - } - } - - // ============================================================ - // PASS 2: Build final navigation structure from categories - // ============================================================ - console.print_item('Building navigation structure...') - - mut final_nav_items := []NavItem{} - - // Add root items first - for item in root_nav_items { - final_nav_items << item - } - - // Sort categories by position and add them - mut sorted_categories := []CategoryInfo{} - for _, cat_info in categories { - sorted_categories << cat_info - } - - // Sort by position - sorted_categories.sort(a.position < b.position) - - // Convert categories to NavCat items and add to navigation - for cat_info in sorted_categories { - // Unwrap NavDoc items from cat_info.nav_items (they're already NavItem) - nav_cat := NavCat{ - label: cat_info.label - collapsible: true - collapsed: false - items: cat_info.nav_items - } - final_nav_items << nav_cat - console.print_debug('Added category to nav: "${cat_info.label}" with ${cat_info.nav_items.len} items') - } - - // Update website navigation - website.nav.my_sidebar = final_nav_items - - console.print_green('Navigation structure built with ${website.pages.len} pages in ${categories.len} categories') -} diff --git a/lib/web/site/play_publish.v b/lib/web/site/play_publish.v deleted file mode 100644 index e1309d1a..00000000 --- a/lib/web/site/play_publish.v +++ /dev/null @@ -1,46 +0,0 @@ -module site - -import os -import incubaid.herolib.core.playbook { PlayBook } -import incubaid.herolib.core.texttools -import time -import incubaid.herolib.ui.console - -// ============================================================ -// PUBLISHING: Configure build and publish destinations -// ============================================================ -fn play_publishing(mut plbook PlayBook, mut config SiteConfig) ! { - // Production publish destinations - mut build_dest_actions := plbook.find(filter: 'site.publish')! - for mut action in build_dest_actions { - mut p := action.params - - path := p.get('path') or { - return error('!!site.publish: must specify "path"') - } - - mut dest := BuildDest{ - path: path - ssh_name: p.get_default('ssh_name', '')! - } - config.build_dest << dest - action.done = true - } - - // Development publish destinations - mut build_dest_dev_actions := plbook.find(filter: 'site.publish_dev')! - for mut action in build_dest_dev_actions { - mut p := action.params - - path := p.get('path') or { - return error('!!site.publish_dev: must specify "path"') - } - - mut dest := BuildDest{ - path: path - ssh_name: p.get_default('ssh_name', '')! - } - config.build_dest_dev << dest - action.done = true - } -} diff --git a/lib/web/site/readme.md b/lib/web/site/readme.md index eda5ed03..40670c8a 100644 --- a/lib/web/site/readme.md +++ b/lib/web/site/readme.md @@ -2,83 +2,43 @@ The Site module provides a structured way to define website configurations, navigation menus, pages, and sections using HeroScript. It's designed to work with static site generators like Docusaurus. +## Purpose + +The Site module allows you to: + +- Define website structure and configuration in a declarative way using HeroScript +- Organize pages into sections/categories +- Configure navigation menus and footers +- Manage page metadata (title, description, slug, etc.) +- Support multiple content collections +- Define build and publish destinations ## Quick Start -### Minimal HeroScript Example - -```heroscript -!!site.config - name: "my_docs" - title: "My Documentation" - -!!site.page src: "docs:introduction" - title: "Getting Started" - -!!site.page src: "setup" - title: "Installation" -``` - -### Processing with V Code - ```v #!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run -import incubaid.herolib.core.playbook +import incubaid.herolib.develop.gittools import incubaid.herolib.web.site -import incubaid.herolib.ui.console +import incubaid.herolib.core.playcmds -// Process HeroScript file -mut plbook := playbook.new(path: './site_config.heroscript')! +// Clone or use existing repository with HeroScript files +mysitepath := gittools.path( + git_url: 'https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech' + git_pull: true +)! -// Execute site configuration -site.play(mut plbook)! +// Process all HeroScript files in the path +playcmds.run(heroscript_path: mysitepath.path)! -// Access the configured site -mut mysite := site.get(name: 'my_docs')! - -// Print available pages -pages_map := mysite.list_pages() -for page_id, _ in pages_map { - console.print_item('Page: ${page_id}') -} - -println('Site has ${mysite.pages.len} pages') +// Get the configured site +mut mysite := site.get(name: 'tfgrid_tech')! +println(mysite) ``` ---- - -## Core Concepts - -### Site -A website configuration that contains pages, navigation structure, and metadata. - -### Page -A single page with: -- **ID**: `collection:page_name` format -- **Title**: Display name (optional - extracted from markdown if not provided) -- **Description**: SEO metadata -- **Draft**: Hidden from navigation if true - -### Category (Section) -Groups related pages together in the navigation sidebar. Automatically collapsed/expandable. - -### Collection -A logical group of pages. Pages reuse the collection once specified. - -```heroscript -!!site.page src: "tech:intro" # Specifies collection "tech" -!!site.page src: "benefits" # Reuses collection "tech" -!!site.page src: "components" # Still uses collection "tech" -!!site.page src: "api:reference" # Switches to collection "api" -!!site.page src: "endpoints" # Uses collection "api" -``` - ---- - ## HeroScript Syntax -### 1. Site Configuration (Required) +### Basic Configuration ```heroscript !!site.config @@ -91,49 +51,20 @@ A logical group of pages. Pages reuse the collection once specified. copyright: "© 2024 My Organization" url: "https://docs.example.com" base_url: "/" - url_home: "/docs" ``` -**Parameters:** -- `name` - Internal site identifier (default: 'default') -- `title` - Main site title (shown in browser tab) -- `description` - Site description for SEO -- `tagline` - Short tagline/subtitle -- `favicon` - Path to favicon image -- `image` - Default OG image for social sharing -- `copyright` - Copyright notice -- `url` - Full site URL for Docusaurus -- `base_url` - Base URL path (e.g., "/" or "/docs/") -- `url_home` - Home page path - -### 2. Metadata Overrides (Optional) - -```heroscript -!!site.config_meta - title: "My Docs - Technical Reference" - image: "img/tech-og.png" - description: "Technical documentation and API reference" -``` - -Overrides specific metadata for SEO without changing core config. - -### 3. Navigation Bar +### Navigation Menu ```heroscript !!site.navbar - title: "My Documentation" + title: "My Site" logo_alt: "Site Logo" logo_src: "img/logo.svg" logo_src_dark: "img/logo-dark.svg" !!site.navbar_item label: "Documentation" - to: "intro" - position: "left" - -!!site.navbar_item - label: "API Reference" - to: "docs/api" + to: "docs/intro" position: "left" !!site.navbar_item @@ -142,13 +73,7 @@ Overrides specific metadata for SEO without changing core config. position: "right" ``` -**Parameters:** -- `label` - Display text (required) -- `to` - Internal link -- `href` - External URL -- `position` - "left" or "right" in navbar - -### 4. Footer Configuration +### Footer Configuration ```heroscript !!site.footer @@ -162,234 +87,242 @@ Overrides specific metadata for SEO without changing core config. !!site.footer_item title: "Docs" label: "Getting Started" - to: "getting-started" + href: "https://docs.example.com/getting-started" !!site.footer_item title: "Community" label: "Discord" href: "https://discord.gg/example" - -!!site.footer_item - title: "Legal" - label: "Privacy" - href: "https://example.com/privacy" ``` -### 5. Announcement Bar (Optional) +## Page Organization + +### Example 1: Simple Pages Without Categories + +When you don't need categories, pages are added sequentially. The collection only needs to be specified once, then it's reused for subsequent pages. ```heroscript -!!site.announcement - id: "new-release" - content: "🎉 Version 2.0 is now available!" - background_color: "#20232a" - text_color: "#fff" - is_closeable: true +!!site.page src: "mycelium_tech:introduction" + description: "Introduction to ThreeFold Technology" + slug: "/" + +!!site.page src: "vision" + description: "Our Vision for the Future Internet" + +!!site.page src: "what" + description: "What ThreeFold is Building" + +!!site.page src: "presentation" + description: "ThreeFold Technology Presentation" + +!!site.page src: "status" + description: "Current Development Status" ``` -### 6. Pages and Categories +**Key Points:** -#### Simple: Pages Without Categories +- First page specifies collection as `tech:introduction` (collection:page_name format) +- Subsequent pages only need the page name (e.g., `vision`) - the `tech` collection is reused +- If `title` is not specified, it will be extracted from the markdown file itself +- Pages are ordered by their appearance in the HeroScript file +- `slug` can be used to customize the URL path (e.g., `"/"` for homepage) -```heroscript -!!site.page src: "guides:introduction" - title: "Getting Started" - description: "Introduction to the platform" +### Example 2: Pages with Categories -!!site.page src: "installation" - title: "Installation" - -!!site.page src: "configuration" - title: "Configuration" -``` - -#### Advanced: Pages With Categories +Categories (sections) help organize pages into logical groups with their own navigation structure. ```heroscript !!site.page_category - name: "basics" - label: "Getting Started" + name: "first_principle_thinking" + label: "First Principle Thinking" -!!site.page src: "guides:introduction" - title: "Introduction" - description: "Learn the basics" +!!site.page src: "first_principle_thinking:hardware_badly_used" + description: "Hardware is not used properly, why it is important to understand hardware" -!!site.page src: "installation" - title: "Installation" +!!site.page src: "internet_risk" + description: "Internet risk, how to mitigate it, and why it is important" -!!site.page src: "configuration" - title: "Configuration" - -!!site.page_category - name: "advanced" - label: "Advanced Topics" - -!!site.page src: "advanced:performance" - title: "Performance Tuning" - -!!site.page src: "scaling" - title: "Scaling Guide" +!!site.page src: "onion_analogy" + description: "Compare onion with a computer, layers of abstraction" ``` -**Page Parameters:** -- `src` - Source as `collection:page` (first page) or just `page_name` (reuse collection) -- `title` - Page title (optional, extracted from markdown if not provided) -- `description` - Page description -- `draft` - Hide from navigation (default: false) -- `hide_title` - Don't show title in page (default: false) +**Key Points:** -### 7. Content Imports +- `!!site.page_category` creates a new section/category +- `name` is the internal identifier (snake_case) +- `label` is the display name (automatically derived from `name` if not specified) +- Category name is converted to title case: `first_principle_thinking` → "First Principle Thinking" +- Once a category is defined, all subsequent pages belong to it until a new category is declared +- Collection persistence works the same: specify once (e.g., `first_principle_thinking:hardware_badly_used`), then reuse + +### Example 3: Advanced Page Configuration + +```heroscript +!!site.page_category + name: "components" + label: "System Components" + position: 100 + +!!site.page src: "mycelium_tech:mycelium" + title: "Mycelium Network" + description: "Peer-to-peer overlay network" + slug: "mycelium-network" + position: 1 + draft: false + hide_title: false + +!!site.page src: "fungistor" + title: "Fungistor Storage" + description: "Distributed storage system" + position: 2 +``` + +**Available Page Parameters:** + +- `src`: Source reference as `collection:page_name` (required for first page in collection) +- `title`: Page title (optional, extracted from markdown if not provided) +- `description`: Page description for metadata +- `slug`: Custom URL slug +- `position`: Manual ordering (auto-incremented if not specified) +- `draft`: Mark page as draft (default: false) +- `hide_title`: Hide the page title in rendering (default: false) +- `path`: Custom path for the page (defaults to category name) +- `category`: Override the current category for this page + +## File Organization + +HeroScript files should be organized with numeric prefixes to control execution order: + +``` +docs/ +├── 0_config.heroscript # Site configuration +├── 1_menu.heroscript # Navigation and footer +├── 2_intro_pages.heroscript # Introduction pages +├── 3_tech_pages.heroscript # Technical documentation +└── 4_api_pages.heroscript # API reference +``` + +**Important:** Files are processed in alphabetical order, so use numeric prefixes (0_, 1_, 2_, etc.) to ensure correct execution sequence. + +## Import External Content ```heroscript !!site.import url: "https://github.com/example/external-docs" - path: "/local/path/to/repo" dest: "external" replace: "PROJECT_NAME:My Project,VERSION:1.0.0" visible: true ``` -### 8. Publishing Destinations +## Publish Destinations ```heroscript !!site.publish path: "/var/www/html/docs" - ssh_name: "production" + ssh_name: "production_server" !!site.publish_dev path: "/tmp/docs-preview" ``` ---- +## Factory Methods -## Common Patterns +### Create or Get a Site -### Pattern 1: Multi-Section Technical Documentation +```v +import incubaid.herolib.web.site -```heroscript -!!site.config - name: "tech_docs" - title: "Technical Documentation" +// Create a new site +mut mysite := site.new(name: 'my_docs')! -!!site.page_category - name: "getting_started" - label: "Getting Started" +// Get an existing site +mut mysite := site.get(name: 'my_docs')! -!!site.page src: "docs:intro" - title: "Introduction" +// Get default site +mut mysite := site.default()! -!!site.page src: "installation" - title: "Installation" +// Check if site exists +if site.exists(name: 'my_docs') { + println('Site exists') +} -!!site.page_category - name: "concepts" - label: "Core Concepts" - -!!site.page src: "concepts:architecture" - title: "Architecture" - -!!site.page src: "components" - title: "Components" - -!!site.page_category - name: "api" - label: "API Reference" - -!!site.page src: "api:rest" - title: "REST API" - -!!site.page src: "graphql" - title: "GraphQL" +// List all sites +sites := site.list() +println(sites) ``` -### Pattern 2: Simple Blog/Knowledge Base +### Using with PlayBook -```heroscript -!!site.config - name: "blog" - title: "Knowledge Base" +```v +import incubaid.herolib.core.playbook +import incubaid.herolib.web.site -!!site.page src: "articles:first_post" - title: "Welcome to Our Blog" +// Create playbook from path +mut plbook := playbook.new(path: '/path/to/heroscripts')! -!!site.page src: "second_post" - title: "Understanding the Basics" +// Process site configuration +site.play(mut plbook)! -!!site.page src: "third_post" - title: "Advanced Techniques" +// Access the configured site +mut mysite := site.get(name: 'my_site')! ``` -### Pattern 3: Project with External Imports +## Data Structures -```heroscript -!!site.config - name: "project_docs" - title: "Project Documentation" +### Site -!!site.import - url: "https://github.com/org/shared-docs" - dest: "shared" - visible: true - -!!site.page_category - name: "product" - label: "Product Guide" - -!!site.page src: "docs:overview" - title: "Overview" - -!!site.page src: "features" - title: "Features" - -!!site.page_category - name: "resources" - label: "Shared Resources" - -!!site.page src: "shared:common" - title: "Common Patterns" +```v +pub struct Site { +pub mut: + pages []Page + sections []Section + siteconfig SiteConfig +} ``` ---- +### Page -## File Organization - -Organize HeroScript files with numeric prefixes to control execution order: - -``` -docs/ -├── 0_config.heroscript -│ └── !!site.config and !!site.config_meta -│ -├── 1_menu.heroscript -│ └── !!site.navbar and !!site.footer -│ -├── 2_pages.heroscript -│ └── !!site.page_category and !!site.page actions -│ -└── 3_publish.heroscript - └── !!site.publish destinations +```v +pub struct Page { +pub mut: + name string // Page identifier + title string // Display title + description string // Page description + draft bool // Draft status + position int // Sort order + hide_title bool // Hide title in rendering + src string // Source as collection:page_name + path string // URL path (without page name) + section_name string // Category/section name + title_nr int // Title numbering level + slug string // Custom URL slug +} ``` -**Why numeric prefixes?** +### Section -Files are processed in alphabetical order. Numeric prefixes ensure: -- Site config runs first -- Navigation menu configures before pages -- Pages build the final structure -- Publishing configured last +```v +pub struct Section { +pub mut: + name string // Internal identifier + position int // Sort order + path string // URL path + label string // Display name +} +``` ---- +## Best Practices -## Processing Order +1. **File Naming**: Use numeric prefixes (0_, 1_, 2_) to control execution order +2. **Collection Reuse**: Specify collection once, then reuse for subsequent pages +3. **Category Organization**: Group related pages under categories for better navigation +4. **Title Extraction**: Let titles be extracted from markdown files when possible +5. **Position Management**: Use automatic positioning unless you need specific ordering +6. **Description**: Always provide descriptions for better SEO and navigation +7. **Draft Status**: Use `draft: true` for work-in-progress pages -The Site module processes HeroScript in this strict order: +## Complete Example -1. Site Configuration -2. Metadata Overrides -3. Imports -4. Navigation -5. Footer -6. Announcement -7. Publishing -8. Pages & Categories +See `examples/web/site/site_example.vsh` for a complete working example. -Each stage depends on previous stages completing successfully. +For a real-world example, check: diff --git a/lib/web/site/siteplay_test.v b/lib/web/site/siteplay_test.v deleted file mode 100644 index 08d41dcc..00000000 --- a/lib/web/site/siteplay_test.v +++ /dev/null @@ -1,445 +0,0 @@ -module site - -import incubaid.herolib.core.playbook -import incubaid.herolib.web.site -import incubaid.herolib.ui.console -import os - -// Big comprehensive HeroScript for testing -const test_heroscript = ' -!!site.config - name: "test_docs" - title: "Test Documentation Site" - description: "A comprehensive test documentation site" - tagline: "Testing everything" - favicon: "img/favicon.png" - image: "img/test-og.png" - copyright: "© 2024 Test Organization" - url: "https://test.example.com" - base_url: "/" - url_home: "/docs" - -!!site.config_meta - title: "Test Docs - Advanced" - image: "img/test-og-alternative.png" - description: "Advanced test documentation" - -!!site.navbar - title: "Test Documentation" - logo_alt: "Test Logo" - logo_src: "img/logo.svg" - logo_src_dark: "img/logo-dark.svg" - -!!site.navbar_item - label: "Getting Started" - to: "intro" - position: "left" - -!!site.navbar_item - label: "API Reference" - to: "api" - position: "left" - -!!site.navbar_item - label: "GitHub" - href: "https://github.com/example/test" - position: "right" - -!!site.navbar_item - label: "Blog" - href: "https://blog.example.com" - position: "right" - -!!site.footer - style: "dark" - -!!site.footer_item - title: "Documentation" - label: "Introduction" - to: "intro" - -!!site.footer_item - title: "Documentation" - label: "Getting Started" - to: "getting-started" - -!!site.footer_item - title: "Documentation" - label: "Advanced Topics" - to: "advanced" - -!!site.footer_item - title: "Community" - label: "Discord" - href: "https://discord.gg/example" - -!!site.footer_item - title: "Community" - label: "Twitter" - href: "https://twitter.com/example" - -!!site.footer_item - title: "Legal" - label: "Privacy Policy" - href: "https://example.com/privacy" - -!!site.footer_item - title: "Legal" - label: "Terms of Service" - href: "https://example.com/terms" - -!!site.announcement - id: "v2-release" - content: "🎉 Version 2.0 is now available! Check out the new features." - background_color: "#1a472a" - text_color: "#fff" - is_closeable: true - -!!site.page_category - name: "getting_started" - label: "Getting Started" - position: 10 - -!!site.page src: "guides:introduction" - title: "Introduction to Test Docs" - description: "Learn what this project is about" - -!!site.page src: "installation" - title: "Installation Guide" - description: "How to install and setup" - -!!site.page src: "quick_start" - title: "Quick Start" - description: "5 minute quick start guide" - -!!site.page_category - name: "concepts" - label: "Core Concepts" - position: 20 - -!!site.page src: "concepts:architecture" - title: "Architecture Overview" - description: "Understanding the system architecture" - -!!site.page src: "components" - title: "Key Components" - description: "Learn about the main components" - -!!site.page src: "workflow" - title: "Typical Workflow" - description: "How to use the system" - -!!site.page_category - name: "api" - label: "API Reference" - position: 30 - -!!site.page src: "api:rest" - title: "REST API" - description: "Complete REST API reference" - -!!site.page src: "graphql" - title: "GraphQL API" - description: "GraphQL API documentation" - -!!site.page src: "webhooks" - title: "Webhooks" - description: "Webhook configuration and examples" - -!!site.page_category - name: "advanced" - label: "Advanced Topics" - position: 40 - -!!site.page src: "advanced:performance" - title: "Performance Optimization" - description: "Tips for optimal performance" - -!!site.page src: "scaling" - title: "Scaling Guide" - description: "How to scale the system" - -!!site.page src: "security" - title: "Security Best Practices" - description: "Security considerations and best practices" - -!!site.page src: "troubleshooting" - title: "Troubleshooting" - description: "Common issues and solutions" - draft: false - -!!site.publish - path: "/var/www/html/docs" - ssh_name: "production-server" - -!!site.publish_dev - path: "/tmp/docs-dev" -' - -fn test_site1() ! { - console.print_header('Site Module Comprehensive Test') - console.lf() - - // ======================================================== - // TEST 1: Create playbook from heroscript - // ======================================================== - console.print_item('TEST 1: Creating playbook from HeroScript') - mut plbook := playbook.new(text: test_heroscript)! - console.print_green('✓ Playbook created successfully') - console.lf() - - // ======================================================== - // TEST 2: Process site configuration - // ======================================================== - console.print_item('TEST 2: Processing site.play()') - site.play(mut plbook)! - console.print_green('✓ Site configuration processed successfully') - console.lf() - - // ======================================================== - // TEST 3: Retrieve site and validate - // ======================================================== - console.print_item('TEST 3: Retrieving configured site') - mut test_site := site.get(name: 'test_docs')! - console.print_green('✓ Site retrieved successfully') - console.lf() - - // ======================================================== - // TEST 4: Validate SiteConfig - // ======================================================== - console.print_header('Validating SiteConfig') - mut config := &test_site.siteconfig - - help_test_string('Site Name', config.name, 'test_docs') - help_test_string('Site Title', config.title, 'Test Documentation Site') - help_test_string('Site Description', config.description, 'A comprehensive test documentation site') - help_test_string('Site Tagline', config.tagline, 'Testing everything') - help_test_string('Copyright', config.copyright, '© 2024 Test Organization') - help_test_string('Base URL', config.base_url, '/') - help_test_string('URL Home', config.url_home, '/docs') - - help_test_string('Meta Title', config.meta_title, 'Test Docs - Advanced') - help_test_string('Meta Image', config.meta_image, 'img/test-og-alternative.png') - - assert config.build_dest.len == 1, 'Should have 1 production build destination' - console.print_green('✓ Production build dest: ${config.build_dest[0].path}') - - assert config.build_dest_dev.len == 1, 'Should have 1 dev build destination' - console.print_green('✓ Dev build dest: ${config.build_dest_dev[0].path}') - - console.lf() - - // ======================================================== - // TEST 5: Validate Menu Configuration - // ======================================================== - console.print_header('Validating Menu Configuration') - mut menu := config.menu - - help_test_string('Menu Title', menu.title, 'Test Documentation') - help_test_string('Menu Logo Alt', menu.logo_alt, 'Test Logo') - help_test_string('Menu Logo Src', menu.logo_src, 'img/logo.svg') - help_test_string('Menu Logo Src Dark', menu.logo_src_dark, 'img/logo-dark.svg') - - assert menu.items.len == 4, 'Should have 4 navbar items, got ${menu.items.len}' - console.print_green('✓ Menu has 4 navbar items') - - // Validate navbar items - help_test_navbar_item(menu.items[0], 'Getting Started', 'intro', '', 'left') - help_test_navbar_item(menu.items[1], 'API Reference', 'api', '', 'left') - help_test_navbar_item(menu.items[2], 'GitHub', '', 'https://github.com/example/test', - 'right') - help_test_navbar_item(menu.items[3], 'Blog', '', 'https://blog.example.com', 'right') - - console.lf() - - // ======================================================== - // TEST 6: Validate Footer Configuration - // ======================================================== - console.print_header('Validating Footer Configuration') - mut footer := config.footer - - help_test_string('Footer Style', footer.style, 'dark') - assert footer.links.len == 3, 'Should have 3 footer link groups, got ${footer.links.len}' - console.print_green('✓ Footer has 3 link groups') - - // Validate footer structure - for link_group in footer.links { - console.print_item('Footer group: "${link_group.title}" has ${link_group.items.len} items') - } - - // Detailed footer validation - mut doc_links := footer.links.filter(it.title == 'Documentation') - assert doc_links.len == 1, 'Should have 1 Documentation link group' - assert doc_links[0].items.len == 3, 'Documentation should have 3 items' - console.print_green('✓ Documentation footer: 3 items') - - mut community_links := footer.links.filter(it.title == 'Community') - assert community_links.len == 1, 'Should have 1 Community link group' - assert community_links[0].items.len == 2, 'Community should have 2 items' - console.print_green('✓ Community footer: 2 items') - - mut legal_links := footer.links.filter(it.title == 'Legal') - assert legal_links.len == 1, 'Should have 1 Legal link group' - assert legal_links[0].items.len == 2, 'Legal should have 2 items' - console.print_green('✓ Legal footer: 2 items') - - console.lf() - - // ======================================================== - // TEST 7: Validate Announcement Bar - // ======================================================== - console.print_header('Validating Announcement Bar') - mut announcement := config.announcement - - help_test_string('Announcement ID', announcement.id, 'v2-release') - help_test_string('Announcement Content', announcement.content, '🎉 Version 2.0 is now available! Check out the new features.') - help_test_string('Announcement BG Color', announcement.background_color, '#1a472a') - help_test_string('Announcement Text Color', announcement.text_color, '#fff') - assert announcement.is_closeable == true, 'Announcement should be closeable' - console.print_green('✓ Announcement bar configured correctly') - - console.lf() - - // ======================================================== - // TEST 8: Validate Pages - // ======================================================== - console.print_header('Validating Pages') - mut pages := test_site.pages.clone() - - assert pages.len == 13, 'Should have 13 pages, got ${pages.len}' - console.print_green('✓ Total pages: ${pages.len}') - - // List and validate pages - mut page_ids := pages.keys() - page_ids.sort() - - for page_id in page_ids { - mut page := pages[page_id] - console.print_debug(' Page: ${page_id} - "${page.title}"') - } - - // Validate specific pages - assert 'guides:introduction' in pages, 'guides:introduction page not found' - console.print_green('✓ Found guides:introduction') - - assert 'concepts:architecture' in pages, 'concepts:architecture page not found' - console.print_green('✓ Found concepts:architecture') - - assert 'api:rest' in pages, 'api:rest page not found' - console.print_green('✓ Found api:rest') - - console.lf() - - // ======================================================== - // TEST 9: Validate Navigation Structure - // ======================================================== - console.print_header('Validating Navigation Structure') - mut sidebar := unsafe { test_site.nav.my_sidebar.clone() } - - console.print_item('Navigation sidebar has ${sidebar.len} items') - - // Count categories - mut category_count := 0 - mut doc_count := 0 - - for item in sidebar { - match item { - site.NavCat { - category_count++ - console.print_debug(' Category: "${item.label}" with ${item.items.len} sub-items') - } - site.NavDoc { - doc_count++ - console.print_debug(' Doc: "${item.label}" (${item.id})') - } - site.NavLink { - console.print_debug(' Link: "${item.label}" -> ${item.href}') - } - } - } - - assert category_count == 4, 'Should have 4 categories, got ${category_count}' - console.print_green('✓ Navigation has 4 categories') - - // Validate category structure - for item in sidebar { - match item { - site.NavCat { - console.print_item('Category: "${item.label}"') - println(' Collapsible: ${item.collapsible}, Collapsed: ${item.collapsed}') - println(' Items: ${item.items.len}') - - // Validate sub-items - for sub_item in item.items { - match sub_item { - site.NavDoc { - println(' - ${sub_item.label} (${sub_item.id})') - } - else { - println(' - Unexpected item type') - } - } - } - } - else {} - } - } - - console.lf() - - // ======================================================== - // TEST 10: Validate Site Factory - // ======================================================== - console.print_header('Validating Site Factory') - - mut all_sites := site.list() - console.print_item('Total sites registered: ${all_sites.len}') - for site_name in all_sites { - console.print_debug(' - ${site_name}') - } - - assert all_sites.contains('test_docs'), 'test_docs should be in sites list' - console.print_green('✓ test_docs found in factory') - - assert site.exists(name: 'test_docs'), 'test_docs should exist' - console.print_green('✓ test_docs verified to exist') - - console.lf() - - // ======================================================== - // FINAL SUMMARY - // ======================================================== - console.print_header('Test Summary') - console.print_green('✓ All tests passed successfully!') - console.print_item('Site Name: ${config.name}') - console.print_item('Pages: ${pages.len}') - console.print_item('Navigation Categories: ${category_count}') - console.print_item('Navbar Items: ${menu.items.len}') - console.print_item('Footer Groups: ${footer.links.len}') - console.print_item('Announcement: Active') - console.print_item('Build Destinations: ${config.build_dest.len} prod, ${config.build_dest_dev.len} dev') - - console.lf() - console.print_green('All validations completed successfully!') -} - -// ============================================================ -// Helper Functions for Testing -// ============================================================ - -fn help_test_string(label string, actual string, expected string) { - if actual == expected { - console.print_green('✓ ${label}: "${actual}"') - } else { - console.print_stderr('✗ ${label}: expected "${expected}", got "${actual}"') - panic('Test failed: ${label}') - } -} - -fn help_test_navbar_item(item MenuItem, label string, to string, href string, position string) { - assert item.label == label, 'Expected label "${label}", got "${item.label}"' - assert item.to == to, 'Expected to "${to}", got "${item.to}"' - assert item.href == href, 'Expected href "${href}", got "${item.href}"' - assert item.position == position, 'Expected position "${position}", got "${item.position}"' - console.print_green('✓ Navbar item: "${label}"') -}