This commit is contained in:
2025-11-30 08:24:36 +01:00
parent 394dd2c88e
commit 0963910572
13 changed files with 648 additions and 1068 deletions

View File

@@ -17,9 +17,7 @@ pub mut:
reset bool
template_update bool
coderoot string
// Client configuration
use_atlas bool // true = atlas_client, false = doctreeclient
atlas_dir string // Required when use_atlas = true
atlas_dir string
}
@[params]
@@ -31,9 +29,7 @@ pub mut:
reset bool
template_update bool
coderoot string
// Client configuration
use_atlas bool // true = atlas_client, false = doctreeclient
atlas_dir string // Required when use_atlas = true
atlas_dir string
}
// return the last know config
@@ -42,8 +38,8 @@ pub fn config() !DocusaurusConfig {
docusaurus_config << DocusaurusConfigParams{}
}
mut args := docusaurus_config[0] or { panic('bug in docusaurus config') }
if args.use_atlas && args.atlas_dir == '' {
return error('use_atlas is true but atlas_dir is not set')
if args.atlas_dir == '' {
return error('atlas_dir is not set')
}
if args.path_build == '' {
args.path_build = '${os.home_dir()}/hero/var/docusaurus/build'
@@ -62,7 +58,6 @@ pub fn config() !DocusaurusConfig {
install: args.install
reset: args.reset
template_update: args.template_update
use_atlas: args.use_atlas
atlas_dir: args.atlas_dir
}
if c.install {

View File

@@ -9,6 +9,7 @@ pub mut:
main Main
navbar Navbar
footer Footer
sidebar_json_txt string //will hold the sidebar.json content
announcement AnnouncementBar
}
@@ -78,7 +79,7 @@ pub mut:
pub struct AnnouncementBar {
pub mut:
id string @[json: 'id']
// id string @[json: 'id']
content string @[json: 'content']
background_color string @[json: 'backgroundColor']
text_color string @[json: 'textColor']
@@ -88,8 +89,9 @@ pub mut:
// ... (struct definitions remain the same) ...
// This function is now a pure transformer: site.SiteConfig -> docusaurus.Configuration
fn new_configuration(site_cfg site.SiteConfig) !Configuration {
fn new_configuration(mysite site.Site) !Configuration {
// Transform site.SiteConfig to docusaurus.Configuration
mut site_cfg := mysite.siteconfig
mut nav_items := []NavbarItem{}
for item in site_cfg.menu.items {
nav_items << NavbarItem{
@@ -116,6 +118,8 @@ fn new_configuration(site_cfg site.SiteConfig) !Configuration {
}
}
sidebar_json_txt := mysite.nav.sidebar_to_json()!
cfg := Configuration{
main: Main{
title: site_cfg.title
@@ -161,13 +165,15 @@ fn new_configuration(site_cfg site.SiteConfig) !Configuration {
links: footer_links
}
announcement: AnnouncementBar{
id: site_cfg.announcement.id
// id: site_cfg.announcement.id
content: site_cfg.announcement.content
background_color: site_cfg.announcement.background_color
text_color: site_cfg.announcement.text_color
is_closeable: site_cfg.announcement.is_closeable
}
sidebar_json_txt: sidebar_json_txt
}
return config_fix(cfg)!
}

View File

@@ -33,7 +33,11 @@ pub fn (mut docsite DocSite) generate() ! {
mut announcement_file := pathlib.get_file(path: '${cfg_path}/announcement.json', create: true)!
announcement_file.write(json.encode_pretty(docsite.config.announcement))!
docsite.generate_docs()!
// generate sidebar.json, now new way to drive docusaurus navigation
mut sidebar_file := pathlib.get_file(path: '${cfg_path}/sidebar.json', create: true)!
sidebar_file.write(docsite.config.sidebar_json_txt)!
docsite.link_docs()!
docsite.import()!
}

View File

@@ -1,442 +0,0 @@
module docusaurus
import incubaid.herolib.core.pathlib
import incubaid.herolib.data.atlas.client as atlas_client
import incubaid.herolib.web.site { Page, Section, Site }
import incubaid.herolib.data.markdown.tools as markdowntools
import incubaid.herolib.ui.console
struct SiteGenerator {
mut:
siteconfig_name string
path pathlib.Path
client IDocClient
flat bool // if flat then won't use sitenames as subdir's
site Site
errors []string // collect errors here
}
// Generate docs from site configuration
pub fn (mut docsite DocSite) generate_docs() ! {
c := config()!
// we generate the docs in the build path
docs_path := '${c.path_build.path}/docs'
// Create the appropriate client based on configuration
mut client_instance := atlas_client.new(export_dir: c.atlas_dir)!
mut client := IDocClient(client_instance)
mut gen := SiteGenerator{
path: pathlib.get_dir(path: docs_path, create: true)!
client: client
flat: true
site: docsite.website
}
for section in gen.site.sections {
gen.section_generate(section)!
}
for page in gen.site.pages {
gen.page_generate(page)!
}
if gen.errors.len > 0 {
println('Page List: is header collection and page name per collection.\nAvailable pages:\n${gen.client.list_markdown()!}')
return error('Errors occurred during site generation:\n${gen.errors.join('\n\n')}\n')
}
}
fn (mut generator SiteGenerator) error(msg string) ! {
console.print_stderr('Error: ${msg}')
generator.errors << msg
}
fn (mut generator SiteGenerator) page_generate(args_ Page) ! {
mut args := args_
mut content := ['---']
mut parts := args.src.split(':')
if parts.len != 2 {
generator.error("Invalid src format for page '${args.src}', expected format: collection:page_name, TODO: fix in ${args.path}, check the collection & page_name exists in the pagelist")!
return
}
collection_name := parts[0]
page_name := parts[1]
mut page_content := generator.client.get_page_content(collection_name, page_name) or {
generator.error("Couldn't find page '${collection_name}:${page_name}' is formatted as collectionname:pagename. TODO: fix in ${args.path}, check the collection & page_name exists in the pagelist. ")!
return
}
if args.description.len == 0 {
descnew := markdowntools.extract_title(page_content)
if descnew != '' {
args.description = descnew
} else {
args.description = page_name
}
}
if args.title.len == 0 {
descnew := markdowntools.extract_title(page_content)
if descnew != '' {
args.title = descnew
} else {
args.title = page_name
}
}
// Escape single quotes in YAML by doubling them
escaped_title := args.title.replace("'", "''")
content << "title: '${escaped_title}'"
if args.description.len > 0 {
escaped_description := args.description.replace("'", "''")
content << "description: '${escaped_description}'"
}
if args.slug.len > 0 {
escaped_slug := args.slug.replace("'", "''")
content << "slug: '${escaped_slug}'"
}
if args.hide_title {
content << 'hide_title: ${args.hide_title}'
}
if args.draft {
content << 'draft: ${args.draft}'
}
if args.position > 0 {
content << 'sidebar_position: ${args.position}'
}
content << '---'
mut c := content.join('\n')
if args.title_nr > 0 {
// Set the title number in the page content
page_content = markdowntools.set_titles(page_content, args.title_nr)
}
// Fix links to account for nested categories
page_content = generator.fix_links(page_content, args.path)
c += '\n${page_content}\n'
if args.path.ends_with('/') || args.path.trim_space() == '' {
// means is dir
args.path += page_name
}
if !args.path.ends_with('.md') {
args.path += '.md'
}
mut pagepath := '${generator.path.path}/${args.path}'
mut pagefile := pathlib.get_file(path: pagepath, create: true)!
pagefile.write(c)!
generator.client.copy_pages(collection_name, page_name, pagefile.path_dir()) or {
generator.error("Couldn't copy pages for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")!
return
}
generator.client.copy_images(collection_name, page_name, pagefile.path_dir()) or {
generator.error("Couldn't copy images for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")!
return
}
generator.client.copy_files(collection_name, page_name, pagefile.path_dir()) or {
generator.error("Couldn't copy files for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")!
return
}
}
fn (mut generator SiteGenerator) section_generate(args_ Section) ! {
mut args := args_
mut c := ''
if args.description.len > 0 {
c = '{
"label": "${args.label}",
"position": ${args.position},
"link": {
"type": "generated-index",
"description": "${args.description}"
}
}'
} else {
c = '{
"label": "${args.label}",
"position": ${args.position},
"link": {
"type": "generated-index"
}
}'
}
mut category_path := '${generator.path.path}/${args.path}/_category_.json'
mut catfile := pathlib.get_file(path: category_path, create: true)!
catfile.write(c)!
}
// Strip numeric prefix from filename (e.g., "03_linux_installation" -> "linux_installation")
// Docusaurus automatically strips these prefixes from URLs
fn strip_numeric_prefix(name string) string {
// Match pattern: digits followed by underscore at the start
if name.len > 2 && name[0].is_digit() {
for i := 1; i < name.len; i++ {
if name[i] == `_` {
// Found the underscore, return everything after it
return name[i + 1..]
}
if !name[i].is_digit() {
// Not a numeric prefix pattern, return as-is
return name
}
}
}
return name
}
// Calculate relative path from current directory to target directory
// current_dir: directory of the current page (e.g., '' for root, 'tokens' for tokens/, 'farming/advanced' for nested)
// target_dir: directory of the target page
// page_name: name of the target page
// Returns: relative path (e.g., './page', '../dir/page', '../../page')
fn calculate_relative_path(current_dir string, target_dir string, page_name string) string {
// Both at root level
if current_dir == '' && target_dir == '' {
return './${page_name}'
}
// Current at root, target in subdirectory
if current_dir == '' && target_dir != '' {
return './${target_dir}/${page_name}'
}
// Current in subdirectory, target at root
if current_dir != '' && target_dir == '' {
// Count directory levels to go up
levels := current_dir.split('/').len
up := '../'.repeat(levels)
return '${up}${page_name}'
}
// Both in subdirectories
current_parts := current_dir.split('/')
target_parts := target_dir.split('/')
// Find common prefix
mut common_len := 0
for i := 0; i < current_parts.len && i < target_parts.len; i++ {
if current_parts[i] == target_parts[i] {
common_len++
} else {
break
}
}
// Calculate how many levels to go up
up_levels := current_parts.len - common_len
mut path_parts := []string{}
// Add ../ for each level up
for _ in 0 .. up_levels {
path_parts << '..'
}
// Add remaining target path parts
for i in common_len .. target_parts.len {
path_parts << target_parts[i]
}
// Add page name
path_parts << page_name
return path_parts.join('/')
}
// Fix links to account for nested categories and Docusaurus URL conventions
fn (generator SiteGenerator) fix_links(content string, current_page_path string) string {
mut result := content
// Extract current page's directory path
mut current_dir := current_page_path.trim('/')
if current_dir.contains('/') && !current_dir.ends_with('/') {
last_part := current_dir.all_after_last('/')
if last_part.contains('.') {
current_dir = current_dir.all_before_last('/')
}
}
// If path is just a filename or empty, current_dir should be empty (root level)
if !current_dir.contains('/') && current_dir.contains('.') {
current_dir = ''
}
// Build maps for link fixing
mut collection_paths := map[string]string{} // collection -> directory path (for nested collections)
mut page_to_path := map[string]string{} // page_name -> full directory path in Docusaurus
mut collection_page_map := map[string]string{} // "collection:page" -> directory path
for page in generator.site.pages {
parts := page.src.split(':')
if parts.len != 2 {
continue
}
collection := parts[0]
page_name := parts[1]
// Extract directory path from page.path
mut dir_path := page.path.trim('/')
if dir_path.contains('/') && !dir_path.ends_with('/') {
last_part := dir_path.all_after_last('/')
if last_part.contains('.') || last_part == page_name {
dir_path = dir_path.all_before_last('/')
}
}
// Store collection -> directory mapping for nested collections
if dir_path != collection && dir_path != '' {
collection_paths[collection] = dir_path
}
// Store page_name -> directory path for fixing same-collection links
// Strip numeric prefix from page_name for the map key
clean_page_name := strip_numeric_prefix(page_name)
page_to_path[clean_page_name] = dir_path
// Store collection:page -> directory path for fixing collection:page format links
collection_page_map['${collection}:${clean_page_name}'] = dir_path
}
// STEP 1: Strip numeric prefixes from all page references in links FIRST
mut lines := result.split('\n')
for i, line in lines {
if !line.contains('](') {
continue
}
mut new_line := line
parts := line.split('](')
if parts.len < 2 {
continue
}
for j := 1; j < parts.len; j++ {
close_idx := parts[j].index(')') or { continue }
link_url := parts[j][..close_idx]
mut new_url := link_url
if link_url.contains('/') {
path_part := link_url.all_before_last('/')
file_part := link_url.all_after_last('/')
new_file := strip_numeric_prefix(file_part)
if new_file != file_part {
new_url = '${path_part}/${new_file}'
}
} else {
new_url = strip_numeric_prefix(link_url)
}
if new_url != link_url {
new_line = new_line.replace('](${link_url})', '](${new_url})')
}
}
lines[i] = new_line
}
result = lines.join('\n')
// STEP 2: Replace ../collection/ with ../actual/nested/path/ for cross-collection links
for collection, actual_path in collection_paths {
result = result.replace('../${collection}/', '../${actual_path}/')
}
// STEP 3: Fix same-collection links: ./page -> correct path based on Docusaurus structure
for page_name, target_dir in page_to_path {
old_link := './${page_name}'
if result.contains(old_link) {
new_link := calculate_relative_path(current_dir, target_dir, page_name)
result = result.replace(old_link, new_link)
}
}
// STEP 4: Convert collection:page format to proper relative paths
// Calculate relative path from current page to target page
for collection_page, target_dir in collection_page_map {
old_pattern := collection_page
if result.contains(old_pattern) {
// Extract just the page name from "collection:page"
page_name := collection_page.all_after(':')
new_link := calculate_relative_path(current_dir, target_dir, page_name)
result = result.replace(old_pattern, new_link)
}
}
// STEP 5: Fix bare page references (from atlas self-contained exports)
// Atlas exports convert cross-collection links to simple relative links like "token_system2.md"
// We need to transform these to proper relative paths based on Docusaurus structure
for page_name, target_dir in page_to_path {
// Match links in the format ](page_name) or ](page_name.md)
old_link_with_md := '](${page_name}.md)'
old_link_without_md := '](${page_name})'
if result.contains(old_link_with_md) || result.contains(old_link_without_md) {
new_link := calculate_relative_path(current_dir, target_dir, page_name)
// Replace both .md and non-.md versions
result = result.replace(old_link_with_md, '](${new_link})')
result = result.replace(old_link_without_md, '](${new_link})')
}
}
// STEP 6: Remove .md extensions from all remaining links (Docusaurus doesn't use them in URLs)
result = result.replace('.md)', ')')
// STEP 7: Fix image links to point to img/ subdirectory
// Images are copied to img/ subdirectory by copy_images(), so we need to update the links
// Transform ![alt](image.png) to ![alt](img/image.png) for local images only
mut image_lines := result.split('\n')
for i, line in image_lines {
// Find image links: ![...](...) but skip external URLs
if line.contains('![') {
mut pos := 0
for {
img_start := line.index_after('![', pos) or { break }
alt_end := line.index_after(']', img_start) or { break }
if alt_end + 1 >= line.len || line[alt_end + 1] != `(` {
pos = alt_end + 1
continue
}
url_start := alt_end + 2
url_end := line.index_after(')', url_start) or { break }
url := line[url_start..url_end]
// Skip external URLs and already-prefixed img/ paths
if url.starts_with('http://') || url.starts_with('https://')
|| url.starts_with('img/') || url.starts_with('./img/') {
pos = url_end + 1
continue
}
// Skip absolute paths and paths with ../
if url.starts_with('/') || url.starts_with('../') {
pos = url_end + 1
continue
}
// This is a local image reference - add img/ prefix
new_url := 'img/${url}'
image_lines[i] = line[0..url_start] + new_url + line[url_end..]
break
}
}
}
result = image_lines.join('\n')
return result
}

View File

@@ -0,0 +1,442 @@
module docusaurus
import incubaid.herolib.core.pathlib
// import incubaid.herolib.data.atlas.client as atlas_client
// import incubaid.herolib.web.site { Page, Section, Site }
// import incubaid.herolib.data.markdown.tools as markdowntools
// import incubaid.herolib.ui.console
// struct SiteGenerator {
// mut:
// siteconfig_name string
// path pathlib.Path
// client IDocClient
// flat bool // if flat then won't use sitenames as subdir's
// site Site
// errors []string // collect errors here
// }
// // Generate docs from site configuration
// pub fn (mut docsite DocSite) generate_docs() ! {
// c := config()!
// // we generate the docs in the build path
// docs_path := '${c.path_build.path}/docs'
// // Create the appropriate client based on configuration
// mut client_instance := atlas_client.new(export_dir: c.atlas_dir)!
// mut client := IDocClient(client_instance)
// mut gen := SiteGenerator{
// path: pathlib.get_dir(path: docs_path, create: true)!
// client: client
// flat: true
// site: docsite.website
// }
// for section in gen.site.sections {
// gen.section_generate(section)!
// }
// for page in gen.site.pages {
// gen.page_generate(page)!
// }
// if gen.errors.len > 0 {
// println('Page List: is header collection and page name per collection.\nAvailable pages:\n${gen.client.list_markdown()!}')
// return error('Errors occurred during site generation:\n${gen.errors.join('\n\n')}\n')
// }
// }
// fn (mut generator SiteGenerator) error(msg string) ! {
// console.print_stderr('Error: ${msg}')
// generator.errors << msg
// }
// fn (mut generator SiteGenerator) page_generate(args_ Page) ! {
// mut args := args_
// mut content := ['---']
// mut parts := args.src.split(':')
// if parts.len != 2 {
// generator.error("Invalid src format for page '${args.src}', expected format: collection:page_name, TODO: fix in ${args.path}, check the collection & page_name exists in the pagelist")!
// return
// }
// collection_name := parts[0]
// page_name := parts[1]
// mut page_content := generator.client.get_page_content(collection_name, page_name) or {
// generator.error("Couldn't find page '${collection_name}:${page_name}' is formatted as collectionname:pagename. TODO: fix in ${args.path}, check the collection & page_name exists in the pagelist. ")!
// return
// }
// if args.description.len == 0 {
// descnew := markdowntools.extract_title(page_content)
// if descnew != '' {
// args.description = descnew
// } else {
// args.description = page_name
// }
// }
// if args.title.len == 0 {
// descnew := markdowntools.extract_title(page_content)
// if descnew != '' {
// args.title = descnew
// } else {
// args.title = page_name
// }
// }
// // Escape single quotes in YAML by doubling them
// escaped_title := args.title.replace("'", "''")
// content << "title: '${escaped_title}'"
// if args.description.len > 0 {
// escaped_description := args.description.replace("'", "''")
// content << "description: '${escaped_description}'"
// }
// if args.slug.len > 0 {
// escaped_slug := args.slug.replace("'", "''")
// content << "slug: '${escaped_slug}'"
// }
// if args.hide_title {
// content << 'hide_title: ${args.hide_title}'
// }
// if args.draft {
// content << 'draft: ${args.draft}'
// }
// if args.position > 0 {
// content << 'sidebar_position: ${args.position}'
// }
// content << '---'
// mut c := content.join('\n')
// if args.title_nr > 0 {
// // Set the title number in the page content
// page_content = markdowntools.set_titles(page_content, args.title_nr)
// }
// // Fix links to account for nested categories
// page_content = generator.fix_links(page_content, args.path)
// c += '\n${page_content}\n'
// if args.path.ends_with('/') || args.path.trim_space() == '' {
// // means is dir
// args.path += page_name
// }
// if !args.path.ends_with('.md') {
// args.path += '.md'
// }
// mut pagepath := '${generator.path.path}/${args.path}'
// mut pagefile := pathlib.get_file(path: pagepath, create: true)!
// pagefile.write(c)!
// generator.client.copy_pages(collection_name, page_name, pagefile.path_dir()) or {
// generator.error("Couldn't copy pages for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")!
// return
// }
// generator.client.copy_images(collection_name, page_name, pagefile.path_dir()) or {
// generator.error("Couldn't copy images for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")!
// return
// }
// generator.client.copy_files(collection_name, page_name, pagefile.path_dir()) or {
// generator.error("Couldn't copy files for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")!
// return
// }
// }
// fn (mut generator SiteGenerator) section_generate(args_ Section) ! {
// mut args := args_
// mut c := ''
// if args.description.len > 0 {
// c = '{
// "label": "${args.label}",
// "position": ${args.position},
// "link": {
// "type": "generated-index",
// "description": "${args.description}"
// }
// }'
// } else {
// c = '{
// "label": "${args.label}",
// "position": ${args.position},
// "link": {
// "type": "generated-index"
// }
// }'
// }
// mut category_path := '${generator.path.path}/${args.path}/_category_.json'
// mut catfile := pathlib.get_file(path: category_path, create: true)!
// catfile.write(c)!
// }
// // Strip numeric prefix from filename (e.g., "03_linux_installation" -> "linux_installation")
// // Docusaurus automatically strips these prefixes from URLs
// fn strip_numeric_prefix(name string) string {
// // Match pattern: digits followed by underscore at the start
// if name.len > 2 && name[0].is_digit() {
// for i := 1; i < name.len; i++ {
// if name[i] == `_` {
// // Found the underscore, return everything after it
// return name[i + 1..]
// }
// if !name[i].is_digit() {
// // Not a numeric prefix pattern, return as-is
// return name
// }
// }
// }
// return name
// }
// // Calculate relative path from current directory to target directory
// // current_dir: directory of the current page (e.g., '' for root, 'tokens' for tokens/, 'farming/advanced' for nested)
// // target_dir: directory of the target page
// // page_name: name of the target page
// // Returns: relative path (e.g., './page', '../dir/page', '../../page')
// fn calculate_relative_path(current_dir string, target_dir string, page_name string) string {
// // Both at root level
// if current_dir == '' && target_dir == '' {
// return './${page_name}'
// }
// // Current at root, target in subdirectory
// if current_dir == '' && target_dir != '' {
// return './${target_dir}/${page_name}'
// }
// // Current in subdirectory, target at root
// if current_dir != '' && target_dir == '' {
// // Count directory levels to go up
// levels := current_dir.split('/').len
// up := '../'.repeat(levels)
// return '${up}${page_name}'
// }
// // Both in subdirectories
// current_parts := current_dir.split('/')
// target_parts := target_dir.split('/')
// // Find common prefix
// mut common_len := 0
// for i := 0; i < current_parts.len && i < target_parts.len; i++ {
// if current_parts[i] == target_parts[i] {
// common_len++
// } else {
// break
// }
// }
// // Calculate how many levels to go up
// up_levels := current_parts.len - common_len
// mut path_parts := []string{}
// // Add ../ for each level up
// for _ in 0 .. up_levels {
// path_parts << '..'
// }
// // Add remaining target path parts
// for i in common_len .. target_parts.len {
// path_parts << target_parts[i]
// }
// // Add page name
// path_parts << page_name
// return path_parts.join('/')
// }
// // Fix links to account for nested categories and Docusaurus URL conventions
// fn (generator SiteGenerator) fix_links(content string, current_page_path string) string {
// mut result := content
// // Extract current page's directory path
// mut current_dir := current_page_path.trim('/')
// if current_dir.contains('/') && !current_dir.ends_with('/') {
// last_part := current_dir.all_after_last('/')
// if last_part.contains('.') {
// current_dir = current_dir.all_before_last('/')
// }
// }
// // If path is just a filename or empty, current_dir should be empty (root level)
// if !current_dir.contains('/') && current_dir.contains('.') {
// current_dir = ''
// }
// // Build maps for link fixing
// mut collection_paths := map[string]string{} // collection -> directory path (for nested collections)
// mut page_to_path := map[string]string{} // page_name -> full directory path in Docusaurus
// mut collection_page_map := map[string]string{} // "collection:page" -> directory path
// for page in generator.site.pages {
// parts := page.src.split(':')
// if parts.len != 2 {
// continue
// }
// collection := parts[0]
// page_name := parts[1]
// // Extract directory path from page.path
// mut dir_path := page.path.trim('/')
// if dir_path.contains('/') && !dir_path.ends_with('/') {
// last_part := dir_path.all_after_last('/')
// if last_part.contains('.') || last_part == page_name {
// dir_path = dir_path.all_before_last('/')
// }
// }
// // Store collection -> directory mapping for nested collections
// if dir_path != collection && dir_path != '' {
// collection_paths[collection] = dir_path
// }
// // Store page_name -> directory path for fixing same-collection links
// // Strip numeric prefix from page_name for the map key
// clean_page_name := strip_numeric_prefix(page_name)
// page_to_path[clean_page_name] = dir_path
// // Store collection:page -> directory path for fixing collection:page format links
// collection_page_map['${collection}:${clean_page_name}'] = dir_path
// }
// // STEP 1: Strip numeric prefixes from all page references in links FIRST
// mut lines := result.split('\n')
// for i, line in lines {
// if !line.contains('](') {
// continue
// }
// mut new_line := line
// parts := line.split('](')
// if parts.len < 2 {
// continue
// }
// for j := 1; j < parts.len; j++ {
// close_idx := parts[j].index(')') or { continue }
// link_url := parts[j][..close_idx]
// mut new_url := link_url
// if link_url.contains('/') {
// path_part := link_url.all_before_last('/')
// file_part := link_url.all_after_last('/')
// new_file := strip_numeric_prefix(file_part)
// if new_file != file_part {
// new_url = '${path_part}/${new_file}'
// }
// } else {
// new_url = strip_numeric_prefix(link_url)
// }
// if new_url != link_url {
// new_line = new_line.replace('](${link_url})', '](${new_url})')
// }
// }
// lines[i] = new_line
// }
// result = lines.join('\n')
// // STEP 2: Replace ../collection/ with ../actual/nested/path/ for cross-collection links
// for collection, actual_path in collection_paths {
// result = result.replace('../${collection}/', '../${actual_path}/')
// }
// // STEP 3: Fix same-collection links: ./page -> correct path based on Docusaurus structure
// for page_name, target_dir in page_to_path {
// old_link := './${page_name}'
// if result.contains(old_link) {
// new_link := calculate_relative_path(current_dir, target_dir, page_name)
// result = result.replace(old_link, new_link)
// }
// }
// // STEP 4: Convert collection:page format to proper relative paths
// // Calculate relative path from current page to target page
// for collection_page, target_dir in collection_page_map {
// old_pattern := collection_page
// if result.contains(old_pattern) {
// // Extract just the page name from "collection:page"
// page_name := collection_page.all_after(':')
// new_link := calculate_relative_path(current_dir, target_dir, page_name)
// result = result.replace(old_pattern, new_link)
// }
// }
// // STEP 5: Fix bare page references (from atlas self-contained exports)
// // Atlas exports convert cross-collection links to simple relative links like "token_system2.md"
// // We need to transform these to proper relative paths based on Docusaurus structure
// for page_name, target_dir in page_to_path {
// // Match links in the format ](page_name) or ](page_name.md)
// old_link_with_md := '](${page_name}.md)'
// old_link_without_md := '](${page_name})'
// if result.contains(old_link_with_md) || result.contains(old_link_without_md) {
// new_link := calculate_relative_path(current_dir, target_dir, page_name)
// // Replace both .md and non-.md versions
// result = result.replace(old_link_with_md, '](${new_link})')
// result = result.replace(old_link_without_md, '](${new_link})')
// }
// }
// // STEP 6: Remove .md extensions from all remaining links (Docusaurus doesn't use them in URLs)
// result = result.replace('.md)', ')')
// // STEP 7: Fix image links to point to img/ subdirectory
// // Images are copied to img/ subdirectory by copy_images(), so we need to update the links
// // Transform ![alt](image.png) to ![alt](img/image.png) for local images only
// mut image_lines := result.split('\n')
// for i, line in image_lines {
// // Find image links: ![...](...) but skip external URLs
// if line.contains('![') {
// mut pos := 0
// for {
// img_start := line.index_after('![', pos) or { break }
// alt_end := line.index_after(']', img_start) or { break }
// if alt_end + 1 >= line.len || line[alt_end + 1] != `(` {
// pos = alt_end + 1
// continue
// }
// url_start := alt_end + 2
// url_end := line.index_after(')', url_start) or { break }
// url := line[url_start..url_end]
// // Skip external URLs and already-prefixed img/ paths
// if url.starts_with('http://') || url.starts_with('https://')
// || url.starts_with('img/') || url.starts_with('./img/') {
// pos = url_end + 1
// continue
// }
// // Skip absolute paths and paths with ../
// if url.starts_with('/') || url.starts_with('../') {
// pos = url_end + 1
// continue
// }
// // This is a local image reference - add img/ prefix
// new_url := 'img/${url}'
// image_lines[i] = line[0..url_start] + new_url + line[url_end..]
// break
// }
// }
// }
// result = image_lines.join('\n')
// return result
// }

View File

@@ -0,0 +1,29 @@
module docusaurus
import incubaid.herolib.core.pathlib
// import incubaid.herolib.data.atlas.client as atlas_client
// import incubaid.herolib.web.site { Page, Section, Site }
import incubaid.herolib.data.markdown.tools as markdowntools
import incubaid.herolib.ui.console
import os
// Generate docs from site configuration
pub fn (mut docsite DocSite) link_docs() ! {
c := config()!
// we generate the docs in the build path
docs_path := '${c.path_build.path}/docs'
//reset it
os.rmdir_all(docs_path)!
os.mkdir(docs_path)!
//TODO: now link all the collections to the docs folder
println(c)
if true{
panic('link_docs is not yet implemented')
}
// $dbg;
}

View File

@@ -1,40 +0,0 @@
module docusaurus
import incubaid.herolib.core.base
import incubaid.herolib.core.texttools
// // Store the Docusaurus site structure in Redis for link processing
// // This maps collection:page to their actual Docusaurus paths
// pub fn (mut docsite DocSite) store_site_structure() ! {
// mut context := base.context()!
// mut redis := context.redis()!
// // Store mapping of collection:page to docusaurus path (without .md extension)
// for page in docsite.website.pages {
// parts := page.src.split(':')
// if parts.len != 2 {
// continue
// }
// collection_name := texttools.name_fix(parts[0])
// page_name := texttools.name_fix(parts[1])
// // Calculate the docusaurus path (without .md extension for URLs)
// mut doc_path := page.path
// // Handle empty or root path
// if doc_path.trim_space() == '' || doc_path == '/' {
// doc_path = page_name
// } else if doc_path.ends_with('/') {
// doc_path += page_name
// }
// // Remove .md extension if present for URL paths
// if doc_path.ends_with('.md') {
// doc_path = doc_path[..doc_path.len - 3]
// }
// // Store in Redis with key format: collection:page.md
// key := '${collection_name}:${page_name}.md'
// redis.hset('doctree_docusaurus_paths', key, doc_path)!
// }
// }

View File

@@ -26,7 +26,7 @@ pub fn dsite_define(sitename string) ! {
name: sitename
path_publish: pathlib.get_dir(path: '${path_build_}/build', create: true)!
path_build: pathlib.get_dir(path: path_build_, create: true)!
config: new_configuration(website.siteconfig)!
config: new_configuration(website)!
website: website
}

View File

@@ -1,106 +0,0 @@
module docusaurus
import os
import incubaid.herolib.core.pathlib
import incubaid.herolib.core.base // For context and Redis, if test needs to manage it
import time
const test_heroscript_content = '!!site.config\n name:"Kristof"\n title:"Internet Geek"\n tagline:"Internet Geek"\n url:"https://friends.threefold.info"\n url_home:"docs/"\n base_url:"/kristof/"\n favicon:"img/favicon.png"\n image:"img/tf_graph.png"\n copyright:"Kristof"\n\n!!site.config_meta\n description:"ThreeFold is laying the foundation for a geo aware Web 4, the next generation of the Internet."\n image:"https://threefold.info/kristof/img/tf_graph.png"\n title:"ThreeFold Technology Vision"\n\n!!site.build_dest\n ssh_name:"production"\n path:"/root/hero/www/info/kristof"\n\n!!site.navbar\n title:"Kristof = Chief Executive Geek"\n logo_alt:"Kristof Logo"\n logo_src:"img/logo.svg"\n logo_src_dark:"img/logo.svg"\n\n!!site.navbar_item\n label:"ThreeFold Technology"\n href:"https://threefold.info/kristof/"\n position:"right"\n\n!!site.navbar_item\n label:"ThreeFold.io"\n href:"https://threefold.io"\n position:"right"\n\n!!site.footer\n style:"dark"\n\n!!site.footer_item\n title:"Docs"\n label:"Introduction"\n href:"/docs"\n\n!!site.footer_item\n title:"Docs"\n label:"TFGrid V4 Docs"\n href:"https://docs.threefold.io/"\n\n!!site.footer_item\n title:"Community"\n label:"Telegram"\n href:"https://t.me/threefold"\n\n!!site.footer_item\n title:"Community"\n label:"X"\n href:"https://x.com/threefold_io"\n\n!!site.footer_item\n title:"Links"\n label:"ThreeFold.io"\n href:"https://threefold.io"\n'
fn test_load_configuration_from_heroscript() ! {
// Ensure context is initialized for Redis connection if siteconfig.new() needs it implicitly
base.context()!
temp_cfg_dir := os.join_path(os.temp_dir(), 'test_docusaurus_cfg_${time.ticks()}')
os.mkdir_all(temp_cfg_dir)!
defer {
os.rmdir_all(temp_cfg_dir) or { eprintln('Error removing temp dir.') }
}
heroscript_path := os.join_path(temp_cfg_dir, 'config.heroscript')
os.write_file(heroscript_path, test_heroscript_content)!
config := load_configuration(temp_cfg_dir)!
// Main assertions
assert config.main.name == 'kristof' // texttools.name_fix converts to lowercase
assert config.main.title == 'Internet Geek'
assert config.main.tagline == 'Internet Geek'
assert config.main.url == 'https://friends.threefold.info'
assert config.main.url_home == 'docs/'
assert config.main.base_url == '/kristof/'
assert config.main.favicon == 'img/favicon.png'
assert config.main.image == 'img/tf_graph.png'
assert config.main.copyright == 'Kristof'
// Metadata assertions
assert config.main.metadata.title == 'ThreeFold Technology Vision'
assert config.main.metadata.description == 'ThreeFold is laying the foundation for a geo aware Web 4, the next generation of the Internet.'
assert config.main.metadata.image == 'https://threefold.info/kristof/img/tf_graph.png'
// Build Dest assertions
assert config.main.build_dest.len == 1
assert config.main.build_dest[0] == '/root/hero/www/info/kristof'
// Navbar assertions
assert config.navbar.title == 'Kristof = Chief Executive Geek'
assert config.navbar.logo.alt == 'Kristof Logo'
assert config.navbar.logo.src == 'img/logo.svg'
assert config.navbar.logo.src_dark == 'img/logo.svg'
assert config.navbar.items.len == 2
assert config.navbar.items[0].label == 'ThreeFold Technology'
assert config.navbar.items[0].href == 'https://threefold.info/kristof/'
assert config.navbar.items[0].position == 'right'
assert config.navbar.items[1].label == 'ThreeFold.io'
assert config.navbar.items[1].href == 'https://threefold.io'
assert config.navbar.items[1].position == 'right'
// Footer assertions
assert config.footer.style == 'dark'
assert config.footer.links.len == 3 // 'Docs', 'Community', 'Links'
// Check 'Docs' footer links
mut docs_link_found := false
for link in config.footer.links {
if link.title == 'Docs' {
docs_link_found = true
assert link.items.len == 2
assert link.items[0].label == 'Introduction'
assert link.items[0].href == '/docs'
assert link.items[1].label == 'TFGrid V4 Docs'
assert link.items[1].href == 'https://docs.threefold.io/'
break
}
}
assert docs_link_found
// Check 'Community' footer links
mut community_link_found := false
for link in config.footer.links {
if link.title == 'Community' {
community_link_found = true
assert link.items.len == 2
assert link.items[0].label == 'Telegram'
assert link.items[0].href == 'https://t.me/threefold'
assert link.items[1].label == 'X'
assert link.items[1].href == 'https://x.com/threefold_io'
break
}
}
assert community_link_found
// Check 'Links' footer links
mut links_link_found := false
for link in config.footer.links {
if link.title == 'Links' {
links_link_found = true
assert link.items.len == 1
assert link.items[0].label == 'ThreeFold.io'
assert link.items[0].href == 'https://threefold.io'
break
}
}
assert links_link_found
println('test_load_configuration_from_heroscript passed successfully.')
}

View File

@@ -19,7 +19,6 @@ pub fn play(mut plbook PlayBook) ! {
template_update: param_define.get_default_false('template_update')
install: param_define.get_default_false('install')
atlas_dir: param_define.get_default('atlas_dir', '${os.home_dir()}/hero/var/atlas_export')!
use_atlas: param_define.get_default_false('use_atlas')
)!
site_name := param_define.get('name') or {

View File

@@ -1,444 +0,0 @@
module site
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
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}"')
}

View File

@@ -94,7 +94,7 @@ fn nav_item_to_json(item NavItem) !NavItemJson {
}
// Convert entire NavConfig sidebar to JSON string
fn (nc NavConfig) sidebar_to_json() !string {
pub fn (nc NavConfig) sidebar_to_json() !string {
mut result := []NavItemJson{}
for item in nc.my_sidebar {
result << nav_item_to_json(item)!

View File

@@ -38,9 +38,8 @@ site.play(mut plbook)!
mut mysite := site.get(name: 'my_docs')!
// Print available pages
pages_map := mysite.list_pages()
for page_id, _ in pages_map {
console.print_item('Page: ${page_id}')
for page_id, page in mysite.pages {
console.print_item('Page: ${page_id} - "${page.title}"')
}
println('Site has ${mysite.pages.len} pages')
@@ -48,6 +47,156 @@ 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
@@ -179,7 +328,6 @@ Overrides specific metadata for SEO without changing core config.
```heroscript
!!site.announcement
id: "new-release"
content: "🎉 Version 2.0 is now available!"
background_color: "#20232a"
text_color: "#fff"
@@ -237,6 +385,11 @@ Overrides specific metadata for SEO without changing core config.
- `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
@@ -377,19 +530,3 @@ Files are processed in alphabetical order. Numeric prefixes ensure:
- Pages build the final structure
- Publishing configured last
---
## Processing Order
The Site module processes HeroScript in this strict order:
1. Site Configuration
2. Metadata Overrides
3. Imports
4. Navigation
5. Footer
6. Announcement
7. Publishing
8. Pages & Categories
Each stage depends on previous stages completing successfully.