...
This commit is contained in:
@@ -72,8 +72,8 @@ 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)
|
||||
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) ! {
|
||||
|
||||
@@ -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
|
||||
|
||||
30
lib/web/docusaurus/interface_atlas_client.v
Normal file
30
lib/web/docusaurus/interface_atlas_client.v
Normal file
@@ -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) !
|
||||
}
|
||||
@@ -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')
|
||||
)!
|
||||
|
||||
|
||||
536
lib/web/site/ai_instructions.md
Normal file
536
lib/web/site/ai_instructions.md
Normal file
@@ -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.
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
18
lib/web/site/model_site_section.v
Normal file
18
lib/web/site/model_site_section.v
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
// ============================================================
|
||||
// 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')!
|
||||
// Process !!site.config_meta for specific metadata overrides
|
||||
mut meta_action := plbook.ensure_once(filter: 'site.config_meta')!
|
||||
mut p_meta := meta_action.params
|
||||
|
||||
// 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')!
|
||||
}
|
||||
|
||||
config_action.done = true // Mark the action as done
|
||||
meta_action.done = true
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// STEP 3: Configure content imports
|
||||
// ============================================================
|
||||
console.print_item('Step 3: Configuring content imports')
|
||||
play_imports(mut plbook, mut config)!
|
||||
|
||||
// ============================================================
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
135
lib/web/site/play_page.v
Normal file
135
lib/web/site/play_page.v
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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: <https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech>
|
||||
|
||||
@@ -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}"')
|
||||
}
|
||||
Reference in New Issue
Block a user