...
This commit is contained in:
@@ -71,9 +71,9 @@ pub struct DevArgs {
|
|||||||
pub mut:
|
pub mut:
|
||||||
host string = 'localhost'
|
host string = 'localhost'
|
||||||
port int = 3000
|
port int = 3000
|
||||||
open bool = true // whether to open the browser automatically
|
open bool = true // whether to open the browser automatically
|
||||||
watch_changes bool // whether to watch for changes in docs and rebuild automatically
|
watch_changes bool = false // 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)
|
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) ! {
|
pub fn (mut s DocSite) open(args DevArgs) ! {
|
||||||
|
|||||||
@@ -142,10 +142,6 @@ fn (mut generator SiteGenerator) page_generate(args_ Page) ! {
|
|||||||
|
|
||||||
pagefile.write(c)!
|
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.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}")!
|
generator.error("Couldn't copy images for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")!
|
||||||
return
|
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
|
module docusaurus
|
||||||
|
|
||||||
import incubaid.herolib.core.playbook { PlayBook }
|
import incubaid.herolib.core.playbook { PlayBook }
|
||||||
import os
|
|
||||||
|
|
||||||
pub fn play(mut plbook PlayBook) ! {
|
pub fn play(mut plbook PlayBook) ! {
|
||||||
if !plbook.exists(filter: 'docusaurus.') {
|
if !plbook.exists(filter: 'docusaurus.') {
|
||||||
@@ -18,7 +17,7 @@ pub fn play(mut plbook PlayBook) ! {
|
|||||||
reset: param_define.get_default_false('reset')
|
reset: param_define.get_default_false('reset')
|
||||||
template_update: param_define.get_default_false('template_update')
|
template_update: param_define.get_default_false('template_update')
|
||||||
install: param_define.get_default_false('install')
|
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')
|
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
|
import incubaid.herolib.core.texttools
|
||||||
|
|
||||||
__global (
|
__global (
|
||||||
mywebsites map[string]&Site
|
websites map[string]&Site
|
||||||
)
|
)
|
||||||
|
|
||||||
@[params]
|
@[params]
|
||||||
@@ -15,13 +15,7 @@ pub mut:
|
|||||||
pub fn new(args FactoryArgs) !&Site {
|
pub fn new(args FactoryArgs) !&Site {
|
||||||
name := texttools.name_fix(args.name)
|
name := texttools.name_fix(args.name)
|
||||||
|
|
||||||
// Check if a site with this name already exists
|
websites[name] = &Site{
|
||||||
if name in mywebsites {
|
|
||||||
// Return the existing site instead of creating a new one
|
|
||||||
return get(name: name)!
|
|
||||||
}
|
|
||||||
|
|
||||||
mywebsites[name] = &Site{
|
|
||||||
siteconfig: SiteConfig{
|
siteconfig: SiteConfig{
|
||||||
name: name
|
name: name
|
||||||
}
|
}
|
||||||
@@ -31,17 +25,18 @@ pub fn new(args FactoryArgs) !&Site {
|
|||||||
|
|
||||||
pub fn get(args FactoryArgs) !&Site {
|
pub fn get(args FactoryArgs) !&Site {
|
||||||
name := texttools.name_fix(args.name)
|
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
|
return sc
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exists(args FactoryArgs) bool {
|
pub fn exists(args FactoryArgs) bool {
|
||||||
name := texttools.name_fix(args.name)
|
name := texttools.name_fix(args.name)
|
||||||
return name in mywebsites
|
mut sc := websites[name] or { return false }
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default() !&Site {
|
pub fn default() !&Site {
|
||||||
if mywebsites.len == 0 {
|
if websites.len == 0 {
|
||||||
return new(name: 'default')!
|
return new(name: 'default')!
|
||||||
}
|
}
|
||||||
return get()!
|
return get()!
|
||||||
@@ -49,5 +44,5 @@ pub fn default() !&Site {
|
|||||||
|
|
||||||
// list returns all site names that have been created
|
// list returns all site names that have been created
|
||||||
pub fn list() []string {
|
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
|
module site
|
||||||
|
|
||||||
// Page represents a single documentation page
|
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
pub mut:
|
pub mut:
|
||||||
id string // Unique identifier: "collection:page_name"
|
name string
|
||||||
title string // Display title (optional, extracted from markdown if empty)
|
title string
|
||||||
description string // Brief description for metadata
|
description string
|
||||||
draft bool // Mark as draft (hidden from navigation)
|
draft bool
|
||||||
hide_title bool // Hide the title when rendering
|
position int
|
||||||
src string // Source reference (same as id in this format)
|
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.playbook { PlayBook }
|
||||||
import incubaid.herolib.core.texttools
|
import incubaid.herolib.core.texttools
|
||||||
import time
|
import time
|
||||||
import incubaid.herolib.ui.console
|
|
||||||
|
|
||||||
// Main entry point for processing site HeroScript
|
|
||||||
pub fn play(mut plbook PlayBook) ! {
|
pub fn play(mut plbook PlayBook) ! {
|
||||||
if !plbook.exists(filter: 'site.') {
|
if !plbook.exists(filter: 'site.') {
|
||||||
return
|
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 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 website := new(name: name)!
|
||||||
mut config := &website.siteconfig
|
mut config := &website.siteconfig
|
||||||
|
|
||||||
// Load core configuration
|
|
||||||
config.name = texttools.name_fix(name)
|
config.name = texttools.name_fix(name)
|
||||||
config.title = p.get_default('title', 'Documentation Site')!
|
config.title = p.get_default('title', 'Documentation Site')!
|
||||||
config.description = p.get_default('description', 'Comprehensive documentation built with Docusaurus.')!
|
config.description = p.get_default('description', 'Comprehensive documentation built with Docusaurus.')!
|
||||||
config.tagline = p.get_default('tagline', 'Your awesome documentation')!
|
config.tagline = p.get_default('tagline', 'Your awesome documentation')!
|
||||||
config.favicon = p.get_default('favicon', 'img/favicon.png')!
|
config.favicon = p.get_default('favicon', 'img/favicon.png')!
|
||||||
config.image = p.get_default('image', 'img/tf_graph.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.url = p.get_default('url', '')!
|
||||||
config.base_url = p.get_default('base_url', '/')!
|
config.base_url = p.get_default('base_url', '/')!
|
||||||
config.url_home = p.get_default('url_home', '')!
|
config.url_home = p.get_default('url_home', '')!
|
||||||
|
|
||||||
config_action.done = true
|
// Process !!site.config_meta for specific metadata overrides
|
||||||
|
mut meta_action := plbook.ensure_once(filter: 'site.config_meta')!
|
||||||
|
mut p_meta := meta_action.params
|
||||||
|
|
||||||
// ============================================================
|
// If 'title' is present in site.config_meta, it overrides. Otherwise, meta_title remains empty or uses site.config.title logic in docusaurus model.
|
||||||
// STEP 2: Apply optional metadata overrides
|
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.
|
||||||
console.print_item('Step 2: Applying metadata overrides')
|
config.meta_image = p_meta.get_default('image', config.image)!
|
||||||
if plbook.exists_once(filter: 'site.config_meta') {
|
// If 'description' is present in site.config_meta, it overrides the main description
|
||||||
mut meta_action := plbook.get(filter: 'site.config_meta')!
|
if p_meta.exists('description') {
|
||||||
mut p_meta := meta_action.params
|
config.description = p_meta.get('description')!
|
||||||
|
|
||||||
config.meta_title = p_meta.get_default('title', config.title)!
|
|
||||||
config.meta_image = p_meta.get_default('image', config.image)!
|
|
||||||
if p_meta.exists('description') {
|
|
||||||
config.description = p_meta.get('description')!
|
|
||||||
}
|
|
||||||
|
|
||||||
meta_action.done = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
config_action.done = true // Mark the action as done
|
||||||
// STEP 3: Configure content imports
|
meta_action.done = true
|
||||||
// ============================================================
|
|
||||||
console.print_item('Step 3: Configuring content imports')
|
|
||||||
play_imports(mut plbook, mut config)!
|
|
||||||
|
|
||||||
// ============================================================
|
play_import(mut plbook, mut config)!
|
||||||
// STEP 4: Configure navigation menu
|
play_menu(mut plbook, mut config)!
|
||||||
// ============================================================
|
|
||||||
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_footer(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)!
|
play_announcement(mut plbook, mut config)!
|
||||||
|
play_publish(mut plbook, mut config)!
|
||||||
// ============================================================
|
play_publish_dev(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_pages(mut plbook, mut website)!
|
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.
|
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
|
## 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
|
```v
|
||||||
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
|
#!/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.web.site
|
||||||
import incubaid.herolib.ui.console
|
import incubaid.herolib.core.playcmds
|
||||||
|
|
||||||
// Process HeroScript file
|
// Clone or use existing repository with HeroScript files
|
||||||
mut plbook := playbook.new(path: './site_config.heroscript')!
|
mysitepath := gittools.path(
|
||||||
|
git_url: 'https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech'
|
||||||
|
git_pull: true
|
||||||
|
)!
|
||||||
|
|
||||||
// Execute site configuration
|
// Process all HeroScript files in the path
|
||||||
site.play(mut plbook)!
|
playcmds.run(heroscript_path: mysitepath.path)!
|
||||||
|
|
||||||
// Access the configured site
|
// Get the configured site
|
||||||
mut mysite := site.get(name: 'my_docs')!
|
mut mysite := site.get(name: 'tfgrid_tech')!
|
||||||
|
println(mysite)
|
||||||
// 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')
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
## HeroScript Syntax
|
||||||
|
|
||||||
### 1. Site Configuration (Required)
|
### Basic Configuration
|
||||||
|
|
||||||
```heroscript
|
```heroscript
|
||||||
!!site.config
|
!!site.config
|
||||||
@@ -91,49 +51,20 @@ A logical group of pages. Pages reuse the collection once specified.
|
|||||||
copyright: "© 2024 My Organization"
|
copyright: "© 2024 My Organization"
|
||||||
url: "https://docs.example.com"
|
url: "https://docs.example.com"
|
||||||
base_url: "/"
|
base_url: "/"
|
||||||
url_home: "/docs"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters:**
|
### Navigation Menu
|
||||||
- `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
|
|
||||||
|
|
||||||
```heroscript
|
```heroscript
|
||||||
!!site.navbar
|
!!site.navbar
|
||||||
title: "My Documentation"
|
title: "My Site"
|
||||||
logo_alt: "Site Logo"
|
logo_alt: "Site Logo"
|
||||||
logo_src: "img/logo.svg"
|
logo_src: "img/logo.svg"
|
||||||
logo_src_dark: "img/logo-dark.svg"
|
logo_src_dark: "img/logo-dark.svg"
|
||||||
|
|
||||||
!!site.navbar_item
|
!!site.navbar_item
|
||||||
label: "Documentation"
|
label: "Documentation"
|
||||||
to: "intro"
|
to: "docs/intro"
|
||||||
position: "left"
|
|
||||||
|
|
||||||
!!site.navbar_item
|
|
||||||
label: "API Reference"
|
|
||||||
to: "docs/api"
|
|
||||||
position: "left"
|
position: "left"
|
||||||
|
|
||||||
!!site.navbar_item
|
!!site.navbar_item
|
||||||
@@ -142,13 +73,7 @@ Overrides specific metadata for SEO without changing core config.
|
|||||||
position: "right"
|
position: "right"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters:**
|
### Footer Configuration
|
||||||
- `label` - Display text (required)
|
|
||||||
- `to` - Internal link
|
|
||||||
- `href` - External URL
|
|
||||||
- `position` - "left" or "right" in navbar
|
|
||||||
|
|
||||||
### 4. Footer Configuration
|
|
||||||
|
|
||||||
```heroscript
|
```heroscript
|
||||||
!!site.footer
|
!!site.footer
|
||||||
@@ -162,234 +87,242 @@ Overrides specific metadata for SEO without changing core config.
|
|||||||
!!site.footer_item
|
!!site.footer_item
|
||||||
title: "Docs"
|
title: "Docs"
|
||||||
label: "Getting Started"
|
label: "Getting Started"
|
||||||
to: "getting-started"
|
href: "https://docs.example.com/getting-started"
|
||||||
|
|
||||||
!!site.footer_item
|
!!site.footer_item
|
||||||
title: "Community"
|
title: "Community"
|
||||||
label: "Discord"
|
label: "Discord"
|
||||||
href: "https://discord.gg/example"
|
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
|
```heroscript
|
||||||
!!site.announcement
|
!!site.page src: "mycelium_tech:introduction"
|
||||||
id: "new-release"
|
description: "Introduction to ThreeFold Technology"
|
||||||
content: "🎉 Version 2.0 is now available!"
|
slug: "/"
|
||||||
background_color: "#20232a"
|
|
||||||
text_color: "#fff"
|
!!site.page src: "vision"
|
||||||
is_closeable: true
|
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
|
### Example 2: Pages with Categories
|
||||||
!!site.page src: "guides:introduction"
|
|
||||||
title: "Getting Started"
|
|
||||||
description: "Introduction to the platform"
|
|
||||||
|
|
||||||
!!site.page src: "installation"
|
Categories (sections) help organize pages into logical groups with their own navigation structure.
|
||||||
title: "Installation"
|
|
||||||
|
|
||||||
!!site.page src: "configuration"
|
|
||||||
title: "Configuration"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Advanced: Pages With Categories
|
|
||||||
|
|
||||||
```heroscript
|
```heroscript
|
||||||
!!site.page_category
|
!!site.page_category
|
||||||
name: "basics"
|
name: "first_principle_thinking"
|
||||||
label: "Getting Started"
|
label: "First Principle Thinking"
|
||||||
|
|
||||||
!!site.page src: "guides:introduction"
|
!!site.page src: "first_principle_thinking:hardware_badly_used"
|
||||||
title: "Introduction"
|
description: "Hardware is not used properly, why it is important to understand hardware"
|
||||||
description: "Learn the basics"
|
|
||||||
|
|
||||||
!!site.page src: "installation"
|
!!site.page src: "internet_risk"
|
||||||
title: "Installation"
|
description: "Internet risk, how to mitigate it, and why it is important"
|
||||||
|
|
||||||
!!site.page src: "configuration"
|
!!site.page src: "onion_analogy"
|
||||||
title: "Configuration"
|
description: "Compare onion with a computer, layers of abstraction"
|
||||||
|
|
||||||
!!site.page_category
|
|
||||||
name: "advanced"
|
|
||||||
label: "Advanced Topics"
|
|
||||||
|
|
||||||
!!site.page src: "advanced:performance"
|
|
||||||
title: "Performance Tuning"
|
|
||||||
|
|
||||||
!!site.page src: "scaling"
|
|
||||||
title: "Scaling Guide"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Page Parameters:**
|
**Key Points:**
|
||||||
- `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)
|
|
||||||
|
|
||||||
### 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
|
```heroscript
|
||||||
!!site.import
|
!!site.import
|
||||||
url: "https://github.com/example/external-docs"
|
url: "https://github.com/example/external-docs"
|
||||||
path: "/local/path/to/repo"
|
|
||||||
dest: "external"
|
dest: "external"
|
||||||
replace: "PROJECT_NAME:My Project,VERSION:1.0.0"
|
replace: "PROJECT_NAME:My Project,VERSION:1.0.0"
|
||||||
visible: true
|
visible: true
|
||||||
```
|
```
|
||||||
|
|
||||||
### 8. Publishing Destinations
|
## Publish Destinations
|
||||||
|
|
||||||
```heroscript
|
```heroscript
|
||||||
!!site.publish
|
!!site.publish
|
||||||
path: "/var/www/html/docs"
|
path: "/var/www/html/docs"
|
||||||
ssh_name: "production"
|
ssh_name: "production_server"
|
||||||
|
|
||||||
!!site.publish_dev
|
!!site.publish_dev
|
||||||
path: "/tmp/docs-preview"
|
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
|
// Create a new site
|
||||||
!!site.config
|
mut mysite := site.new(name: 'my_docs')!
|
||||||
name: "tech_docs"
|
|
||||||
title: "Technical Documentation"
|
|
||||||
|
|
||||||
!!site.page_category
|
// Get an existing site
|
||||||
name: "getting_started"
|
mut mysite := site.get(name: 'my_docs')!
|
||||||
label: "Getting Started"
|
|
||||||
|
|
||||||
!!site.page src: "docs:intro"
|
// Get default site
|
||||||
title: "Introduction"
|
mut mysite := site.default()!
|
||||||
|
|
||||||
!!site.page src: "installation"
|
// Check if site exists
|
||||||
title: "Installation"
|
if site.exists(name: 'my_docs') {
|
||||||
|
println('Site exists')
|
||||||
|
}
|
||||||
|
|
||||||
!!site.page_category
|
// List all sites
|
||||||
name: "concepts"
|
sites := site.list()
|
||||||
label: "Core Concepts"
|
println(sites)
|
||||||
|
|
||||||
!!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"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pattern 2: Simple Blog/Knowledge Base
|
### Using with PlayBook
|
||||||
|
|
||||||
```heroscript
|
```v
|
||||||
!!site.config
|
import incubaid.herolib.core.playbook
|
||||||
name: "blog"
|
import incubaid.herolib.web.site
|
||||||
title: "Knowledge Base"
|
|
||||||
|
|
||||||
!!site.page src: "articles:first_post"
|
// Create playbook from path
|
||||||
title: "Welcome to Our Blog"
|
mut plbook := playbook.new(path: '/path/to/heroscripts')!
|
||||||
|
|
||||||
!!site.page src: "second_post"
|
// Process site configuration
|
||||||
title: "Understanding the Basics"
|
site.play(mut plbook)!
|
||||||
|
|
||||||
!!site.page src: "third_post"
|
// Access the configured site
|
||||||
title: "Advanced Techniques"
|
mut mysite := site.get(name: 'my_site')!
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pattern 3: Project with External Imports
|
## Data Structures
|
||||||
|
|
||||||
```heroscript
|
### Site
|
||||||
!!site.config
|
|
||||||
name: "project_docs"
|
|
||||||
title: "Project Documentation"
|
|
||||||
|
|
||||||
!!site.import
|
```v
|
||||||
url: "https://github.com/org/shared-docs"
|
pub struct Site {
|
||||||
dest: "shared"
|
pub mut:
|
||||||
visible: true
|
pages []Page
|
||||||
|
sections []Section
|
||||||
!!site.page_category
|
siteconfig SiteConfig
|
||||||
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"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### Page
|
||||||
|
|
||||||
## File Organization
|
```v
|
||||||
|
pub struct Page {
|
||||||
Organize HeroScript files with numeric prefixes to control execution order:
|
pub mut:
|
||||||
|
name string // Page identifier
|
||||||
```
|
title string // Display title
|
||||||
docs/
|
description string // Page description
|
||||||
├── 0_config.heroscript
|
draft bool // Draft status
|
||||||
│ └── !!site.config and !!site.config_meta
|
position int // Sort order
|
||||||
│
|
hide_title bool // Hide title in rendering
|
||||||
├── 1_menu.heroscript
|
src string // Source as collection:page_name
|
||||||
│ └── !!site.navbar and !!site.footer
|
path string // URL path (without page name)
|
||||||
│
|
section_name string // Category/section name
|
||||||
├── 2_pages.heroscript
|
title_nr int // Title numbering level
|
||||||
│ └── !!site.page_category and !!site.page actions
|
slug string // Custom URL slug
|
||||||
│
|
}
|
||||||
└── 3_publish.heroscript
|
|
||||||
└── !!site.publish destinations
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Why numeric prefixes?**
|
### Section
|
||||||
|
|
||||||
Files are processed in alphabetical order. Numeric prefixes ensure:
|
```v
|
||||||
- Site config runs first
|
pub struct Section {
|
||||||
- Navigation menu configures before pages
|
pub mut:
|
||||||
- Pages build the final structure
|
name string // Internal identifier
|
||||||
- Publishing configured last
|
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
|
See `examples/web/site/site_example.vsh` for a complete working example.
|
||||||
2. Metadata Overrides
|
|
||||||
3. Imports
|
|
||||||
4. Navigation
|
|
||||||
5. Footer
|
|
||||||
6. Announcement
|
|
||||||
7. Publishing
|
|
||||||
8. Pages & Categories
|
|
||||||
|
|
||||||
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