...
This commit is contained in:
58
lib/web/doctree/meta/factory.v
Normal file
58
lib/web/doctree/meta/factory.v
Normal file
@@ -0,0 +1,58 @@
|
||||
module meta
|
||||
|
||||
import incubaid.herolib.core.texttools
|
||||
|
||||
__global (
|
||||
sites_global map[string]&Site
|
||||
)
|
||||
|
||||
@[params]
|
||||
pub struct FactoryArgs {
|
||||
pub mut:
|
||||
name string = 'default'
|
||||
}
|
||||
|
||||
pub fn new(args FactoryArgs) !&Site {
|
||||
name := texttools.name_fix(args.name)
|
||||
|
||||
// Check if a site with this name already exists
|
||||
if name in sites_global {
|
||||
// Return the existing site instead of creating a new one
|
||||
return get(name: name)!
|
||||
}
|
||||
|
||||
mut site := Site{
|
||||
nav: SideBar{}
|
||||
siteconfig: SiteConfig{
|
||||
name: name
|
||||
}
|
||||
}
|
||||
sites_global[name] = &site
|
||||
return get(name: name)!
|
||||
}
|
||||
|
||||
pub fn get(args FactoryArgs) !&Site {
|
||||
name := texttools.name_fix(args.name)
|
||||
// mut sc := sites_global[name] or { return error('siteconfig with name "${name}" does not exist') }
|
||||
return sites_global[name] or {
|
||||
print_backtrace()
|
||||
return error('could not get site with name:${name}')
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exists(args FactoryArgs) bool {
|
||||
name := texttools.name_fix(args.name)
|
||||
return name in sites_global
|
||||
}
|
||||
|
||||
pub fn default() !&Site {
|
||||
if sites_global.len == 0 {
|
||||
return new(name: 'default')!
|
||||
}
|
||||
return get()!
|
||||
}
|
||||
|
||||
// list returns all site names that have been created
|
||||
pub fn list() []string {
|
||||
return sites_global.keys()
|
||||
}
|
||||
39
lib/web/doctree/meta/model_page.v
Normal file
39
lib/web/doctree/meta/model_page.v
Normal file
@@ -0,0 +1,39 @@
|
||||
module meta
|
||||
|
||||
import incubaid.herolib.data.doctree.client as doctree_client
|
||||
import incubaid.herolib.data.markdown.tools as markdowntools
|
||||
|
||||
|
||||
// Page represents a single documentation page
|
||||
pub struct Page {
|
||||
pub mut:
|
||||
id string // Unique identifier: "collection:page_name"
|
||||
title string // Display title (optional, extracted from markdown if empty)
|
||||
description string // Brief description for metadata
|
||||
questions []Question
|
||||
}
|
||||
|
||||
pub struct Question {
|
||||
pub mut:
|
||||
question string
|
||||
answer string
|
||||
}
|
||||
|
||||
pub fn (mut p Page) content(client doctree_client.AtlasClient) !string {
|
||||
mut c := client.get_page_content(p.id)!
|
||||
|
||||
if p.title =="" {
|
||||
p.title = markdowntools.extract_title(c)
|
||||
}
|
||||
//TODO in future should do AI
|
||||
if p.description =="" {
|
||||
p.description = p.title
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
36
lib/web/doctree/meta/model_sidebar.v
Normal file
36
lib/web/doctree/meta/model_sidebar.v
Normal file
@@ -0,0 +1,36 @@
|
||||
module meta
|
||||
|
||||
import json
|
||||
|
||||
// ============================================================================
|
||||
// Sidebar Navigation Models (Domain Types)
|
||||
// ============================================================================
|
||||
|
||||
pub struct SideBar {
|
||||
pub mut:
|
||||
my_sidebar []NavItem
|
||||
}
|
||||
|
||||
pub type NavItem = NavDoc | NavCat | NavLink
|
||||
|
||||
pub struct NavDoc {
|
||||
pub:
|
||||
id string
|
||||
label string
|
||||
hide_title bool
|
||||
}
|
||||
|
||||
pub struct NavCat {
|
||||
pub mut:
|
||||
label string
|
||||
collapsible bool = true
|
||||
collapsed bool
|
||||
items []NavItem
|
||||
}
|
||||
|
||||
pub struct NavLink {
|
||||
pub:
|
||||
label string
|
||||
href string
|
||||
description string
|
||||
}
|
||||
9
lib/web/doctree/meta/model_site.v
Normal file
9
lib/web/doctree/meta/model_site.v
Normal file
@@ -0,0 +1,9 @@
|
||||
module meta
|
||||
|
||||
@[heap]
|
||||
pub struct Site {
|
||||
pub mut:
|
||||
pages map[string]Page // key: "collection:page_name"
|
||||
nav SideBar // Navigation sidebar configuration
|
||||
siteconfig SiteConfig // Full site configuration
|
||||
}
|
||||
97
lib/web/doctree/meta/model_siteconfig.v
Normal file
97
lib/web/doctree/meta/model_siteconfig.v
Normal file
@@ -0,0 +1,97 @@
|
||||
module meta
|
||||
|
||||
import os
|
||||
// Combined config structure
|
||||
|
||||
@[heap]
|
||||
pub struct SiteConfig {
|
||||
pub mut:
|
||||
name string
|
||||
title string = 'My Documentation Site' // General site title
|
||||
description string // General site description, can be used for meta if meta_description not set
|
||||
tagline string
|
||||
favicon string = 'img/favicon.png'
|
||||
image string = 'img/tf_graph.png' // General site image, can be used for meta if meta_image not set
|
||||
copyright string = 'someone'
|
||||
footer Footer
|
||||
menu Menu
|
||||
imports []ImportItem
|
||||
|
||||
// New fields for Docusaurus compatibility
|
||||
url string // The main URL of the site (from !!site.config url:)
|
||||
base_url string // The base URL for Docusaurus (from !!site.config base_url:)
|
||||
url_home string // The home page path relative to base_url (from !!site.config url_home:)
|
||||
|
||||
meta_title string // Specific title for SEO metadata (from !!site.config_meta title:)
|
||||
meta_image string // Specific image for SEO metadata (og:image) (from !!site.config_meta image:)
|
||||
|
||||
build_dest []BuildDest // Production build destinations (from !!site.build_dest)
|
||||
build_dest_dev []BuildDest // Development build destinations (from !!site.build_dest_dev)
|
||||
|
||||
announcement AnnouncementBar // Announcement bar configuration (from !!site.announcement)
|
||||
}
|
||||
|
||||
// Announcement bar config structure
|
||||
pub struct AnnouncementBar {
|
||||
pub mut:
|
||||
// id string @[json: 'id']
|
||||
content string @[json: 'content']
|
||||
background_color string @[json: 'backgroundColor']
|
||||
text_color string @[json: 'textColor']
|
||||
is_closeable bool @[json: 'isCloseable']
|
||||
}
|
||||
|
||||
// Footer config structures
|
||||
pub struct FooterItem {
|
||||
pub mut:
|
||||
label string
|
||||
to string
|
||||
href string
|
||||
}
|
||||
|
||||
pub struct FooterLink {
|
||||
pub mut:
|
||||
title string
|
||||
items []FooterItem
|
||||
}
|
||||
|
||||
pub struct Footer {
|
||||
pub mut:
|
||||
style string = 'dark'
|
||||
links []FooterLink
|
||||
}
|
||||
|
||||
// menu config structures
|
||||
pub struct MenuItem {
|
||||
pub mut:
|
||||
href string
|
||||
to string
|
||||
label string
|
||||
position string
|
||||
}
|
||||
|
||||
pub struct Menu {
|
||||
pub mut:
|
||||
title string
|
||||
items []MenuItem
|
||||
logo_alt string @[json: 'logoAlt']
|
||||
logo_src string @[json: 'logoSrc']
|
||||
logo_src_dark string @[json: 'logoSrcDark']
|
||||
}
|
||||
|
||||
pub struct BuildDest {
|
||||
pub mut:
|
||||
path string
|
||||
ssh_name string
|
||||
}
|
||||
|
||||
// is to import one docusaurus site into another, can be used to e.g. import static parts from one location into the build one we are building
|
||||
pub struct ImportItem {
|
||||
pub mut:
|
||||
name string // will normally be empty
|
||||
url string // http git url can be to specific path
|
||||
path string
|
||||
dest string // location in the docs folder of the place where we will build the documentation site e.g. docusaurus
|
||||
replace map[string]string // will replace ${NAME} in the imported content
|
||||
visible bool = true
|
||||
}
|
||||
96
lib/web/doctree/meta/play.v
Normal file
96
lib/web/doctree/meta/play.v
Normal file
@@ -0,0 +1,96 @@
|
||||
module meta
|
||||
|
||||
import os
|
||||
import incubaid.herolib.core.playbook { PlayBook }
|
||||
import incubaid.herolib.core.texttools
|
||||
import time
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
// Main entry point for processing site HeroScript
|
||||
pub fn play(mut plbook PlayBook) ! {
|
||||
if !plbook.exists(filter: 'site.') {
|
||||
return
|
||||
}
|
||||
|
||||
console.print_header('Processing Site Configuration')
|
||||
|
||||
// ============================================================
|
||||
// STEP 1: Initialize core site configuration
|
||||
// ============================================================
|
||||
console.print_item('Step 1: Loading site configuration')
|
||||
mut config_action := plbook.ensure_once(filter: 'site.config')!
|
||||
mut p := config_action.params
|
||||
|
||||
name := p.get_default('name', 'default')!
|
||||
mut website := new(name: name)!
|
||||
mut config := &website.siteconfig
|
||||
|
||||
// Load core configuration
|
||||
config.name = texttools.name_fix(name)
|
||||
config.title = p.get_default('title', 'Documentation Site')!
|
||||
config.description = p.get_default('description', 'Comprehensive documentation built with Docusaurus.')!
|
||||
config.tagline = p.get_default('tagline', 'Your awesome documentation')!
|
||||
config.favicon = p.get_default('favicon', 'img/favicon.png')!
|
||||
config.image = p.get_default('image', 'img/tf_graph.png')!
|
||||
config.copyright = p.get_default('copyright', '© ${time.now().year} Example Organization')!
|
||||
config.url = p.get_default('url', '')!
|
||||
config.base_url = p.get_default('base_url', '/')!
|
||||
config.url_home = p.get_default('url_home', '')!
|
||||
|
||||
config_action.done = true
|
||||
|
||||
// ============================================================
|
||||
// STEP 2: Apply optional metadata overrides
|
||||
// ============================================================
|
||||
console.print_item('Step 2: Applying metadata overrides')
|
||||
if plbook.exists_once(filter: 'site.config_meta') {
|
||||
mut meta_action := plbook.get(filter: 'site.config_meta')!
|
||||
mut p_meta := meta_action.params
|
||||
|
||||
config.meta_title = p_meta.get_default('title', config.title)!
|
||||
config.meta_image = p_meta.get_default('image', config.image)!
|
||||
if p_meta.exists('description') {
|
||||
config.description = p_meta.get('description')!
|
||||
}
|
||||
|
||||
meta_action.done = true
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// STEP 3: Configure content imports
|
||||
// ============================================================
|
||||
console.print_item('Step 3: Configuring content imports')
|
||||
play_imports(mut plbook, mut config)!
|
||||
|
||||
// ============================================================
|
||||
// STEP 4: Configure navigation menu
|
||||
// ============================================================
|
||||
console.print_item('Step 4: Configuring navigation menu')
|
||||
play_navbar(mut plbook, mut config)!
|
||||
|
||||
// ============================================================
|
||||
// STEP 5: Configure footer
|
||||
// ============================================================
|
||||
console.print_item('Step 5: Configuring footer')
|
||||
play_footer(mut plbook, mut config)!
|
||||
|
||||
// ============================================================
|
||||
// STEP 6: Configure announcement bar (optional)
|
||||
// ============================================================
|
||||
console.print_item('Step 6: Configuring announcement bar (if present)')
|
||||
play_announcement(mut plbook, mut config)!
|
||||
|
||||
// ============================================================
|
||||
// STEP 7: Configure publish destinations
|
||||
// ============================================================
|
||||
console.print_item('Step 7: Configuring publish destinations')
|
||||
play_publishing(mut plbook, mut config)!
|
||||
|
||||
// ============================================================
|
||||
// STEP 8: Build pages and navigation structure
|
||||
// ============================================================
|
||||
console.print_item('Step 8: Processing pages and building navigation')
|
||||
play_pages(mut plbook, mut website)!
|
||||
|
||||
console.print_green('Site configuration complete')
|
||||
}
|
||||
34
lib/web/doctree/meta/play_announcement.v
Normal file
34
lib/web/doctree/meta/play_announcement.v
Normal file
@@ -0,0 +1,34 @@
|
||||
module meta
|
||||
|
||||
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('id')!
|
||||
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
|
||||
}
|
||||
}
|
||||
62
lib/web/doctree/meta/play_footer.v
Normal file
62
lib/web/doctree/meta/play_footer.v
Normal file
@@ -0,0 +1,62 @@
|
||||
module meta
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
51
lib/web/doctree/meta/play_imports.v
Normal file
51
lib/web/doctree/meta/play_imports.v
Normal file
@@ -0,0 +1,51 @@
|
||||
module meta
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
60
lib/web/doctree/meta/play_navbar.v
Normal file
60
lib/web/doctree/meta/play_navbar.v
Normal file
@@ -0,0 +1,60 @@
|
||||
module meta
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
193
lib/web/doctree/meta/play_pages.v
Normal file
193
lib/web/doctree/meta/play_pages.v
Normal file
@@ -0,0 +1,193 @@
|
||||
module meta
|
||||
|
||||
import os
|
||||
import incubaid.herolib.core.playbook { PlayBook }
|
||||
import incubaid.herolib.core.texttools
|
||||
import time
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 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)
|
||||
|
||||
// label is empty when not specified
|
||||
mut label := p.get_default('label', "")!
|
||||
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 with human-readable label
|
||||
// nav_label := page_title.len
|
||||
nav_doc := NavDoc{
|
||||
id: page.id
|
||||
label: page.title
|
||||
hide_title: page.hide_title
|
||||
}
|
||||
|
||||
// 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')
|
||||
}
|
||||
46
lib/web/doctree/meta/play_publish.v
Normal file
46
lib/web/doctree/meta/play_publish.v
Normal file
@@ -0,0 +1,46 @@
|
||||
module meta
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
557
lib/web/doctree/meta/readme.md
Normal file
557
lib/web/doctree/meta/readme.md
Normal file
@@ -0,0 +1,557 @@
|
||||
# Site Module
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Minimal HeroScript Example
|
||||
|
||||
```heroscript
|
||||
!!site.config
|
||||
name: "my_docs"
|
||||
title: "My Documentation"
|
||||
|
||||
!!site.page src: "docs:introduction"
|
||||
title: "Getting Started"
|
||||
|
||||
!!site.page src: "setup"
|
||||
title: "Installation"
|
||||
```
|
||||
|
||||
### Processing with V Code
|
||||
|
||||
```v
|
||||
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import incubaid.herolib.core.playbook
|
||||
import incubaid.herolib.web.site
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
// Process HeroScript file
|
||||
mut plbook := playbook.new(path: './site_config.heroscript')!
|
||||
|
||||
// Execute site configuration
|
||||
site.play(mut plbook)!
|
||||
|
||||
// Access the configured site
|
||||
mut mysite := site.get(name: 'my_docs')!
|
||||
|
||||
// Print available pages
|
||||
for page_id, page in mysite.pages {
|
||||
console.print_item('Page: ${page_id} - "${page.title}"')
|
||||
}
|
||||
|
||||
println('Site has ${mysite.pages.len} pages')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Site Factory
|
||||
|
||||
Factory functions to create and retrieve site instances:
|
||||
|
||||
```v
|
||||
// Create a new site
|
||||
mut mysite := site.new(name: 'my_docs')!
|
||||
|
||||
// Get existing site
|
||||
mut mysite := site.get(name: 'my_docs')!
|
||||
|
||||
// Check if site exists
|
||||
if site.exists(name: 'my_docs') {
|
||||
println('Site exists')
|
||||
}
|
||||
|
||||
// Get all site names
|
||||
site_names := site.list() // Returns []string
|
||||
|
||||
// Get default site (creates if needed)
|
||||
mut default := site.default()!
|
||||
```
|
||||
|
||||
### Site Object Structure
|
||||
|
||||
```v
|
||||
pub struct Site {
|
||||
pub mut:
|
||||
pages map[string]Page // key: "collection:page_name"
|
||||
nav NavConfig // Navigation sidebar
|
||||
siteconfig SiteConfig // Full configuration
|
||||
}
|
||||
```
|
||||
|
||||
### Accessing Pages
|
||||
|
||||
```v
|
||||
// Access all pages
|
||||
pages := mysite.pages // map[string]Page
|
||||
|
||||
// Get specific page
|
||||
page := mysite.pages['docs:introduction']
|
||||
|
||||
// Page structure
|
||||
pub struct Page {
|
||||
pub mut:
|
||||
id string // "collection:page_name"
|
||||
title string // Display title
|
||||
description string // SEO metadata
|
||||
draft bool // Hidden if true
|
||||
hide_title bool // Don't show title in rendering
|
||||
src string // Source reference
|
||||
}
|
||||
```
|
||||
|
||||
### Navigation Structure
|
||||
|
||||
```v
|
||||
// Access sidebar navigation
|
||||
sidebar := mysite.nav.my_sidebar // []NavItem
|
||||
|
||||
// NavItem is a sum type (can be one of three types):
|
||||
pub type NavItem = NavDoc | NavCat | NavLink
|
||||
|
||||
// Navigation items:
|
||||
|
||||
pub struct NavDoc {
|
||||
pub:
|
||||
id string // page id
|
||||
label string // display name
|
||||
}
|
||||
|
||||
pub struct NavCat {
|
||||
pub mut:
|
||||
label string
|
||||
collapsible bool
|
||||
collapsed bool
|
||||
items []NavItem // nested NavDoc/NavCat/NavLink
|
||||
}
|
||||
|
||||
pub struct NavLink {
|
||||
pub:
|
||||
label string
|
||||
href string
|
||||
description string
|
||||
}
|
||||
|
||||
// Example: iterate navigation
|
||||
for item in mysite.nav.my_sidebar {
|
||||
match item {
|
||||
NavDoc {
|
||||
println('Page: ${item.label} (${item.id})')
|
||||
}
|
||||
NavCat {
|
||||
println('Category: ${item.label} (${item.items.len} items)')
|
||||
}
|
||||
NavLink {
|
||||
println('Link: ${item.label} -> ${item.href}')
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Site Configuration
|
||||
|
||||
```v
|
||||
pub struct SiteConfig {
|
||||
pub mut:
|
||||
// Core
|
||||
name string
|
||||
title string
|
||||
description string
|
||||
tagline string
|
||||
favicon string
|
||||
image string
|
||||
copyright string
|
||||
|
||||
// URLs (Docusaurus)
|
||||
url string // Full site URL
|
||||
base_url string // Base path (e.g., "/" or "/docs/")
|
||||
url_home string // Home page path
|
||||
|
||||
// SEO Metadata
|
||||
meta_title string // SEO title override
|
||||
meta_image string // OG image override
|
||||
|
||||
// Publishing
|
||||
build_dest []BuildDest // Production destinations
|
||||
build_dest_dev []BuildDest // Development destinations
|
||||
|
||||
// Navigation & Footer
|
||||
footer Footer
|
||||
menu Menu
|
||||
announcement AnnouncementBar
|
||||
|
||||
// Imports
|
||||
imports []ImportItem
|
||||
}
|
||||
|
||||
pub struct BuildDest {
|
||||
pub mut:
|
||||
path string
|
||||
ssh_name string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Site
|
||||
A website configuration that contains pages, navigation structure, and metadata.
|
||||
|
||||
### Page
|
||||
A single page with:
|
||||
- **ID**: `collection:page_name` format
|
||||
- **Title**: Display name (optional - extracted from markdown if not provided)
|
||||
- **Description**: SEO metadata
|
||||
- **Draft**: Hidden from navigation if true
|
||||
|
||||
### Category (Section)
|
||||
Groups related pages together in the navigation sidebar. Automatically collapsed/expandable.
|
||||
|
||||
### Collection
|
||||
A logical group of pages. Pages reuse the collection once specified.
|
||||
|
||||
```heroscript
|
||||
!!site.page src: "tech:intro" # Specifies collection "tech"
|
||||
!!site.page src: "benefits" # Reuses collection "tech"
|
||||
!!site.page src: "components" # Still uses collection "tech"
|
||||
!!site.page src: "api:reference" # Switches to collection "api"
|
||||
!!site.page src: "endpoints" # Uses collection "api"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HeroScript Syntax
|
||||
|
||||
### 1. Site Configuration (Required)
|
||||
|
||||
```heroscript
|
||||
!!site.config
|
||||
name: "my_site"
|
||||
title: "My Documentation Site"
|
||||
description: "Comprehensive documentation"
|
||||
tagline: "Your awesome documentation"
|
||||
favicon: "img/favicon.png"
|
||||
image: "img/site-image.png"
|
||||
copyright: "© 2024 My Organization"
|
||||
url: "https://docs.example.com"
|
||||
base_url: "/"
|
||||
url_home: "/docs"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `name` - Internal site identifier (default: 'default')
|
||||
- `title` - Main site title (shown in browser tab)
|
||||
- `description` - Site description for SEO
|
||||
- `tagline` - Short tagline/subtitle
|
||||
- `favicon` - Path to favicon image
|
||||
- `image` - Default OG image for social sharing
|
||||
- `copyright` - Copyright notice
|
||||
- `url` - Full site URL for Docusaurus
|
||||
- `base_url` - Base URL path (e.g., "/" or "/docs/")
|
||||
- `url_home` - Home page path
|
||||
|
||||
### 2. Metadata Overrides (Optional)
|
||||
|
||||
```heroscript
|
||||
!!site.config_meta
|
||||
title: "My Docs - Technical Reference"
|
||||
image: "img/tech-og.png"
|
||||
description: "Technical documentation and API reference"
|
||||
```
|
||||
|
||||
Overrides specific metadata for SEO without changing core config.
|
||||
|
||||
### 3. Navigation Bar
|
||||
|
||||
```heroscript
|
||||
!!site.navbar
|
||||
title: "My Documentation"
|
||||
logo_alt: "Site Logo"
|
||||
logo_src: "img/logo.svg"
|
||||
logo_src_dark: "img/logo-dark.svg"
|
||||
|
||||
!!site.navbar_item
|
||||
label: "Documentation"
|
||||
to: "intro"
|
||||
position: "left"
|
||||
|
||||
!!site.navbar_item
|
||||
label: "API Reference"
|
||||
to: "docs/api"
|
||||
position: "left"
|
||||
|
||||
!!site.navbar_item
|
||||
label: "GitHub"
|
||||
href: "https://github.com/myorg/myrepo"
|
||||
position: "right"
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `label` - Display text (required)
|
||||
- `to` - Internal link
|
||||
- `href` - External URL
|
||||
- `position` - "left" or "right" in navbar
|
||||
|
||||
### 4. Footer Configuration
|
||||
|
||||
```heroscript
|
||||
!!site.footer
|
||||
style: "dark"
|
||||
|
||||
!!site.footer_item
|
||||
title: "Docs"
|
||||
label: "Introduction"
|
||||
to: "intro"
|
||||
|
||||
!!site.footer_item
|
||||
title: "Docs"
|
||||
label: "Getting Started"
|
||||
to: "getting-started"
|
||||
|
||||
!!site.footer_item
|
||||
title: "Community"
|
||||
label: "Discord"
|
||||
href: "https://discord.gg/example"
|
||||
|
||||
!!site.footer_item
|
||||
title: "Legal"
|
||||
label: "Privacy"
|
||||
href: "https://example.com/privacy"
|
||||
```
|
||||
|
||||
### 5. Announcement Bar (Optional)
|
||||
|
||||
```heroscript
|
||||
!!site.announcement
|
||||
content: "🎉 Version 2.0 is now available!"
|
||||
background_color: "#20232a"
|
||||
text_color: "#fff"
|
||||
is_closeable: true
|
||||
```
|
||||
|
||||
### 6. Pages and Categories
|
||||
|
||||
#### Simple: Pages Without Categories
|
||||
|
||||
```heroscript
|
||||
!!site.page src: "guides:introduction"
|
||||
title: "Getting Started"
|
||||
description: "Introduction to the platform"
|
||||
|
||||
!!site.page src: "installation"
|
||||
title: "Installation"
|
||||
|
||||
!!site.page src: "configuration"
|
||||
title: "Configuration"
|
||||
```
|
||||
|
||||
#### Advanced: Pages With Categories
|
||||
|
||||
```heroscript
|
||||
!!site.page_category
|
||||
name: "basics"
|
||||
label: "Getting Started"
|
||||
|
||||
!!site.page src: "guides:introduction"
|
||||
title: "Introduction"
|
||||
description: "Learn the basics"
|
||||
|
||||
!!site.page src: "installation"
|
||||
title: "Installation"
|
||||
|
||||
!!site.page src: "configuration"
|
||||
title: "Configuration"
|
||||
|
||||
!!site.page_category
|
||||
name: "advanced"
|
||||
label: "Advanced Topics"
|
||||
|
||||
!!site.page src: "advanced:performance"
|
||||
title: "Performance Tuning"
|
||||
|
||||
!!site.page src: "scaling"
|
||||
title: "Scaling Guide"
|
||||
```
|
||||
|
||||
**Page Parameters:**
|
||||
- `src` - Source as `collection:page` (first page) or just `page_name` (reuse collection)
|
||||
- `title` - Page title (optional, extracted from markdown if not provided)
|
||||
- `description` - Page description
|
||||
- `draft` - Hide from navigation (default: false)
|
||||
- `hide_title` - Don't show title in page (default: false)
|
||||
|
||||
**Category Parameters:**
|
||||
- `name` - Category identifier (required)
|
||||
- `label` - Display label (auto-generated from name if omitted)
|
||||
- `position` - Sort order (auto-incremented if omitted)
|
||||
|
||||
### 7. Content Imports
|
||||
|
||||
```heroscript
|
||||
!!site.import
|
||||
url: "https://github.com/example/external-docs"
|
||||
path: "/local/path/to/repo"
|
||||
dest: "external"
|
||||
replace: "PROJECT_NAME:My Project,VERSION:1.0.0"
|
||||
visible: true
|
||||
```
|
||||
|
||||
### 8. Publishing Destinations
|
||||
|
||||
```heroscript
|
||||
!!site.publish
|
||||
path: "/var/www/html/docs"
|
||||
ssh_name: "production"
|
||||
|
||||
!!site.publish_dev
|
||||
path: "/tmp/docs-preview"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Multi-Section Technical Documentation
|
||||
|
||||
```heroscript
|
||||
!!site.config
|
||||
name: "tech_docs"
|
||||
title: "Technical Documentation"
|
||||
|
||||
!!site.page_category
|
||||
name: "getting_started"
|
||||
label: "Getting Started"
|
||||
|
||||
!!site.page src: "docs:intro"
|
||||
title: "Introduction"
|
||||
|
||||
!!site.page src: "installation"
|
||||
title: "Installation"
|
||||
|
||||
!!site.page_category
|
||||
name: "concepts"
|
||||
label: "Core Concepts"
|
||||
|
||||
!!site.page src: "concepts:architecture"
|
||||
title: "Architecture"
|
||||
|
||||
!!site.page src: "components"
|
||||
title: "Components"
|
||||
|
||||
!!site.page_category
|
||||
name: "api"
|
||||
label: "API Reference"
|
||||
|
||||
!!site.page src: "api:rest"
|
||||
title: "REST API"
|
||||
|
||||
!!site.page src: "graphql"
|
||||
title: "GraphQL"
|
||||
```
|
||||
|
||||
### Pattern 2: Simple Blog/Knowledge Base
|
||||
|
||||
```heroscript
|
||||
!!site.config
|
||||
name: "blog"
|
||||
title: "Knowledge Base"
|
||||
|
||||
!!site.page src: "articles:first_post"
|
||||
title: "Welcome to Our Blog"
|
||||
|
||||
!!site.page src: "second_post"
|
||||
title: "Understanding the Basics"
|
||||
|
||||
!!site.page src: "third_post"
|
||||
title: "Advanced Techniques"
|
||||
```
|
||||
|
||||
### Pattern 3: Project with External Imports
|
||||
|
||||
```heroscript
|
||||
!!site.config
|
||||
name: "project_docs"
|
||||
title: "Project Documentation"
|
||||
|
||||
!!site.import
|
||||
url: "https://github.com/org/shared-docs"
|
||||
dest: "shared"
|
||||
visible: true
|
||||
|
||||
!!site.page_category
|
||||
name: "product"
|
||||
label: "Product Guide"
|
||||
|
||||
!!site.page src: "docs:overview"
|
||||
title: "Overview"
|
||||
|
||||
!!site.page src: "features"
|
||||
title: "Features"
|
||||
|
||||
!!site.page_category
|
||||
name: "resources"
|
||||
label: "Shared Resources"
|
||||
|
||||
!!site.page src: "shared:common"
|
||||
title: "Common Patterns"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Organization
|
||||
|
||||
### Recommended Ebook Structure
|
||||
|
||||
The modern ebook structure uses `.hero` files for configuration and `.heroscript` files for page definitions:
|
||||
|
||||
```
|
||||
my_ebook/
|
||||
├── scan.hero # !!doctree.scan - collection scanning
|
||||
├── config.hero # !!site.config - site configuration
|
||||
├── menus.hero # !!site.navbar and !!site.footer
|
||||
├── include.hero # !!docusaurus.define and !!doctree.export
|
||||
├── 1_intro.heroscript # Page definitions (categories + pages)
|
||||
├── 2_concepts.heroscript # More page definitions
|
||||
└── 3_advanced.heroscript # Additional pages
|
||||
```
|
||||
|
||||
### File Types
|
||||
|
||||
- **`.hero` files**: Configuration files processed in any order
|
||||
- **`.heroscript` files**: Page definition files processed alphabetically
|
||||
|
||||
Use numeric prefixes on `.heroscript` files to control page/category ordering in the sidebar.
|
||||
|
||||
### Example scan.hero
|
||||
|
||||
```heroscript
|
||||
!!doctree.scan path:"../../collections/my_collection"
|
||||
```
|
||||
|
||||
### Example include.hero
|
||||
|
||||
```heroscript
|
||||
// Include shared configuration (optional)
|
||||
!!play.include path:'../../heroscriptall' replace:'SITENAME:my_ebook'
|
||||
|
||||
// Or define directly
|
||||
!!docusaurus.define name:'my_ebook'
|
||||
|
||||
!!doctree.export include:true
|
||||
```
|
||||
|
||||
### Running an Ebook
|
||||
|
||||
```bash
|
||||
# Development server
|
||||
hero docs -d -p /path/to/my_ebook
|
||||
|
||||
# Build for production
|
||||
hero docs -p /path/to/my_ebook
|
||||
```
|
||||
|
||||
447
lib/web/doctree/meta/siteplay_test.v
Normal file
447
lib/web/doctree/meta/siteplay_test.v
Normal file
@@ -0,0 +1,447 @@
|
||||
module meta
|
||||
|
||||
import incubaid.herolib.core.playbook
|
||||
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
|
||||
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()')
|
||||
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, 'Advanced test documentation')
|
||||
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 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()
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn test_site2() ! {
|
||||
console.print_header('Site Module Comprehensive Test')
|
||||
console.lf()
|
||||
|
||||
mut plbook := playbook.new(text: test_heroscript)!
|
||||
mut test_site := site.get(name: 'test_docs')!
|
||||
|
||||
// ========================================================
|
||||
// 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 {
|
||||
NavCat {
|
||||
category_count++
|
||||
console.print_debug(' Category: "${item.label}" with ${item.items.len} sub-items')
|
||||
}
|
||||
NavDoc {
|
||||
doc_count++
|
||||
console.print_debug(' Doc: "${item.label}" (${item.id})')
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
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 := 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 exists(name: 'test_docs'), 'test_docs should exist'
|
||||
console.print_green('✓ test_docs verified to exist')
|
||||
|
||||
console.lf()
|
||||
|
||||
// println(test_site)
|
||||
// if true{panic("ss33")}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 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}"')
|
||||
}
|
||||
2
lib/web/doctree/meta/utils.v
Normal file
2
lib/web/doctree/meta/utils.v
Normal file
@@ -0,0 +1,2 @@
|
||||
module meta
|
||||
|
||||
Reference in New Issue
Block a user