Compare commits
3 Commits
developmen
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e053f8aaf | |||
| fe12d1cc18 | |||
| ecb9fbfa67 |
@@ -1,4 +1,4 @@
|
||||
module builder
|
||||
|
||||
pub fn (mut node Node) ubuntu_sources_fix() {
|
||||
}
|
||||
// pub fn (mut node Node) ubuntu_sources_fix() {
|
||||
// }
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
module atlas
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
import os
|
||||
|
||||
const test_dir = '/tmp/atlas_save_test'
|
||||
|
||||
fn testsuite_begin() {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
os.mkdir_all(test_dir)!
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
fn test_save_and_load_basic() {
|
||||
// Create a collection with some content
|
||||
col_path := '${test_dir}/docs'
|
||||
os.mkdir_all(col_path)!
|
||||
|
||||
mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)!
|
||||
cfile.write('name:docs')!
|
||||
|
||||
mut page1 := pathlib.get_file(path: '${col_path}/intro.md', create: true)!
|
||||
page1.write('# Introduction\n\nWelcome to the docs!')!
|
||||
|
||||
mut page2 := pathlib.get_file(path: '${col_path}/guide.md', create: true)!
|
||||
page2.write('# Guide\n\nMore content here.')!
|
||||
|
||||
// Create and scan atlas
|
||||
mut a := new(name: 'my_docs')!
|
||||
a.scan(path: test_dir)!
|
||||
|
||||
assert a.collections.len == 1
|
||||
|
||||
// Save all collections
|
||||
// a.save(destination_meta: '/tmp/atlas_meta')!
|
||||
// assert os.exists('${col_path}/.collection.json')
|
||||
|
||||
// // Load in a new atlas
|
||||
// mut a2 := new(name: 'loaded_docs')!
|
||||
// a2.load_from_directory(test_dir)!
|
||||
|
||||
// assert a2.collections.len == 1
|
||||
|
||||
// // Access loaded data
|
||||
// loaded_col := a2.get_collection('docs')!
|
||||
// assert loaded_col.name == 'docs'
|
||||
// assert loaded_col.pages.len == 2
|
||||
|
||||
// // Verify pages exist
|
||||
// assert loaded_col.page_exists('intro')
|
||||
// assert loaded_col.page_exists('guide')
|
||||
|
||||
// // Read page content
|
||||
// mut intro_page := loaded_col.page_get('intro')!
|
||||
// content := intro_page.read_content()!
|
||||
// assert content.contains('# Introduction')
|
||||
// assert content.contains('Welcome to the docs!')
|
||||
}
|
||||
|
||||
fn test_save_and_load_with_includes() {
|
||||
col_path := '${test_dir}/docs_include'
|
||||
os.mkdir_all(col_path)!
|
||||
|
||||
mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)!
|
||||
cfile.write('name:docs')!
|
||||
|
||||
mut page1 := pathlib.get_file(path: '${col_path}/intro.md', create: true)!
|
||||
page1.write('# Introduction\n\nWelcome to the docs!')!
|
||||
|
||||
mut page2 := pathlib.get_file(path: '${col_path}/guide.md', create: true)!
|
||||
page2.write('# Guide\n\n!!include docs:intro\n\nMore content here.')!
|
||||
|
||||
// Create and scan atlas
|
||||
mut a := new(name: 'my_docs')!
|
||||
a.scan(path: '${test_dir}/docs_include')!
|
||||
|
||||
// Validate links (should find the include)
|
||||
a.validate_links()!
|
||||
|
||||
col := a.get_collection('docs')!
|
||||
assert !col.has_errors()
|
||||
|
||||
// // Save
|
||||
// a.save(destination_meta: '/tmp/atlas_meta')!
|
||||
|
||||
// // Load
|
||||
// mut a2 := new(name: 'loaded')!
|
||||
// a2.load_from_directory('${test_dir}/docs_include')!
|
||||
|
||||
// loaded_col := a2.get_collection('docs')!
|
||||
// assert loaded_col.pages.len == 2
|
||||
// assert !loaded_col.has_errors()
|
||||
}
|
||||
|
||||
fn test_save_and_load_with_errors() {
|
||||
col_path := '${test_dir}/docs_errors'
|
||||
os.mkdir_all(col_path)!
|
||||
|
||||
mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)!
|
||||
cfile.write('name:docs')!
|
||||
|
||||
// Create page with broken link
|
||||
mut page1 := pathlib.get_file(path: '${col_path}/broken.md', create: true)!
|
||||
page1.write('[Broken link](nonexistent)')!
|
||||
|
||||
// Create and scan atlas
|
||||
mut a := new(name: 'my_docs')!
|
||||
a.scan(path: '${test_dir}/docs_errors')!
|
||||
|
||||
// Validate - will generate errors
|
||||
a.validate_links()!
|
||||
|
||||
col := a.get_collection('docs')!
|
||||
assert col.has_errors()
|
||||
initial_error_count := col.errors.len
|
||||
|
||||
// // Save with errors
|
||||
// a.save(destination_meta: '/tmp/atlas_meta')!
|
||||
|
||||
// // Load
|
||||
// mut a2 := new(name: 'loaded')!
|
||||
// a2.load_from_directory('${test_dir}/docs_errors')!
|
||||
|
||||
// loaded_col := a2.get_collection('docs')!
|
||||
// assert loaded_col.has_errors()
|
||||
// assert loaded_col.errors.len == initial_error_count
|
||||
// assert loaded_col.error_cache.len == initial_error_count
|
||||
}
|
||||
|
||||
fn test_save_and_load_multiple_collections() {
|
||||
// Create multiple collections
|
||||
col1_path := '${test_dir}/multi/col1'
|
||||
col2_path := '${test_dir}/multi/col2'
|
||||
|
||||
os.mkdir_all(col1_path)!
|
||||
os.mkdir_all(col2_path)!
|
||||
|
||||
mut cfile1 := pathlib.get_file(path: '${col1_path}/.collection', create: true)!
|
||||
cfile1.write('name:col1')!
|
||||
|
||||
mut cfile2 := pathlib.get_file(path: '${col2_path}/.collection', create: true)!
|
||||
cfile2.write('name:col2')!
|
||||
|
||||
mut page1 := pathlib.get_file(path: '${col1_path}/page1.md', create: true)!
|
||||
page1.write('# Page 1')!
|
||||
|
||||
mut page2 := pathlib.get_file(path: '${col2_path}/page2.md', create: true)!
|
||||
page2.write('# Page 2')!
|
||||
|
||||
// Create and save
|
||||
mut a := new(name: 'multi')!
|
||||
a.scan(path: '${test_dir}/multi')!
|
||||
|
||||
assert a.collections.len == 2
|
||||
|
||||
// a.save(destination_meta: '/tmp/atlas_meta')!
|
||||
|
||||
// // Load from directory
|
||||
// mut a2 := new(name: 'loaded')!
|
||||
// a2.load_from_directory('${test_dir}/multi')!
|
||||
|
||||
// assert a2.collections.len == 2
|
||||
// assert a2.get_collection('col1')!.page_exists('page1')
|
||||
// assert a2.get_collection('col2')!.page_exists('page2')
|
||||
}
|
||||
|
||||
fn test_save_and_load_with_images() {
|
||||
col_path := '${test_dir}/docs_images'
|
||||
os.mkdir_all(col_path)!
|
||||
os.mkdir_all('${col_path}/img')!
|
||||
|
||||
mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)!
|
||||
cfile.write('name:docs')!
|
||||
|
||||
mut page := pathlib.get_file(path: '${col_path}/page.md', create: true)!
|
||||
page.write('# Page with image')!
|
||||
|
||||
// Create a dummy image file
|
||||
mut img := pathlib.get_file(path: '${col_path}/img/test.png', create: true)!
|
||||
img.write('fake png data')!
|
||||
|
||||
// Create and scan
|
||||
mut a := new(name: 'my_docs')!
|
||||
a.scan(path: '${test_dir}/docs_images')!
|
||||
|
||||
col := a.get_collection('docs')!
|
||||
// assert col.images.len == 1
|
||||
assert col.image_exists('test.png')!
|
||||
|
||||
// // Save
|
||||
// a.save(destination_meta: '/tmp/atlas_meta')!
|
||||
|
||||
// // Load
|
||||
// mut a2 := new(name: 'loaded')!
|
||||
// a2.load_from_directory('${test_dir}/docs_images')!
|
||||
|
||||
// loaded_col := a2.get_collection('docs')!
|
||||
// assert loaded_col.images.len == 1
|
||||
// assert loaded_col.image_exists('test.png')!
|
||||
|
||||
img_file := col.image_get('test.png')!
|
||||
assert img_file.name == 'test.png'
|
||||
assert img_file.is_image()
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
module atlas
|
||||
|
||||
import incubaid.herolib.core.texttools
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.ui.console
|
||||
import incubaid.herolib.data.paramsparser
|
||||
|
||||
__global (
|
||||
atlases shared map[string]&Atlas
|
||||
)
|
||||
|
||||
@[params]
|
||||
pub struct AtlasNewArgs {
|
||||
pub mut:
|
||||
name string = 'default'
|
||||
}
|
||||
|
||||
// Create a new Atlas
|
||||
pub fn new(args AtlasNewArgs) !&Atlas {
|
||||
mut name := texttools.name_fix(args.name)
|
||||
|
||||
mut a := &Atlas{
|
||||
name: name
|
||||
}
|
||||
|
||||
set(a)
|
||||
return a
|
||||
}
|
||||
|
||||
// Get Atlas from global map
|
||||
pub fn get(name string) !&Atlas {
|
||||
mut fixed_name := texttools.name_fix(name)
|
||||
rlock atlases {
|
||||
if fixed_name in atlases {
|
||||
return atlases[fixed_name] or { return error('Atlas ${name} not found') }
|
||||
}
|
||||
}
|
||||
return error("Atlas '${name}' not found")
|
||||
}
|
||||
|
||||
// Check if Atlas exists
|
||||
pub fn exists(name string) bool {
|
||||
mut fixed_name := texttools.name_fix(name)
|
||||
rlock atlases {
|
||||
return fixed_name in atlases
|
||||
}
|
||||
}
|
||||
|
||||
// List all Atlas names
|
||||
pub fn list() []string {
|
||||
rlock atlases {
|
||||
return atlases.keys()
|
||||
}
|
||||
}
|
||||
|
||||
// Store Atlas in global map
|
||||
fn set(atlas &Atlas) {
|
||||
lock atlases {
|
||||
atlases[atlas.name] = atlas
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
module atlas
|
||||
|
||||
// Get a page from any collection using format "collection:page"
|
||||
pub fn (a Atlas) page_get(key string) !&Page {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error('Invalid page key format. Use "collection:page" in page_get')
|
||||
}
|
||||
|
||||
col := a.get_collection(parts[0])!
|
||||
return col.page_get(parts[1])!
|
||||
}
|
||||
|
||||
// Get an image from any collection using format "collection:image"
|
||||
pub fn (a Atlas) image_get(key string) !&File {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error('Invalid image key format. Use "collection:image" in image_get')
|
||||
}
|
||||
|
||||
col := a.get_collection(parts[0])!
|
||||
return col.image_get(parts[1])!
|
||||
}
|
||||
|
||||
// Get a file from any collection using format "collection:file"
|
||||
pub fn (a Atlas) file_get(key string) !&File {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error('Invalid file key format. Use "collection:file" in file_get')
|
||||
}
|
||||
|
||||
col := a.get_collection(parts[0])!
|
||||
return col.file_get(parts[1])!
|
||||
}
|
||||
|
||||
// Get a file (can be image) from any collection using format "collection:file"
|
||||
pub fn (a Atlas) file_or_image_get(key string) !&File {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error('Invalid file key format. Use "collection:file"')
|
||||
}
|
||||
col := a.get_collection(parts[0])!
|
||||
return col.file_or_image_get(parts[1])!
|
||||
}
|
||||
|
||||
// Check if page exists
|
||||
pub fn (a Atlas) page_exists(key string) !bool {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error("Invalid file key format. Use 'collection:file' in page_exists")
|
||||
}
|
||||
|
||||
col := a.get_collection(parts[0]) or { return false }
|
||||
return col.page_exists(parts[1])
|
||||
}
|
||||
|
||||
// Check if image exists
|
||||
pub fn (a Atlas) image_exists(key string) !bool {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error("Invalid file key format. Use 'collection:file' in image_exists")
|
||||
}
|
||||
|
||||
col := a.get_collection(parts[0]) or { return false }
|
||||
return col.image_exists(parts[1])
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
pub fn (a Atlas) file_exists(key string) !bool {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error("Invalid file key format. Use 'collection:file' in file_exists")
|
||||
}
|
||||
|
||||
col := a.get_collection(parts[0]) or { return false }
|
||||
return col.file_exists(parts[1])
|
||||
}
|
||||
|
||||
pub fn (a Atlas) file_or_image_exists(key string) !bool {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error("Invalid file key format. Use 'collection:file' in file_or_image_exists")
|
||||
}
|
||||
col := a.get_collection(parts[0]) or { return false }
|
||||
return col.file_or_image_exists(parts[1])
|
||||
}
|
||||
|
||||
// List all pages in Atlas
|
||||
pub fn (a Atlas) list_pages() map[string][]string {
|
||||
mut result := map[string][]string{}
|
||||
|
||||
for col_name, col in a.collections {
|
||||
mut page_names := []string{}
|
||||
for page_name, _ in col.pages {
|
||||
page_names << page_name
|
||||
}
|
||||
page_names.sort()
|
||||
result[col_name] = page_names
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
|
||||
- first find all pages
|
||||
- then for each page find all links
|
||||
@@ -1,10 +1,10 @@
|
||||
# AtlasClient
|
||||
# DocTreeClient
|
||||
|
||||
A simple API for accessing document collections exported by the `atlas` module.
|
||||
A simple API for accessing document collections exported by the `doctree` module.
|
||||
|
||||
## What It Does
|
||||
|
||||
AtlasClient provides methods to:
|
||||
DocTreeClient provides methods to:
|
||||
|
||||
- List collections, pages, files, and images
|
||||
- Check if resources exist
|
||||
@@ -15,10 +15,10 @@ AtlasClient provides methods to:
|
||||
## Quick Start
|
||||
|
||||
```v
|
||||
import incubaid.herolib.web.atlas_client
|
||||
import incubaid.herolib.web.doctree_client
|
||||
|
||||
// Create client
|
||||
mut client := atlas_client.new(export_dir: '${os.home_dir()}/hero/var/atlas_export')!
|
||||
// Create client, exports will be in $/hero/var/doctree_export by default
|
||||
mut client := doctree_client.new()!
|
||||
|
||||
// List collections
|
||||
collections := client.list_collections()!
|
||||
@@ -34,7 +34,7 @@ if client.has_errors('my_collection')! {
|
||||
|
||||
## Export Structure
|
||||
|
||||
Atlas exports to this structure:
|
||||
DocTree exports to this structure:
|
||||
|
||||
```txt
|
||||
export_dir/
|
||||
@@ -87,9 +87,9 @@ Names are normalized using `name_fix()`:
|
||||
|
||||
## Example
|
||||
|
||||
See `examples/data/atlas_client/basic_usage.vsh` for a complete working example.
|
||||
See `examples/data/doctree_client/basic_usage.vsh` for a complete working example.
|
||||
|
||||
## See Also
|
||||
|
||||
- `lib/data/atlas/` - Atlas module for exporting collections
|
||||
- `lib/data/doctree/` - DocTree module for exporting collections
|
||||
- `lib/web/doctreeclient/` - Alternative client for doctree collections
|
||||
@@ -7,17 +7,17 @@ import os
|
||||
import json
|
||||
import incubaid.herolib.core.redisclient
|
||||
|
||||
// AtlasClient provides access to Atlas-exported documentation collections
|
||||
// DocTreeClient provides access to exported documentation collections
|
||||
// It reads from both the exported directory structure and Redis metadata
|
||||
pub struct AtlasClient {
|
||||
pub struct DocTreeClient {
|
||||
pub mut:
|
||||
redis &redisclient.Redis
|
||||
export_dir string // Path to the atlas export directory (contains content/ and meta/)
|
||||
export_dir string // Path to the doctree export directory (contains content/ and meta/)
|
||||
}
|
||||
|
||||
// get_page_path returns the path for a page in a collection
|
||||
// Pages are stored in {export_dir}/content/{collection}/{page}.md
|
||||
pub fn (mut c AtlasClient) get_page_path(collection_name string, page_name string) !string {
|
||||
pub fn (mut c DocTreeClient) get_page_path(collection_name string, page_name string) !string {
|
||||
// Apply name normalization
|
||||
fixed_collection_name := texttools.name_fix(collection_name)
|
||||
fixed_page_name := texttools.name_fix(page_name)
|
||||
@@ -40,9 +40,9 @@ pub fn (mut c AtlasClient) get_page_path(collection_name string, page_name strin
|
||||
|
||||
// get_file_path returns the path for a file in a collection
|
||||
// Files are stored in {export_dir}/content/{collection}/{filename}
|
||||
pub fn (mut c AtlasClient) get_file_path(collection_name_ string, file_name_ string) !string {
|
||||
collection_name := texttools.name_fix_no_ext(collection_name_)
|
||||
file_name := texttools.name_fix_keepext(file_name_)
|
||||
pub fn (mut c DocTreeClient) get_file_path(collection_name_ string, file_name_ string) !string {
|
||||
collection_name := texttools.name_fix(collection_name_)
|
||||
file_name := texttools.name_fix(file_name_)
|
||||
|
||||
// Check if export directory exists
|
||||
if !os.exists(c.export_dir) {
|
||||
@@ -62,11 +62,11 @@ pub fn (mut c AtlasClient) get_file_path(collection_name_ string, file_name_ str
|
||||
|
||||
// get_image_path returns the path for an image in a collection
|
||||
// Images are stored in {export_dir}/content/{collection}/{imagename}
|
||||
pub fn (mut c AtlasClient) get_image_path(collection_name_ string, image_name_ string) !string {
|
||||
pub fn (mut c DocTreeClient) get_image_path(collection_name_ string, image_name_ string) !string {
|
||||
// Apply name normalization
|
||||
collection_name := texttools.name_fix_no_ext(collection_name_)
|
||||
collection_name := texttools.name_fix(collection_name_)
|
||||
// Images keep their original names with extensions
|
||||
image_name := texttools.name_fix_keepext(image_name_)
|
||||
image_name := texttools.name_fix(image_name_)
|
||||
|
||||
// Check if export directory exists
|
||||
if !os.exists(c.export_dir) {
|
||||
@@ -85,28 +85,28 @@ pub fn (mut c AtlasClient) get_image_path(collection_name_ string, image_name_ s
|
||||
}
|
||||
|
||||
// page_exists checks if a page exists in a collection
|
||||
pub fn (mut c AtlasClient) page_exists(collection_name string, page_name string) bool {
|
||||
pub fn (mut c DocTreeClient) page_exists(collection_name string, page_name string) bool {
|
||||
// Try to get the page path - if it succeeds, the page exists
|
||||
_ := c.get_page_path(collection_name, page_name) or { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// file_exists checks if a file exists in a collection
|
||||
pub fn (mut c AtlasClient) file_exists(collection_name string, file_name string) bool {
|
||||
pub fn (mut c DocTreeClient) file_exists(collection_name string, file_name string) bool {
|
||||
// Try to get the file path - if it succeeds, the file exists
|
||||
_ := c.get_file_path(collection_name, file_name) or { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// image_exists checks if an image exists in a collection
|
||||
pub fn (mut c AtlasClient) image_exists(collection_name string, image_name string) bool {
|
||||
pub fn (mut c DocTreeClient) image_exists(collection_name string, image_name string) bool {
|
||||
// Try to get the image path - if it succeeds, the image exists
|
||||
_ := c.get_image_path(collection_name, image_name) or { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// get_page_content returns the content of a page in a collection
|
||||
pub fn (mut c AtlasClient) get_page_content(collection_name string, page_name string) !string {
|
||||
pub fn (mut c DocTreeClient) get_page_content(collection_name string, page_name string) !string {
|
||||
// Get the path for the page
|
||||
page_path := c.get_page_path(collection_name, page_name)!
|
||||
|
||||
@@ -124,7 +124,7 @@ pub fn (mut c AtlasClient) get_page_content(collection_name string, page_name st
|
||||
|
||||
// list_collections returns a list of all collection names
|
||||
// Collections are directories in {export_dir}/content/
|
||||
pub fn (mut c AtlasClient) list_collections() ![]string {
|
||||
pub fn (mut c DocTreeClient) list_collections() ![]string {
|
||||
content_dir := os.join_path(c.export_dir, 'content')
|
||||
|
||||
// Check if content directory exists
|
||||
@@ -148,7 +148,7 @@ pub fn (mut c AtlasClient) list_collections() ![]string {
|
||||
|
||||
// list_pages returns a list of all page names in a collection
|
||||
// Uses metadata to get the authoritative list of pages that belong to this collection
|
||||
pub fn (mut c AtlasClient) list_pages(collection_name string) ![]string {
|
||||
pub fn (mut c DocTreeClient) list_pages(collection_name string) ![]string {
|
||||
// Get metadata which contains the authoritative list of pages
|
||||
metadata := c.get_collection_metadata(collection_name)!
|
||||
|
||||
@@ -162,7 +162,7 @@ pub fn (mut c AtlasClient) list_pages(collection_name string) ![]string {
|
||||
}
|
||||
|
||||
// list_files returns a list of all file names in a collection (excluding pages and images)
|
||||
pub fn (mut c AtlasClient) list_files(collection_name string) ![]string {
|
||||
pub fn (mut c DocTreeClient) list_files(collection_name string) ![]string {
|
||||
metadata := c.get_collection_metadata(collection_name)!
|
||||
mut file_names := []string{}
|
||||
for file_name, file_meta in metadata.files {
|
||||
@@ -174,7 +174,7 @@ pub fn (mut c AtlasClient) list_files(collection_name string) ![]string {
|
||||
}
|
||||
|
||||
// list_images returns a list of all image names in a collection
|
||||
pub fn (mut c AtlasClient) list_images(collection_name string) ![]string {
|
||||
pub fn (mut c DocTreeClient) list_images(collection_name string) ![]string {
|
||||
metadata := c.get_collection_metadata(collection_name)!
|
||||
mut images := []string{}
|
||||
for file_name, file_meta in metadata.files {
|
||||
@@ -187,7 +187,7 @@ pub fn (mut c AtlasClient) list_images(collection_name string) ![]string {
|
||||
|
||||
// list_pages_map returns a map of collection names to a list of page names within that collection.
|
||||
// The structure is map[collectionname][]pagename.
|
||||
pub fn (mut c AtlasClient) list_pages_map() !map[string][]string {
|
||||
pub fn (mut c DocTreeClient) list_pages_map() !map[string][]string {
|
||||
mut result := map[string][]string{}
|
||||
collections := c.list_collections()!
|
||||
|
||||
@@ -199,38 +199,11 @@ pub fn (mut c AtlasClient) list_pages_map() !map[string][]string {
|
||||
return result
|
||||
}
|
||||
|
||||
// list_markdown returns the collections and their pages in markdown format.
|
||||
pub fn (mut c AtlasClient) list_markdown() !string {
|
||||
mut markdown_output := ''
|
||||
pages_map := c.list_pages_map()!
|
||||
|
||||
if pages_map.len == 0 {
|
||||
return 'No collections or pages found in this atlas export.'
|
||||
}
|
||||
|
||||
mut sorted_collections := pages_map.keys()
|
||||
sorted_collections.sort()
|
||||
|
||||
for col_name in sorted_collections {
|
||||
page_names := pages_map[col_name]
|
||||
markdown_output += '## ${col_name}\n'
|
||||
if page_names.len == 0 {
|
||||
markdown_output += ' * No pages in this collection.\n'
|
||||
} else {
|
||||
for page_name in page_names {
|
||||
markdown_output += ' * ${page_name}\n'
|
||||
}
|
||||
}
|
||||
markdown_output += '\n' // Add a newline for spacing between collections
|
||||
}
|
||||
return markdown_output
|
||||
}
|
||||
|
||||
// get_collection_metadata reads and parses the metadata JSON file for a collection
|
||||
// Metadata is stored in {export_dir}/meta/{collection}.json
|
||||
pub fn (mut c AtlasClient) get_collection_metadata(collection_name string) !CollectionMetadata {
|
||||
pub fn (mut c DocTreeClient) get_collection_metadata(collection_name string) !CollectionMetadata {
|
||||
// Apply name normalization
|
||||
fixed_collection_name := texttools.name_fix_no_ext(collection_name)
|
||||
fixed_collection_name := texttools.name_fix(collection_name)
|
||||
|
||||
meta_path := os.join_path(c.export_dir, 'meta', '${fixed_collection_name}.json')
|
||||
|
||||
@@ -247,78 +220,95 @@ pub fn (mut c AtlasClient) get_collection_metadata(collection_name string) !Coll
|
||||
return metadata
|
||||
}
|
||||
|
||||
// get_page_links returns the links found in a page by reading the metadata
|
||||
pub fn (mut c AtlasClient) get_page_links(collection_name string, page_name string) ![]LinkMetadata {
|
||||
// Get collection metadata
|
||||
metadata := c.get_collection_metadata(collection_name)!
|
||||
// Apply name normalization to page name
|
||||
fixed_page_name := texttools.name_fix_no_ext(page_name)
|
||||
|
||||
// Find the page in metadata
|
||||
if fixed_page_name in metadata.pages {
|
||||
return metadata.pages[fixed_page_name].links
|
||||
}
|
||||
return error('page_not_found: Page "${page_name}" not found in collection metadata, for collection: "${collection_name}"')
|
||||
}
|
||||
|
||||
// get_collection_errors returns the errors for a collection from metadata
|
||||
pub fn (mut c AtlasClient) get_collection_errors(collection_name string) ![]ErrorMetadata {
|
||||
pub fn (mut c DocTreeClient) get_collection_errors(collection_name string) ![]ErrorMetadata {
|
||||
metadata := c.get_collection_metadata(collection_name)!
|
||||
return metadata.errors
|
||||
}
|
||||
|
||||
// has_errors checks if a collection has any errors
|
||||
pub fn (mut c AtlasClient) has_errors(collection_name string) bool {
|
||||
pub fn (mut c DocTreeClient) has_errors(collection_name string) bool {
|
||||
errors := c.get_collection_errors(collection_name) or { return false }
|
||||
return errors.len > 0
|
||||
}
|
||||
|
||||
pub fn (mut c AtlasClient) copy_images(collection_name string, page_name string, destination_path string) ! {
|
||||
// Get page links from metadata
|
||||
links := c.get_page_links(collection_name, page_name)!
|
||||
|
||||
// Create img subdirectory
|
||||
mut img_dest := pathlib.get_dir(path: '${destination_path}/img', create: true)!
|
||||
|
||||
// Copy only image links
|
||||
for link in links {
|
||||
if link.file_type != .image {
|
||||
continue
|
||||
}
|
||||
if link.status == .external {
|
||||
continue
|
||||
}
|
||||
// Get image path and copy
|
||||
img_path := c.get_image_path(link.target_collection_name, link.target_item_name)!
|
||||
mut src := pathlib.get_file(path: img_path)!
|
||||
src.copy(dest: '${img_dest.path}/${src.name_fix_keepext()}')!
|
||||
// console.print_debug('Copied image: ${src.path} to ${img_dest.path}/${src.name_fix_keepext()}')
|
||||
}
|
||||
pub fn (mut c DocTreeClient) copy_collection(collection_name string, destination_path string) ! {
|
||||
// TODO: list over all pages, links & files and copy them to destination
|
||||
|
||||
}
|
||||
|
||||
// copy_files copies all non-image files from a page to a destination directory
|
||||
// Files are placed in {destination}/files/ subdirectory
|
||||
// Only copies files referenced in the page (via links)
|
||||
pub fn (mut c AtlasClient) copy_files(collection_name string, page_name string, destination_path string) ! {
|
||||
// Get page links from metadata
|
||||
links := c.get_page_links(collection_name, page_name)!
|
||||
// // will copy all pages linked from a page to a destination directory as well as the page itself
|
||||
// pub fn (mut c DocTreeClient) copy_pages(collection_name string, page_name string, destination_path string) ! {
|
||||
// // TODO: copy page itself
|
||||
|
||||
// Create files subdirectory
|
||||
mut files_dest := pathlib.get_dir(path: '${destination_path}/files', create: true)!
|
||||
// // Get page links from metadata
|
||||
// links := c.get_page_links(collection_name, page_name)!
|
||||
|
||||
// Copy only file links (non-image files)
|
||||
for link in links {
|
||||
if link.file_type != .file {
|
||||
continue
|
||||
}
|
||||
if link.status == .external {
|
||||
continue
|
||||
}
|
||||
// println(link)
|
||||
// Get file path and copy
|
||||
file_path := c.get_file_path(link.target_collection_name, link.target_item_name)!
|
||||
mut src := pathlib.get_file(path: file_path)!
|
||||
// src.copy(dest: '${files_dest.path}/${src.name_fix_keepext()}')!
|
||||
console.print_debug('Copied file: ${src.path} to ${files_dest.path}/${src.name_fix_keepext()}')
|
||||
}
|
||||
}
|
||||
// // Create img subdirectory
|
||||
// mut img_dest := pathlib.get_dir(path: '${destination_path}', create: true)!
|
||||
|
||||
// // Copy only image links
|
||||
// for link in links {
|
||||
// if link.file_type != .page {
|
||||
// continue
|
||||
// }
|
||||
// if link.status == .external {
|
||||
// continue
|
||||
// }
|
||||
// // Get image path and copy
|
||||
// img_path := c.get_page_path(link.target_collection_name, link.target_item_name)!
|
||||
// mut src := pathlib.get_file(path: img_path)!
|
||||
// src.copy(dest: '${img_dest.path}/${src.name_fix_no_ext()}')!
|
||||
// console.print_debug(' ********. Copied page: ${src.path} to ${img_dest.path}/${src.name_fix_no_ext()}')
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn (mut c DocTreeClient) copy_images(collection_name string, page_name string, destination_path string) ! {
|
||||
// // Get page links from metadata
|
||||
// links := c.get_page_links(collection_name, page_name)!
|
||||
|
||||
// // Create img subdirectory
|
||||
// mut img_dest := pathlib.get_dir(path: '${destination_path}/img', create: true)!
|
||||
|
||||
// // Copy only image links
|
||||
// for link in links {
|
||||
// if link.file_type != .image {
|
||||
// continue
|
||||
// }
|
||||
// if link.status == .external {
|
||||
// continue
|
||||
// }
|
||||
// // Get image path and copy
|
||||
// img_path := c.get_image_path(link.target_collection_name, link.target_item_name)!
|
||||
// mut src := pathlib.get_file(path: img_path)!
|
||||
// src.copy(dest: '${img_dest.path}/${src.name_fix_no_ext()}')!
|
||||
// // console.print_debug('Copied image: ${src.path} to ${img_dest.path}/${src.name_fix()}')
|
||||
// }
|
||||
// }
|
||||
|
||||
// // copy_files copies all non-image files from a page to a destination directory
|
||||
// // Files are placed in {destination}/files/ subdirectory
|
||||
// // Only copies files referenced in the page (via links)
|
||||
// pub fn (mut c DocTreeClient) copy_files(collection_name string, page_name string, destination_path string) ! {
|
||||
// // Get page links from metadata
|
||||
// links := c.get_page_links(collection_name, page_name)!
|
||||
|
||||
// // Create files subdirectory
|
||||
// mut files_dest := pathlib.get_dir(path: '${destination_path}/files', create: true)!
|
||||
|
||||
// // Copy only file links (non-image files)
|
||||
// for link in links {
|
||||
// if link.file_type != .file {
|
||||
// continue
|
||||
// }
|
||||
// if link.status == .external {
|
||||
// continue
|
||||
// }
|
||||
// // println(link)
|
||||
// // Get file path and copy
|
||||
// file_path := c.get_file_path(link.target_collection_name, link.target_item_name)!
|
||||
// mut src := pathlib.get_file(path: file_path)!
|
||||
// // src.copy(dest: '${files_dest.path}/${src.name_fix_no_ext()}')!
|
||||
// console.print_debug('Copied file: ${src.path} to ${files_dest.path}/${src.name_fix_no_ext()}')
|
||||
// }
|
||||
// }
|
||||
@@ -5,7 +5,7 @@ import incubaid.herolib.core.texttools
|
||||
|
||||
// Helper function to create a test export directory structure
|
||||
fn setup_test_export() string {
|
||||
test_dir := os.join_path(os.temp_dir(), 'atlas_client_test_${os.getpid()}')
|
||||
test_dir := os.join_path(os.temp_dir(), 'doctree_client_test_${os.getpid()}')
|
||||
|
||||
// Clean up if exists
|
||||
if os.exists(test_dir) {
|
||||
@@ -54,28 +54,7 @@ fn setup_test_export() string {
|
||||
"name": "page2",
|
||||
"path": "",
|
||||
"collection_name": "testcollection",
|
||||
"links": [
|
||||
{
|
||||
"src": "logo.png",
|
||||
"text": "logo",
|
||||
"target": "logo.png",
|
||||
"line": 3,
|
||||
"target_collection_name": "testcollection",
|
||||
"target_item_name": "logo.png",
|
||||
"status": "ok",
|
||||
"file_type": "image"
|
||||
},
|
||||
{
|
||||
"src": "data.csv",
|
||||
"text": "data",
|
||||
"target": "data.csv",
|
||||
"line": 4,
|
||||
"target_collection_name": "testcollection",
|
||||
"target_item_name": "data.csv",
|
||||
"status": "ok",
|
||||
"file_type": "file"
|
||||
}
|
||||
]
|
||||
"links": []
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
@@ -110,14 +89,7 @@ fn setup_test_export() string {
|
||||
}
|
||||
},
|
||||
"files": {},
|
||||
"errors": [
|
||||
{
|
||||
"category": "test",
|
||||
"page_key": "intro",
|
||||
"message": "Test error",
|
||||
"line": 10
|
||||
}
|
||||
]
|
||||
"errors": []
|
||||
}'
|
||||
os.write_file(os.join_path(test_dir, 'meta', 'anothercollection.json'), metadata2) or {
|
||||
panic(err)
|
||||
@@ -455,23 +427,6 @@ fn test_list_pages_map() {
|
||||
assert pages_map['anothercollection'].len == 1
|
||||
}
|
||||
|
||||
// Test list_markdown
|
||||
fn test_list_markdown() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
markdown := client.list_markdown() or { panic(err) }
|
||||
|
||||
assert markdown.contains('testcollection')
|
||||
assert markdown.contains('anothercollection')
|
||||
assert markdown.contains('page1')
|
||||
assert markdown.contains('page2')
|
||||
assert markdown.contains('intro')
|
||||
assert markdown.contains('##')
|
||||
assert markdown.contains('*')
|
||||
}
|
||||
|
||||
// Test get_collection_metadata - success
|
||||
fn test_get_collection_metadata_success() {
|
||||
test_dir := setup_test_export()
|
||||
@@ -485,21 +440,6 @@ fn test_get_collection_metadata_success() {
|
||||
assert metadata.errors.len == 0
|
||||
}
|
||||
|
||||
// Test get_collection_metadata - with errors
|
||||
fn test_get_collection_metadata_with_errors() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
metadata := client.get_collection_metadata('anothercollection') or { panic(err) }
|
||||
|
||||
assert metadata.name == 'anothercollection'
|
||||
assert metadata.pages.len == 1
|
||||
assert metadata.errors.len == 1
|
||||
assert metadata.errors[0].message == 'Test error'
|
||||
assert metadata.errors[0].line == 10
|
||||
}
|
||||
|
||||
// Test get_collection_metadata - not found
|
||||
fn test_get_collection_metadata_not_found() {
|
||||
test_dir := setup_test_export()
|
||||
@@ -513,78 +453,17 @@ fn test_get_collection_metadata_not_found() {
|
||||
assert false, 'Should have returned an error'
|
||||
}
|
||||
|
||||
// Test get_page_links - success
|
||||
fn test_get_page_links_success() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
links := client.get_page_links('testcollection', 'page2') or { panic(err) }
|
||||
|
||||
assert links.len == 2
|
||||
assert links[0].target_item_name == 'logo.png'
|
||||
assert links[0].target_collection_name == 'testcollection'
|
||||
assert links[0].file_type == .image
|
||||
}
|
||||
|
||||
// Test get_page_links - no links
|
||||
fn test_get_page_links_empty() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
links := client.get_page_links('testcollection', 'page1') or { panic(err) }
|
||||
|
||||
assert links.len == 0
|
||||
}
|
||||
|
||||
// Test get_page_links - page not found
|
||||
fn test_get_page_links_page_not_found() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
client.get_page_links('testcollection', 'nonexistent') or {
|
||||
assert err.msg().contains('page_not_found')
|
||||
return
|
||||
}
|
||||
assert false, 'Should have returned an error'
|
||||
}
|
||||
|
||||
// Test get_collection_errors - success
|
||||
fn test_get_collection_errors_success() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
errors := client.get_collection_errors('anothercollection') or { panic(err) }
|
||||
|
||||
assert errors.len == 1
|
||||
assert errors[0].message == 'Test error'
|
||||
}
|
||||
|
||||
// Test get_collection_errors - no errors
|
||||
fn test_get_collection_errors_empty() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
errors := client.get_collection_errors('testcollection') or { panic(err) }
|
||||
|
||||
assert errors.len == 0
|
||||
}
|
||||
|
||||
// Test has_errors - true
|
||||
fn test_has_errors_true() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
has_errors := client.has_errors('anothercollection')
|
||||
|
||||
assert has_errors == true
|
||||
}
|
||||
|
||||
// Test has_errors - false
|
||||
fn test_has_errors_false() {
|
||||
test_dir := setup_test_export()
|
||||
@@ -596,7 +475,7 @@ fn test_has_errors_false() {
|
||||
assert has_errors == false
|
||||
}
|
||||
|
||||
// Test has_errors - collection not found
|
||||
// Test has_errors - collection not found returns false
|
||||
fn test_has_errors_collection_not_found() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
@@ -613,64 +492,16 @@ fn test_copy_images_success() {
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
dest_dir := os.join_path(os.temp_dir(), 'copy_dest_${os.getpid()}')
|
||||
defer { os.rmdir_all(dest_dir) or {} }
|
||||
|
||||
os.mkdir_all(dest_dir) or { panic(err) }
|
||||
defer { cleanup_test_export(dest_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
client.copy_images('testcollection', 'page2', dest_dir) or { panic(err) }
|
||||
|
||||
// Check that logo.png was copied to img subdirectory
|
||||
assert os.exists(os.join_path(dest_dir, 'img', 'logo.png'))
|
||||
}
|
||||
|
||||
// Test copy_images - no images
|
||||
fn test_copy_images_no_images() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
dest_dir := os.join_path(os.temp_dir(), 'copy_dest_empty_${os.getpid()}')
|
||||
os.mkdir_all(dest_dir) or { panic(err) }
|
||||
defer { cleanup_test_export(dest_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
client.copy_images('testcollection', 'page1', dest_dir) or { panic(err) }
|
||||
|
||||
// Should succeed even with no images
|
||||
assert true
|
||||
}
|
||||
|
||||
// Test copy_files - success
|
||||
fn test_copy_files_success() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
dest_dir := os.join_path(os.temp_dir(), 'copy_files_dest_${os.getpid()}')
|
||||
os.mkdir_all(dest_dir) or { panic(err) }
|
||||
defer { cleanup_test_export(dest_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
// Note: test data would need to be updated to have file links in page2
|
||||
// For now, this test demonstrates the pattern
|
||||
client.copy_files('testcollection', 'page2', dest_dir) or { panic(err) }
|
||||
|
||||
// Check that files were copied to files subdirectory
|
||||
// assert os.exists(os.join_path(dest_dir, 'files', 'somefile.csv'))
|
||||
}
|
||||
|
||||
// Test copy_files - no files
|
||||
fn test_copy_files_no_files() {
|
||||
test_dir := setup_test_export()
|
||||
defer { cleanup_test_export(test_dir) }
|
||||
|
||||
dest_dir := os.join_path(os.temp_dir(), 'copy_files_empty_${os.getpid()}')
|
||||
os.mkdir_all(dest_dir) or { panic(err) }
|
||||
defer { cleanup_test_export(dest_dir) }
|
||||
|
||||
mut client := new(export_dir: test_dir) or { panic(err) }
|
||||
client.copy_files('testcollection', 'page1', dest_dir) or { panic(err) }
|
||||
|
||||
// Should succeed even with no file links
|
||||
assert true
|
||||
// Check that images were copied to img subdirectory
|
||||
assert os.exists(os.join_path(dest_dir, 'img', 'logo.png'))
|
||||
assert os.exists(os.join_path(dest_dir, 'img', 'banner.jpg'))
|
||||
}
|
||||
|
||||
// Test naming normalization edge cases
|
||||
@@ -3,18 +3,18 @@ module client
|
||||
import incubaid.herolib.core.base
|
||||
|
||||
@[params]
|
||||
pub struct AtlasClientArgs {
|
||||
pub struct DocTreeClientArgs {
|
||||
pub:
|
||||
export_dir string @[required] // Path to atlas export directory
|
||||
export_dir string @[required] // Path to doctree export directory
|
||||
}
|
||||
|
||||
// Create a new AtlasClient instance
|
||||
// Create a new DocTreeClient instance
|
||||
// The export_dir should point to the directory containing content/ and meta/ subdirectories
|
||||
pub fn new(args AtlasClientArgs) !&AtlasClient {
|
||||
pub fn new(args DocTreeClientArgs) !&DocTreeClient {
|
||||
mut context := base.context()!
|
||||
mut redis := context.redis()!
|
||||
|
||||
return &AtlasClient{
|
||||
return &DocTreeClient{
|
||||
redis: redis
|
||||
export_dir: args.export_dir
|
||||
}
|
||||
28
lib/web/doctree/client/markdown.v
Normal file
28
lib/web/doctree/client/markdown.v
Normal file
@@ -0,0 +1,28 @@
|
||||
module client
|
||||
|
||||
// list_markdown returns the collections and their pages in markdown format.
|
||||
pub fn (mut c DocTreeClient) list_markdown() !string {
|
||||
mut markdown_output := ''
|
||||
pages_map := c.list_pages_map()!
|
||||
|
||||
if pages_map.len == 0 {
|
||||
return 'No collections or pages found in this doctree export.'
|
||||
}
|
||||
|
||||
mut sorted_collections := pages_map.keys()
|
||||
sorted_collections.sort()
|
||||
|
||||
for col_name in sorted_collections {
|
||||
page_names := pages_map[col_name]
|
||||
markdown_output += '## ${col_name}\n'
|
||||
if page_names.len == 0 {
|
||||
markdown_output += ' * No pages in this collection.\n'
|
||||
} else {
|
||||
for page_name in page_names {
|
||||
markdown_output += ' * ${page_name}\n'
|
||||
}
|
||||
}
|
||||
markdown_output += '\n' // Add a newline for spacing between collections
|
||||
}
|
||||
return markdown_output
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
module client
|
||||
|
||||
// AtlasClient provides access to Atlas-exported documentation collections
|
||||
// DocTreeClient provides access to DocTree-exported documentation collections
|
||||
// It reads from both the exported directory structure and Redis metadata
|
||||
|
||||
// List of recognized image file extensions
|
||||
@@ -22,6 +22,16 @@ pub mut:
|
||||
path string
|
||||
collection_name string
|
||||
links []LinkMetadata
|
||||
title string
|
||||
description string
|
||||
questions []Question
|
||||
|
||||
}
|
||||
|
||||
pub struct Question {
|
||||
pub mut:
|
||||
question string
|
||||
answer string
|
||||
}
|
||||
|
||||
pub struct FileMetadata {
|
||||
@@ -1,11 +1,11 @@
|
||||
module atlas
|
||||
module core
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.core.texttools
|
||||
import incubaid.herolib.develop.gittools
|
||||
import incubaid.herolib.web.doctree as doctreetools
|
||||
|
||||
import incubaid.herolib.data.paramsparser { Params }
|
||||
import incubaid.herolib.ui.console
|
||||
import os
|
||||
|
||||
|
||||
pub struct Session {
|
||||
pub mut:
|
||||
@@ -21,7 +21,7 @@ pub mut:
|
||||
path string // absolute path
|
||||
pages map[string]&Page
|
||||
files map[string]&File
|
||||
atlas &Atlas @[skip; str: skip]
|
||||
doctree &DocTree @[skip; str: skip]
|
||||
errors []CollectionError
|
||||
error_cache map[string]bool
|
||||
git_url string
|
||||
@@ -41,7 +41,7 @@ fn (mut c Collection) init_pre() ! {
|
||||
}
|
||||
|
||||
fn (mut c Collection) init_post() ! {
|
||||
c.validate_links()!
|
||||
c.find_links()!
|
||||
c.init_git_info()!
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ fn (mut c Collection) add_file(mut p pathlib.Path) ! {
|
||||
|
||||
// Get a page by name
|
||||
pub fn (c Collection) page_get(name_ string) !&Page {
|
||||
name := texttools.name_fix_no_ext(name_)
|
||||
name := doctreetools.name_fix(name_)
|
||||
return c.pages[name] or { return PageNotFound{
|
||||
collection: c.name
|
||||
page: name
|
||||
@@ -104,7 +104,7 @@ pub fn (c Collection) page_get(name_ string) !&Page {
|
||||
|
||||
// Get an image by name
|
||||
pub fn (c Collection) image_get(name_ string) !&File {
|
||||
name := texttools.name_fix_keepext(name_)
|
||||
name := doctreetools.name_fix(name_)
|
||||
mut img := c.files[name] or { return FileNotFound{
|
||||
collection: c.name
|
||||
file: name
|
||||
@@ -117,7 +117,7 @@ pub fn (c Collection) image_get(name_ string) !&File {
|
||||
|
||||
// Get a file by name
|
||||
pub fn (c Collection) file_get(name_ string) !&File {
|
||||
name := texttools.name_fix_keepext(name_)
|
||||
name := doctreetools.name_fix(name_)
|
||||
mut f := c.files[name] or { return FileNotFound{
|
||||
collection: c.name
|
||||
file: name
|
||||
@@ -129,7 +129,7 @@ pub fn (c Collection) file_get(name_ string) !&File {
|
||||
}
|
||||
|
||||
pub fn (c Collection) file_or_image_get(name_ string) !&File {
|
||||
name := texttools.name_fix_keepext(name_)
|
||||
name := doctreetools.name_fix(name_)
|
||||
mut f := c.files[name] or { return FileNotFound{
|
||||
collection: c.name
|
||||
file: name
|
||||
@@ -139,26 +139,26 @@ pub fn (c Collection) file_or_image_get(name_ string) !&File {
|
||||
|
||||
// Check if page exists
|
||||
pub fn (c Collection) page_exists(name_ string) !bool {
|
||||
name := texttools.name_fix_no_ext(name_)
|
||||
name := doctreetools.name_fix(name_)
|
||||
return name in c.pages
|
||||
}
|
||||
|
||||
// Check if image exists
|
||||
pub fn (c Collection) image_exists(name_ string) !bool {
|
||||
name := texttools.name_fix_keepext(name_)
|
||||
name := doctreetools.name_fix(name_)
|
||||
f := c.files[name] or { return false }
|
||||
return f.ftype == .image
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
pub fn (c Collection) file_exists(name_ string) !bool {
|
||||
name := texttools.name_fix_keepext(name_)
|
||||
name := doctreetools.name_fix(name_)
|
||||
f := c.files[name] or { return false }
|
||||
return f.ftype == .file
|
||||
}
|
||||
|
||||
pub fn (c Collection) file_or_image_exists(name_ string) !bool {
|
||||
name := texttools.name_fix_keepext(name_)
|
||||
name := doctreetools.name_fix(name_)
|
||||
_ := c.files[name] or { return false }
|
||||
return true
|
||||
}
|
||||
@@ -247,31 +247,6 @@ pub fn (c Collection) print_errors() {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate all links in collection
|
||||
pub fn (mut c Collection) validate_links() ! {
|
||||
for _, mut page in c.pages {
|
||||
content := page.content(include: true)!
|
||||
page.links = page.find_links(content)! // will walk over links see if errors and add errors
|
||||
}
|
||||
}
|
||||
|
||||
// Fix all links in collection (rewrite files)
|
||||
pub fn (mut c Collection) fix_links() ! {
|
||||
for _, mut page in c.pages {
|
||||
// Read original content
|
||||
content := page.content()!
|
||||
|
||||
// Fix links
|
||||
fixed_content := page.content_with_fixed_links()!
|
||||
|
||||
// Write back if changed
|
||||
if fixed_content != content {
|
||||
mut p := page.path()!
|
||||
p.write(fixed_content)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if session can read this collection
|
||||
pub fn (c Collection) can_read(session Session) bool {
|
||||
// If no ACL set, everyone can read
|
||||
@@ -280,8 +255,8 @@ pub fn (c Collection) can_read(session Session) bool {
|
||||
}
|
||||
|
||||
// Get user's groups
|
||||
mut atlas := c.atlas
|
||||
groups := atlas.groups_get(session)
|
||||
mut doctree := c.doctree
|
||||
groups := doctree.groups_get(session)
|
||||
group_names := groups.map(it.name)
|
||||
|
||||
// Check if any of user's groups are in read ACL
|
||||
@@ -302,8 +277,8 @@ pub fn (c Collection) can_write(session Session) bool {
|
||||
}
|
||||
|
||||
// Get user's groups
|
||||
mut atlas := c.atlas
|
||||
groups := atlas.groups_get(session)
|
||||
mut doctree := c.doctree
|
||||
groups := doctree.groups_get(session)
|
||||
group_names := groups.map(it.name)
|
||||
|
||||
// Check if any of user's groups are in write ACL
|
||||
@@ -315,104 +290,3 @@ pub fn (c Collection) can_write(session Session) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Detect git repository URL for a collection
|
||||
fn (mut c Collection) init_git_info() ! {
|
||||
mut current_path := c.path()!
|
||||
|
||||
// Walk up directory tree to find .git
|
||||
mut git_repo := current_path.parent_find('.git') or {
|
||||
// No git repo found
|
||||
return
|
||||
}
|
||||
|
||||
if git_repo.path == '' {
|
||||
panic('Unexpected empty git repo path')
|
||||
}
|
||||
|
||||
mut gs := gittools.new()!
|
||||
mut p := c.path()!
|
||||
mut location := gs.gitlocation_from_path(p.path)!
|
||||
|
||||
r := os.execute_opt('cd ${p.path} && git branch --show-current')!
|
||||
|
||||
location.branch_or_tag = r.output.trim_space()
|
||||
|
||||
c.git_url = location.web_url()!
|
||||
}
|
||||
|
||||
////////////SCANNING FUNCTIONS ?//////////////////////////////////////////////////////
|
||||
|
||||
fn (mut c Collection) scan(mut dir pathlib.Path) ! {
|
||||
mut entries := dir.list(recursive: false)!
|
||||
|
||||
for mut entry in entries.paths {
|
||||
// Skip hidden files/dirs
|
||||
if entry.name().starts_with('.') || entry.name().starts_with('_') {
|
||||
continue
|
||||
}
|
||||
|
||||
if entry.is_dir() {
|
||||
// Recursively scan subdirectories
|
||||
mut mutable_entry := entry
|
||||
c.scan(mut mutable_entry)!
|
||||
continue
|
||||
}
|
||||
|
||||
// Process files based on extension
|
||||
match entry.extension_lower() {
|
||||
'md' {
|
||||
mut mutable_entry := entry
|
||||
c.add_page(mut mutable_entry)!
|
||||
}
|
||||
else {
|
||||
mut mutable_entry := entry
|
||||
c.add_file(mut mutable_entry)!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan for ACL files
|
||||
fn (mut c Collection) scan_acl() ! {
|
||||
// Look for read.acl in collection directory
|
||||
read_acl_path := '${c.path()!.path}/read.acl'
|
||||
if os.exists(read_acl_path) {
|
||||
content := os.read_file(read_acl_path)!
|
||||
// Split by newlines and normalize
|
||||
c.acl_read = content.split('\n')
|
||||
.map(it.trim_space())
|
||||
.filter(it.len > 0)
|
||||
.map(it.to_lower())
|
||||
}
|
||||
|
||||
// Look for write.acl in collection directory
|
||||
write_acl_path := '${c.path()!.path}/write.acl'
|
||||
if os.exists(write_acl_path) {
|
||||
content := os.read_file(write_acl_path)!
|
||||
// Split by newlines and normalize
|
||||
c.acl_write = content.split('\n')
|
||||
.map(it.trim_space())
|
||||
.filter(it.len > 0)
|
||||
.map(it.to_lower())
|
||||
}
|
||||
}
|
||||
|
||||
// scan_groups scans the collection's directory for .group files and loads them into memory.
|
||||
pub fn (mut c Collection) scan_groups() ! {
|
||||
if c.name != 'groups' {
|
||||
return error('scan_groups only works on "groups" collection')
|
||||
}
|
||||
mut p := c.path()!
|
||||
mut entries := p.list(recursive: false)!
|
||||
|
||||
for mut entry in entries.paths {
|
||||
if entry.extension_lower() == 'group' {
|
||||
filename := entry.name_fix_no_ext()
|
||||
mut visited := map[string]bool{}
|
||||
mut group := parse_group_file(filename, c.path()!.path, mut visited)!
|
||||
|
||||
c.atlas.group_add(mut group)!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
module atlas
|
||||
module core
|
||||
|
||||
import crypto.md5
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
|
||||
pub enum CollectionErrorCategory {
|
||||
circular_include
|
||||
69
lib/web/doctree/core/collection_process.v
Normal file
69
lib/web/doctree/core/collection_process.v
Normal file
@@ -0,0 +1,69 @@
|
||||
module core
|
||||
|
||||
import incubaid.herolib.develop.gittools
|
||||
import os
|
||||
import incubaid.herolib.data.markdown.tools as markdowntools
|
||||
|
||||
// Validate all links in collection
|
||||
fn (mut c Collection) find_links() ! {
|
||||
for _, mut page in c.pages {
|
||||
content := page.content(include: true)!
|
||||
page.links = page.find_links(content)! // will walk over links see if errors and add errors
|
||||
}
|
||||
}
|
||||
|
||||
// Fix all links in collection (rewrite files)
|
||||
fn (mut c Collection) fix_links() ! {
|
||||
for _, mut page in c.pages {
|
||||
// Read original content
|
||||
content := page.content()!
|
||||
|
||||
// Fix links
|
||||
fixed_content := page.content_with_fixed_links()!
|
||||
|
||||
// Write back if changed
|
||||
if fixed_content != content {
|
||||
mut p := page.path()!
|
||||
p.write(fixed_content)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn (mut c Collection) title_descriptions() ! {
|
||||
for _, mut p in c.pages {
|
||||
if p.title == '' {
|
||||
p.title = markdowntools.extract_title(p.content(include: true)!)
|
||||
}
|
||||
// TODO in future should do AI
|
||||
if p.description == '' {
|
||||
p.description = p.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Detect git repository URL for a collection
|
||||
fn (mut c Collection) init_git_info() ! {
|
||||
mut current_path := c.path()!
|
||||
|
||||
// Walk up directory tree to find .git
|
||||
mut git_repo := current_path.parent_find('.git') or {
|
||||
// No git repo found
|
||||
return
|
||||
}
|
||||
|
||||
if git_repo.path == '' {
|
||||
panic('Unexpected empty git repo path')
|
||||
}
|
||||
|
||||
mut gs := gittools.new()!
|
||||
mut p := c.path()!
|
||||
mut location := gs.gitlocation_from_path(p.path)!
|
||||
|
||||
r := os.execute_opt('cd ${p.path} && git branch --show-current')!
|
||||
|
||||
location.branch_or_tag = r.output.trim_space()
|
||||
|
||||
c.git_url = location.web_url()!
|
||||
}
|
||||
84
lib/web/doctree/core/collection_scan.v
Normal file
84
lib/web/doctree/core/collection_scan.v
Normal file
@@ -0,0 +1,84 @@
|
||||
module core
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
|
||||
|
||||
|
||||
|
||||
import os
|
||||
|
||||
////////////SCANNING FUNCTIONS ?//////////////////////////////////////////////////////
|
||||
|
||||
fn (mut c Collection) scan(mut dir pathlib.Path) ! {
|
||||
mut entries := dir.list(recursive: false)!
|
||||
|
||||
for mut entry in entries.paths {
|
||||
// Skip hidden files/dirs
|
||||
if entry.name().starts_with('.') || entry.name().starts_with('_') {
|
||||
continue
|
||||
}
|
||||
|
||||
if entry.is_dir() {
|
||||
// Recursively scan subdirectories
|
||||
mut mutable_entry := entry
|
||||
c.scan(mut mutable_entry)!
|
||||
continue
|
||||
}
|
||||
|
||||
// Process files based on extension
|
||||
match entry.extension_lower() {
|
||||
'md' {
|
||||
mut mutable_entry := entry
|
||||
c.add_page(mut mutable_entry)!
|
||||
}
|
||||
else {
|
||||
mut mutable_entry := entry
|
||||
c.add_file(mut mutable_entry)!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan for ACL files
|
||||
fn (mut c Collection) scan_acl() ! {
|
||||
// Look for read.acl in collection directory
|
||||
read_acl_path := '${c.path()!.path}/read.acl'
|
||||
if os.exists(read_acl_path) {
|
||||
content := os.read_file(read_acl_path)!
|
||||
// Split by newlines and normalize
|
||||
c.acl_read = content.split('\n')
|
||||
.map(it.trim_space())
|
||||
.filter(it.len > 0)
|
||||
.map(it.to_lower())
|
||||
}
|
||||
|
||||
// Look for write.acl in collection directory
|
||||
write_acl_path := '${c.path()!.path}/write.acl'
|
||||
if os.exists(write_acl_path) {
|
||||
content := os.read_file(write_acl_path)!
|
||||
// Split by newlines and normalize
|
||||
c.acl_write = content.split('\n')
|
||||
.map(it.trim_space())
|
||||
.filter(it.len > 0)
|
||||
.map(it.to_lower())
|
||||
}
|
||||
}
|
||||
|
||||
// scan_groups scans the collection's directory for .group files and loads them into memory.
|
||||
pub fn (mut c Collection) scan_groups() ! {
|
||||
if c.name != 'groups' {
|
||||
return error('scan_groups only works on "groups" collection')
|
||||
}
|
||||
mut p := c.path()!
|
||||
mut entries := p.list(recursive: false)!
|
||||
|
||||
for mut entry in entries.paths {
|
||||
if entry.extension_lower() == 'group' {
|
||||
filename := entry.name_fix_no_ext()
|
||||
mut visited := map[string]bool{}
|
||||
mut group := parse_group_file(filename, c.path()!.path, mut visited)!
|
||||
|
||||
c.doctree.group_add(mut group)!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
module atlas
|
||||
module core
|
||||
|
||||
import incubaid.herolib.core.texttools
|
||||
import incubaid.herolib.web.doctree
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.ui.console
|
||||
import incubaid.herolib.data.paramsparser
|
||||
|
||||
@[heap]
|
||||
pub struct Atlas {
|
||||
pub struct DocTree {
|
||||
pub mut:
|
||||
name string
|
||||
collections map[string]&Collection
|
||||
@@ -14,7 +14,7 @@ pub mut:
|
||||
}
|
||||
|
||||
// Create a new collection
|
||||
fn (mut self Atlas) add_collection(mut path pathlib.Path) !Collection {
|
||||
fn (mut self DocTree) add_collection(mut path pathlib.Path) !Collection {
|
||||
mut name := path.name_fix_no_ext()
|
||||
mut filepath := path.file_get('.collection')!
|
||||
content := filepath.read()!
|
||||
@@ -24,18 +24,17 @@ fn (mut self Atlas) add_collection(mut path pathlib.Path) !Collection {
|
||||
name = params.get('name')!
|
||||
}
|
||||
}
|
||||
|
||||
name = texttools.name_fix(name)
|
||||
console.print_item("Adding collection '${name}' to Atlas '${self.name}' at path '${path.path}'")
|
||||
name = doctree.name_fix(name)
|
||||
console.print_item("Adding collection '${name}' to DocTree '${self.name}' at path '${path.path}'")
|
||||
|
||||
if name in self.collections {
|
||||
return error('Collection ${name} already exists in Atlas ${self.name}')
|
||||
return error('Collection ${name} already exists in DocTree ${self.name}')
|
||||
}
|
||||
|
||||
mut c := Collection{
|
||||
name: name
|
||||
path: path.path // absolute path
|
||||
atlas: &self // Set atlas reference
|
||||
doctree: &self // Set doctree reference
|
||||
error_cache: map[string]bool{}
|
||||
}
|
||||
|
||||
@@ -47,38 +46,24 @@ fn (mut self Atlas) add_collection(mut path pathlib.Path) !Collection {
|
||||
}
|
||||
|
||||
// Get a collection by name
|
||||
pub fn (a Atlas) get_collection(name string) !&Collection {
|
||||
pub fn (a DocTree) get_collection(name string) !&Collection {
|
||||
return a.collections[name] or {
|
||||
return CollectionNotFound{
|
||||
name: name
|
||||
msg: 'Collection not found in Atlas ${a.name}'
|
||||
msg: 'Collection not found in DocTree ${a.name}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate all links in all collections
|
||||
pub fn (mut a Atlas) init_post() ! {
|
||||
pub fn (mut a DocTree) init_post() ! {
|
||||
for _, mut col in a.collections {
|
||||
col.init_post()!
|
||||
}
|
||||
}
|
||||
|
||||
// Validate all links in all collections
|
||||
pub fn (mut a Atlas) validate_links() ! {
|
||||
for _, mut col in a.collections {
|
||||
col.validate_links()!
|
||||
}
|
||||
}
|
||||
|
||||
// Fix all links in all collections (rewrite source files)
|
||||
pub fn (mut a Atlas) fix_links() ! {
|
||||
for _, mut col in a.collections {
|
||||
col.fix_links()!
|
||||
}
|
||||
}
|
||||
|
||||
// Add a group to the atlas
|
||||
pub fn (mut a Atlas) group_add(mut group Group) ! {
|
||||
// Add a group to the doctree
|
||||
pub fn (mut a DocTree) group_add(mut group Group) ! {
|
||||
if group.name in a.groups {
|
||||
return error('Group ${group.name} already exists')
|
||||
}
|
||||
@@ -86,13 +71,13 @@ pub fn (mut a Atlas) group_add(mut group Group) ! {
|
||||
}
|
||||
|
||||
// Get a group by name
|
||||
pub fn (a Atlas) group_get(name string) !&Group {
|
||||
name_lower := texttools.name_fix(name)
|
||||
pub fn (a DocTree) group_get(name string) !&Group {
|
||||
name_lower := doctree.name_fix(name)
|
||||
return a.groups[name_lower] or { return error('Group ${name} not found') }
|
||||
}
|
||||
|
||||
// Get all groups matching a session's email
|
||||
pub fn (a Atlas) groups_get(session Session) []&Group {
|
||||
pub fn (a DocTree) groups_get(session Session) []&Group {
|
||||
mut matching := []&Group{}
|
||||
|
||||
email_lower := session.email.to_lower()
|
||||
@@ -117,7 +102,7 @@ pub mut:
|
||||
ignore []string // list of directory names to ignore
|
||||
}
|
||||
|
||||
pub fn (mut a Atlas) scan(args ScanArgs) ! {
|
||||
pub fn (mut a DocTree) scan(args ScanArgs) ! {
|
||||
mut path := pathlib.get_dir(path: args.path)!
|
||||
mut ignore := args.ignore.clone()
|
||||
ignore = ignore.map(it.to_lower())
|
||||
@@ -125,7 +110,7 @@ pub fn (mut a Atlas) scan(args ScanArgs) ! {
|
||||
}
|
||||
|
||||
// Scan a directory for collections
|
||||
fn (mut a Atlas) scan_(mut dir pathlib.Path, ignore_ []string) ! {
|
||||
fn (mut a DocTree) scan_(mut dir pathlib.Path, ignore_ []string) ! {
|
||||
console.print_item('Scanning directory: ${dir.path}')
|
||||
if !dir.is_dir() {
|
||||
return error('Path is not a directory: ${dir.path}')
|
||||
@@ -1,4 +1,4 @@
|
||||
module atlas
|
||||
module core
|
||||
|
||||
pub struct CollectionNotFound {
|
||||
Error
|
||||
@@ -1,4 +1,4 @@
|
||||
module atlas
|
||||
module core
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.core.base
|
||||
@@ -7,22 +7,27 @@ import json
|
||||
@[params]
|
||||
pub struct ExportArgs {
|
||||
pub mut:
|
||||
destination string @[requireds]
|
||||
destination string @[required]
|
||||
reset bool = true
|
||||
include bool = true
|
||||
redis bool = true
|
||||
}
|
||||
|
||||
// Export all collections
|
||||
pub fn (mut a Atlas) export(args ExportArgs) ! {
|
||||
// Export all collections and do all processing steps
|
||||
pub fn (mut a DocTree) export(args ExportArgs) ! {
|
||||
mut dest := pathlib.get_dir(path: args.destination, create: true)!
|
||||
|
||||
if args.reset {
|
||||
dest.empty()!
|
||||
}
|
||||
|
||||
// Validate links before export to populate page.links
|
||||
a.validate_links()!
|
||||
// first make sure we have all links identified, in the pages itself
|
||||
// and make sure we know the git info
|
||||
for _, mut col in a.collections {
|
||||
col.find_links()!
|
||||
col.init_git_info()!
|
||||
col.title_descriptions()!
|
||||
}
|
||||
|
||||
for _, mut col in a.collections {
|
||||
col.export(
|
||||
@@ -32,6 +37,10 @@ pub fn (mut a Atlas) export(args ExportArgs) ! {
|
||||
redis: args.redis
|
||||
)!
|
||||
}
|
||||
|
||||
for _, mut col in a.collections {
|
||||
col.fix_links()!
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
@@ -90,13 +99,51 @@ pub fn (mut c Collection) export(args CollectionExportArgs) ! {
|
||||
c.collect_cross_collection_references(mut page, mut cross_collection_pages, mut
|
||||
cross_collection_files, mut processed_cross_pages)!
|
||||
|
||||
// println('------- ${c.name} ${page.key()}')
|
||||
// if page.key() == 'geoaware:solution' && c.name == 'mycelium_nodes_tiers' {
|
||||
// println(cross_collection_pages)
|
||||
// println(cross_collection_files)
|
||||
// // println(processed_cross_pages)
|
||||
// $dbg;
|
||||
// }
|
||||
|
||||
// copy the pages to the right exported path
|
||||
for _, mut ref_page in cross_collection_pages {
|
||||
mut src_file := ref_page.path()!
|
||||
mut subdir_path := pathlib.get_dir(
|
||||
path: '${col_dir.path}'
|
||||
create: true
|
||||
)!
|
||||
mut dest_path := '${subdir_path.path}/${ref_page.name}.md'
|
||||
src_file.copy(dest: dest_path)!
|
||||
// println(dest_path)
|
||||
// $dbg;
|
||||
}
|
||||
// copy the files to the right exported path
|
||||
for _, mut ref_file in cross_collection_files {
|
||||
mut src_file2 := ref_file.path()!
|
||||
|
||||
// Determine subdirectory based on file type
|
||||
mut subdir := if ref_file.is_image() { 'img' } else { 'files' }
|
||||
|
||||
// Ensure subdirectory exists
|
||||
mut subdir_path := pathlib.get_dir(
|
||||
path: '${col_dir.path}/${subdir}'
|
||||
create: true
|
||||
)!
|
||||
|
||||
mut dest_path := '${subdir_path.path}/${ref_file.name}'
|
||||
mut dest_file2 := pathlib.get_file(path: dest_path, create: true)!
|
||||
src_file2.copy(dest: dest_file2.path)!
|
||||
}
|
||||
|
||||
processed_local_pages[page.name] = true
|
||||
|
||||
// Redis operations...
|
||||
if args.redis {
|
||||
mut context := base.context()!
|
||||
mut redis := context.redis()!
|
||||
redis.hset('atlas:${c.name}', page.name, page.path)!
|
||||
redis.hset('doctree:${c.name}', page.name, page.path)!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,65 +164,6 @@ pub fn (mut c Collection) export(args CollectionExportArgs) ! {
|
||||
mut dest_file := pathlib.get_file(path: dest_path, create: true)!
|
||||
src_file.copy(dest: dest_file.path)!
|
||||
}
|
||||
|
||||
// Second pass: copy all collected cross-collection pages and process their links recursively
|
||||
// Keep iterating until no new cross-collection references are found
|
||||
for {
|
||||
mut found_new_references := false
|
||||
|
||||
// Process all cross-collection pages we haven't processed yet
|
||||
for page_key, mut ref_page in cross_collection_pages {
|
||||
if page_key in processed_cross_pages {
|
||||
continue // Already processed this page's links
|
||||
}
|
||||
|
||||
// Mark as processed to avoid infinite loops
|
||||
processed_cross_pages[page_key] = true
|
||||
found_new_references = true
|
||||
|
||||
// Get the referenced page content with includes processed
|
||||
ref_content := ref_page.content_with_fixed_links(
|
||||
include: args.include
|
||||
cross_collection: true
|
||||
export_mode: true
|
||||
)!
|
||||
|
||||
// Write the referenced page to this collection's directory
|
||||
mut dest_file := pathlib.get_file(
|
||||
path: '${col_dir.path}/${ref_page.name}.md'
|
||||
create: true
|
||||
)!
|
||||
dest_file.write(ref_content)!
|
||||
|
||||
// CRITICAL: Recursively process links in this cross-collection page
|
||||
// This ensures we get pages/files/images referenced by ref_page
|
||||
c.collect_cross_collection_references(mut ref_page, mut cross_collection_pages, mut
|
||||
cross_collection_files, mut processed_cross_pages)!
|
||||
}
|
||||
|
||||
// If we didn't find any new references, we're done with the recursive pass
|
||||
if !found_new_references {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Third pass: copy ALL collected cross-collection referenced files/images
|
||||
for _, mut ref_file in cross_collection_files {
|
||||
mut src_file := ref_file.path()!
|
||||
|
||||
// Determine subdirectory based on file type
|
||||
mut subdir := if ref_file.is_image() { 'img' } else { 'files' }
|
||||
|
||||
// Ensure subdirectory exists
|
||||
mut subdir_path := pathlib.get_dir(
|
||||
path: '${col_dir.path}/${subdir}'
|
||||
create: true
|
||||
)!
|
||||
|
||||
mut dest_path := '${subdir_path.path}/${ref_file.name}'
|
||||
mut dest_file := pathlib.get_file(path: dest_path, create: true)!
|
||||
src_file.copy(dest: dest_file.path)!
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to recursively collect cross-collection references
|
||||
@@ -184,6 +172,17 @@ fn (mut c Collection) collect_cross_collection_references(mut page Page,
|
||||
mut all_cross_pages map[string]&Page,
|
||||
mut all_cross_files map[string]&File,
|
||||
mut processed_pages map[string]bool) ! {
|
||||
page_key := page.key()
|
||||
|
||||
// If we've already processed this page, skip it (prevents infinite loops with cycles)
|
||||
if page_key in processed_pages {
|
||||
return
|
||||
}
|
||||
|
||||
// Mark this page as processed BEFORE recursing (prevents infinite loops with circular references)
|
||||
processed_pages[page_key] = true
|
||||
|
||||
// Process all links in the current page
|
||||
// Use cached links from validation (before transformation) to preserve collection info
|
||||
for mut link in page.links {
|
||||
if link.status != .found {
|
||||
@@ -192,15 +191,19 @@ fn (mut c Collection) collect_cross_collection_references(mut page Page,
|
||||
|
||||
is_local := link.target_collection_name == c.name
|
||||
|
||||
// Collect cross-collection page references
|
||||
// Collect cross-collection page references and recursively process them
|
||||
if link.file_type == .page && !is_local {
|
||||
page_key := '${link.target_collection_name}:${link.target_item_name}'
|
||||
page_ref := '${link.target_collection_name}:${link.target_item_name}'
|
||||
|
||||
// Only add if not already collected
|
||||
if page_key !in all_cross_pages {
|
||||
if page_ref !in all_cross_pages {
|
||||
mut target_page := link.target_page()!
|
||||
all_cross_pages[page_key] = target_page
|
||||
// Don't mark as processed yet - we'll do that when we actually process its links
|
||||
all_cross_pages[page_ref] = target_page
|
||||
|
||||
// Recursively process the target page's links to find more cross-collection references
|
||||
// This ensures we collect ALL transitive cross-collection page and file references
|
||||
c.collect_cross_collection_references(mut target_page, mut all_cross_pages, mut
|
||||
all_cross_files, mut processed_pages)!
|
||||
}
|
||||
}
|
||||
|
||||
61
lib/web/doctree/core/factory.v
Normal file
61
lib/web/doctree/core/factory.v
Normal file
@@ -0,0 +1,61 @@
|
||||
module core
|
||||
|
||||
import incubaid.herolib.web.doctree as doctreetools
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.ui.console
|
||||
import incubaid.herolib.data.paramsparser
|
||||
|
||||
__global (
|
||||
doctrees shared map[string]&DocTree
|
||||
)
|
||||
|
||||
@[params]
|
||||
pub struct DocTreeNewArgs {
|
||||
pub mut:
|
||||
name string = 'default'
|
||||
}
|
||||
|
||||
// Create a new DocTree
|
||||
pub fn new(args DocTreeNewArgs) !&DocTree {
|
||||
mut name := doctreetools.name_fix(args.name)
|
||||
|
||||
mut a := &DocTree{
|
||||
name: name
|
||||
}
|
||||
|
||||
set(a)
|
||||
return a
|
||||
}
|
||||
|
||||
// Get DocTree from global map
|
||||
pub fn get(name string) !&DocTree {
|
||||
mut fixed_name := doctreetools.name_fix(name)
|
||||
rlock doctrees {
|
||||
if fixed_name in doctrees {
|
||||
return doctrees[fixed_name] or { return error('DocTree ${name} not found') }
|
||||
}
|
||||
}
|
||||
return error("DocTree '${name}' not found")
|
||||
}
|
||||
|
||||
// Check if DocTree exists
|
||||
pub fn exists(name string) bool {
|
||||
mut fixed_name := doctreetools.name_fix(name)
|
||||
rlock doctrees {
|
||||
return fixed_name in doctrees
|
||||
}
|
||||
}
|
||||
|
||||
// List all DocTree names
|
||||
pub fn list() []string {
|
||||
rlock doctrees {
|
||||
return doctrees.keys()
|
||||
}
|
||||
}
|
||||
|
||||
// Store DocTree in global map
|
||||
fn set(doctree &DocTree) {
|
||||
lock doctrees {
|
||||
doctrees[doctree.name] = doctree
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
module atlas
|
||||
module core
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
import os
|
||||
86
lib/web/doctree/core/getters.v
Normal file
86
lib/web/doctree/core/getters.v
Normal file
@@ -0,0 +1,86 @@
|
||||
module core
|
||||
|
||||
import incubaid.herolib.web.doctree
|
||||
|
||||
// Get a page from any collection using format "collection:page"
|
||||
pub fn (a DocTree) page_get(key string) !&Page {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error('Invalid page key format. Use "collection:page" in page_get')
|
||||
}
|
||||
|
||||
col := a.get_collection(parts[0])!
|
||||
return col.page_get(parts[1])!
|
||||
}
|
||||
|
||||
// Get an image from any collection using format "collection:image"
|
||||
pub fn (a DocTree) image_get(key string) !&File {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error('Invalid image key format. Use "collection:image" in image_get')
|
||||
}
|
||||
|
||||
col := a.get_collection(parts[0])!
|
||||
return col.image_get(parts[1])!
|
||||
}
|
||||
|
||||
// Get a file from any collection using format "collection:file"
|
||||
pub fn (a DocTree) file_get(key string) !&File {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error('Invalid file key format. Use "collection:file" in file_get')
|
||||
}
|
||||
|
||||
col := a.get_collection(parts[0])!
|
||||
return col.file_get(parts[1])!
|
||||
}
|
||||
|
||||
// Get a file (can be image) from any collection using format "collection:file"
|
||||
pub fn (a DocTree) file_or_image_get(key string) !&File {
|
||||
c, n := doctree.key_parse(key)!
|
||||
col := a.get_collection(c)!
|
||||
return col.file_or_image_get(n)!
|
||||
}
|
||||
|
||||
// Check if page exists
|
||||
pub fn (a DocTree) page_exists(key string) !bool {
|
||||
c, n := doctree.key_parse(key)!
|
||||
col := a.get_collection(c) or { return false }
|
||||
return col.page_exists(n)
|
||||
}
|
||||
|
||||
// Check if image exists
|
||||
pub fn (a DocTree) image_exists(key string) !bool {
|
||||
c, n := doctree.key_parse(key)!
|
||||
col := a.get_collection(c) or { return false }
|
||||
return col.image_exists(n)
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
pub fn (a DocTree) file_exists(key string) !bool {
|
||||
c, n := doctree.key_parse(key)!
|
||||
col := a.get_collection(c) or { return false }
|
||||
return col.file_exists(n)
|
||||
}
|
||||
|
||||
pub fn (a DocTree) file_or_image_exists(key string) !bool {
|
||||
c, n := doctree.key_parse(key)!
|
||||
col := a.get_collection(c) or { return false }
|
||||
return col.file_or_image_exists(n)
|
||||
}
|
||||
|
||||
// List all pages in DocTree
|
||||
pub fn (a DocTree) list_pages() map[string][]string {
|
||||
mut result := map[string][]string{}
|
||||
|
||||
for col_name, col in a.collections {
|
||||
mut page_names := []string{}
|
||||
for page_name, _ in col.pages {
|
||||
page_names << page_name
|
||||
}
|
||||
page_names.sort()
|
||||
result[col_name] = page_names
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
module atlas
|
||||
module core
|
||||
|
||||
import incubaid.herolib.core.texttools
|
||||
import incubaid.herolib.web.doctree
|
||||
import incubaid.herolib.core.pathlib
|
||||
import os
|
||||
|
||||
@@ -20,7 +20,7 @@ pub mut:
|
||||
|
||||
// Create a new Group
|
||||
pub fn new_group(args GroupNewArgs) !Group {
|
||||
mut name := texttools.name_fix(args.name)
|
||||
mut name := doctree.name_fix(args.name)
|
||||
mut patterns := args.patterns.map(it.to_lower())
|
||||
|
||||
return Group{
|
||||
@@ -72,7 +72,7 @@ fn parse_group_file(filename string, base_path string, mut visited map[string]bo
|
||||
visited[filename] = true
|
||||
|
||||
mut group := Group{
|
||||
name: texttools.name_fix(filename)
|
||||
name: doctree.name_fix(filename)
|
||||
patterns: []string{}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
in atlas/
|
||||
in doctree/
|
||||
|
||||
check format of groups
|
||||
see content/groups
|
||||
@@ -7,9 +7,9 @@ now the groups end with .group
|
||||
|
||||
check how the include works, so we can include another group in the group as defined, only works in same folder
|
||||
|
||||
in the scan function in atlas, now make scan_groups function, find groups, only do this for collection as named groups
|
||||
do not add collection groups to atlas, this is a system collection
|
||||
in the scan function in doctree, now make scan_groups function, find groups, only do this for collection as named groups
|
||||
do not add collection groups to doctree, this is a system collection
|
||||
|
||||
make the groups and add them to atlas
|
||||
make the groups and add them to doctree
|
||||
|
||||
give clear instructions for coding agent how to write the code
|
||||
@@ -1,7 +1,6 @@
|
||||
module atlas
|
||||
module core
|
||||
|
||||
import incubaid.herolib.core.texttools
|
||||
import incubaid.herolib.ui.console
|
||||
import incubaid.herolib.web.doctree as doctreetools
|
||||
|
||||
pub enum LinkFileType {
|
||||
page // Default: link to another page
|
||||
@@ -43,7 +42,7 @@ pub fn (mut self Link) target_page() !&Page {
|
||||
if self.status == .external {
|
||||
return error('External links do not have a target page')
|
||||
}
|
||||
return self.page.collection.atlas.page_get(self.key())
|
||||
return self.page.collection.doctree.page_get(self.key())
|
||||
}
|
||||
|
||||
// Get the target file this link points to
|
||||
@@ -51,7 +50,7 @@ pub fn (mut self Link) target_file() !&File {
|
||||
if self.status == .external {
|
||||
return error('External links do not have a target file')
|
||||
}
|
||||
return self.page.collection.atlas.file_or_image_get(self.key())
|
||||
return self.page.collection.doctree.file_or_image_get(self.key())
|
||||
}
|
||||
|
||||
// Find all markdown links in content
|
||||
@@ -161,23 +160,10 @@ fn (mut p Page) parse_link_target(mut link Link) ! {
|
||||
|
||||
// Format: $collection:$pagename or $collection:$pagename.md
|
||||
if target.contains(':') {
|
||||
parts := target.split(':')
|
||||
if parts.len >= 2 {
|
||||
link.target_collection_name = texttools.name_fix(parts[0])
|
||||
// For file links, use name without extension; for page links, normalize normally
|
||||
if link.file_type == .file {
|
||||
link.target_item_name = texttools.name_fix_no_ext(parts[1])
|
||||
} else {
|
||||
link.target_item_name = normalize_page_name(parts[1])
|
||||
}
|
||||
}
|
||||
link.target_collection_name, link.target_item_name = doctreetools.key_parse(target)!
|
||||
} else {
|
||||
// For file links, use name without extension; for page links, normalize normally
|
||||
if link.file_type == .file {
|
||||
link.target_item_name = texttools.name_fix_no_ext(target).trim_space()
|
||||
} else {
|
||||
link.target_item_name = normalize_page_name(target).trim_space()
|
||||
}
|
||||
link.target_item_name = doctreetools.name_fix(target)
|
||||
link.target_collection_name = p.collection.name
|
||||
}
|
||||
|
||||
@@ -189,11 +175,11 @@ fn (mut p Page) parse_link_target(mut link Link) ! {
|
||||
mut error_prefix := 'Broken link'
|
||||
|
||||
if link.file_type == .file || link.file_type == .image {
|
||||
target_exists = p.collection.atlas.file_or_image_exists(link.key())!
|
||||
target_exists = p.collection.doctree.file_or_image_exists(link.key())!
|
||||
error_category = .invalid_file_reference
|
||||
error_prefix = if link.file_type == .file { 'Broken file link' } else { 'Broken image link' }
|
||||
} else {
|
||||
target_exists = p.collection.atlas.page_exists(link.key())!
|
||||
target_exists = p.collection.doctree.page_exists(link.key())!
|
||||
}
|
||||
|
||||
// console.print_debug('Link target exists: ${target_exists} for key=${link.key()}')
|
||||
@@ -298,14 +284,3 @@ fn (mut p Page) filesystem_link_path(mut link Link) !string {
|
||||
|
||||
return target_path.path_relative(source_path.path)!
|
||||
}
|
||||
|
||||
/////////////TOOLS//////////////////////////////////
|
||||
|
||||
// Normalize page name (remove .md, apply name_fix)
|
||||
fn normalize_page_name(name string) string {
|
||||
mut clean := name
|
||||
if clean.ends_with('.md') {
|
||||
clean = clean[0..clean.len - 3]
|
||||
}
|
||||
return texttools.name_fix(clean)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
module atlas
|
||||
module core
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.core.texttools
|
||||
import incubaid.herolib.web.doctree as doctreetools
|
||||
|
||||
@[heap]
|
||||
pub struct Page {
|
||||
@@ -11,9 +11,18 @@ pub mut:
|
||||
collection_name string
|
||||
links []Link
|
||||
// macros []Macro
|
||||
title string
|
||||
description string
|
||||
questions []Question
|
||||
collection &Collection @[skip; str: skip] // Reference to parent collection
|
||||
}
|
||||
|
||||
pub struct Question {
|
||||
pub mut:
|
||||
question string
|
||||
answer string
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct NewPageArgs {
|
||||
pub:
|
||||
@@ -36,7 +45,7 @@ pub mut:
|
||||
include bool
|
||||
}
|
||||
|
||||
// Read content without processing includes
|
||||
// Read content can be with or without processing includes
|
||||
pub fn (mut p Page) content(args ReadContentArgs) !string {
|
||||
mut mypath := p.path()!
|
||||
mut content := mypath.read()!
|
||||
@@ -49,7 +58,7 @@ pub fn (mut p Page) content(args ReadContentArgs) !string {
|
||||
|
||||
// Recursively process includes
|
||||
fn (mut p Page) process_includes(content string, mut visited map[string]bool) !string {
|
||||
mut atlas := p.collection.atlas
|
||||
mut doctree := p.collection.doctree
|
||||
// Prevent circular includes
|
||||
page_key := p.key()
|
||||
if page_key in visited {
|
||||
@@ -80,34 +89,16 @@ fn (mut p Page) process_includes(content string, mut visited map[string]bool) !s
|
||||
mut target_page := ''
|
||||
|
||||
if include_ref.contains(':') {
|
||||
parts := include_ref.split(':')
|
||||
if parts.len == 2 {
|
||||
target_collection = texttools.name_fix(parts[0])
|
||||
target_page = texttools.name_fix(parts[1])
|
||||
} else {
|
||||
p.collection.error(
|
||||
category: .include_syntax_error
|
||||
page_key: page_key
|
||||
message: 'Invalid include format: `${include_ref}`'
|
||||
show_console: false
|
||||
)
|
||||
processed_lines << '<!-- Invalid include format: ${include_ref} -->'
|
||||
continue
|
||||
}
|
||||
target_collection, target_page = doctreetools.key_parse(include_ref)!
|
||||
} else {
|
||||
target_page = texttools.name_fix(include_ref)
|
||||
}
|
||||
|
||||
// Remove .md extension if present
|
||||
if target_page.ends_with('.md') {
|
||||
target_page = target_page[0..target_page.len - 3]
|
||||
target_page = doctreetools.name_fix(include_ref)
|
||||
}
|
||||
|
||||
// Build page key
|
||||
page_ref := '${target_collection}:${target_page}'
|
||||
|
||||
// Get the referenced page from atlas
|
||||
mut include_page := atlas.page_get(page_ref) or {
|
||||
// Get the referenced page from doctree
|
||||
mut include_page := doctree.page_get(page_ref) or {
|
||||
p.collection.error(
|
||||
category: .missing_include
|
||||
page_key: page_key
|
||||
@@ -1,36 +1,36 @@
|
||||
module atlas
|
||||
module core
|
||||
|
||||
import incubaid.herolib.core.playbook { PlayBook }
|
||||
import incubaid.herolib.develop.gittools
|
||||
import incubaid.herolib.ui.console
|
||||
import os
|
||||
|
||||
// Play function to process HeroScript actions for Atlas
|
||||
// Play function to process HeroScript actions for DocTree
|
||||
pub fn play(mut plbook PlayBook) ! {
|
||||
if !plbook.exists(filter: 'atlas.') {
|
||||
if !plbook.exists(filter: 'doctree.') {
|
||||
return
|
||||
}
|
||||
|
||||
// Track which atlases we've processed in this playbook
|
||||
mut processed_atlases := map[string]bool{}
|
||||
// Track which doctrees we've processed in this playbook
|
||||
mut processed_doctreees := map[string]bool{}
|
||||
|
||||
mut name := ''
|
||||
|
||||
// Process scan actions - scan directories for collections
|
||||
mut scan_actions := plbook.find(filter: 'atlas.scan')!
|
||||
mut scan_actions := plbook.find(filter: 'doctree.scan')!
|
||||
for mut action in scan_actions {
|
||||
mut p := action.params
|
||||
name = p.get_default('name', 'main')!
|
||||
ignore := p.get_list_default('ignore', [])!
|
||||
console.print_item("Scanning Atlas '${name}' with ignore patterns: ${ignore}")
|
||||
// Get or create atlas from global map
|
||||
mut atlas_instance := if exists(name) {
|
||||
console.print_item("Scanning DocTree '${name}' with ignore patterns: ${ignore}")
|
||||
// Get or create doctree from global map
|
||||
mut doctree_instance := if exists(name) {
|
||||
get(name)!
|
||||
} else {
|
||||
console.print_debug('Atlas not found, creating a new one')
|
||||
console.print_debug('DocTree not found, creating a new one')
|
||||
new(name: name)!
|
||||
}
|
||||
processed_atlases[name] = true
|
||||
processed_doctreees[name] = true
|
||||
|
||||
mut path := p.get_default('path', '')!
|
||||
|
||||
@@ -45,38 +45,38 @@ pub fn play(mut plbook PlayBook) ! {
|
||||
)!.path
|
||||
}
|
||||
if path == '' {
|
||||
return error('Either "path" or "git_url" must be provided for atlas.scan action.')
|
||||
return error('Either "path" or "git_url" must be provided for doctree.scan action.')
|
||||
}
|
||||
atlas_instance.scan(path: path, ignore: ignore)!
|
||||
doctree_instance.scan(path: path, ignore: ignore)!
|
||||
action.done = true
|
||||
|
||||
// No need to call set() again - atlas is already in global map from new()
|
||||
// No need to call set() again - doctree is already in global map from new()
|
||||
// and we're modifying it by reference
|
||||
}
|
||||
|
||||
// Run init_post on all processed atlases
|
||||
for atlas_name, _ in processed_atlases {
|
||||
mut atlas_instance_post := get(atlas_name)!
|
||||
atlas_instance_post.init_post()!
|
||||
// Run init_post on all processed doctrees
|
||||
for doctree_name, _ in processed_doctreees {
|
||||
mut doctree_instance_post := get(doctree_name)!
|
||||
doctree_instance_post.init_post()!
|
||||
}
|
||||
|
||||
// Process export actions - export collections to destination
|
||||
mut export_actions := plbook.find(filter: 'atlas.export')!
|
||||
mut export_actions := plbook.find(filter: 'doctree.export')!
|
||||
|
||||
// Process explicit export actions
|
||||
for mut action in export_actions {
|
||||
mut p := action.params
|
||||
name = p.get_default('name', 'main')!
|
||||
destination := p.get_default('destination', '${os.home_dir()}/hero/var/atlas_export')!
|
||||
destination := p.get_default('destination', '${os.home_dir()}/hero/var/doctree_export')!
|
||||
reset := p.get_default_true('reset')
|
||||
include := p.get_default_true('include')
|
||||
redis := p.get_default_true('redis')
|
||||
|
||||
mut atlas_instance := get(name) or {
|
||||
return error("Atlas '${name}' not found. Use !!atlas.scan first.")
|
||||
mut doctree_instance := get(name) or {
|
||||
return error("DocTree '${name}' not found. Use !!doctree.scan first.")
|
||||
}
|
||||
|
||||
atlas_instance.export(
|
||||
doctree_instance.export(
|
||||
destination: destination
|
||||
reset: reset
|
||||
include: include
|
||||
@@ -1,26 +1,34 @@
|
||||
module atlas
|
||||
module core
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
import os
|
||||
import json
|
||||
|
||||
const test_base = '/tmp/atlas_test'
|
||||
const test_base = '/tmp/doctree_test'
|
||||
|
||||
fn testsuite_begin() {
|
||||
// Clean up before and after each test
|
||||
fn setup_test() {
|
||||
os.rmdir_all(test_base) or {}
|
||||
os.mkdir_all(test_base)!
|
||||
os.mkdir_all(test_base) or {}
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
fn cleanup_test() {
|
||||
os.rmdir_all(test_base) or {}
|
||||
}
|
||||
|
||||
fn test_create_atlas() {
|
||||
mut a := new(name: 'test_atlas')!
|
||||
assert a.name == 'test_atlas'
|
||||
fn test_create_doctree() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
mut a := new(name: 'test_doctree')!
|
||||
assert a.name == 'test_doctree'
|
||||
assert a.collections.len == 0
|
||||
}
|
||||
|
||||
fn test_add_collection() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Create test collection
|
||||
col_path := '${test_base}/col1'
|
||||
os.mkdir_all(col_path)!
|
||||
@@ -38,6 +46,9 @@ fn test_add_collection() {
|
||||
}
|
||||
|
||||
fn test_scan() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Create test structure
|
||||
os.mkdir_all('${test_base}/docs/guides')!
|
||||
mut cfile := pathlib.get_file(path: '${test_base}/docs/guides/.collection', create: true)!
|
||||
@@ -55,6 +66,9 @@ fn test_scan() {
|
||||
}
|
||||
|
||||
fn test_export() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Setup
|
||||
col_path := '${test_base}/source/col1'
|
||||
export_path := '${test_base}/export'
|
||||
@@ -76,6 +90,9 @@ fn test_export() {
|
||||
}
|
||||
|
||||
fn test_export_with_includes() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Setup: Create pages with includes
|
||||
col_path := '${test_base}/include_test'
|
||||
os.mkdir_all(col_path)!
|
||||
@@ -95,7 +112,7 @@ fn test_export_with_includes() {
|
||||
a.add_collection(mut pathlib.get_dir(path: col_path)!)!
|
||||
|
||||
export_path := '${test_base}/export_include'
|
||||
a.export(destination: export_path, include: true)!
|
||||
a.export(destination: export_path, include: true, redis: false)!
|
||||
|
||||
// Verify exported page1 has page2 content included
|
||||
exported := os.read_file('${export_path}/content/test_col/page1.md')!
|
||||
@@ -105,6 +122,9 @@ fn test_export_with_includes() {
|
||||
}
|
||||
|
||||
fn test_export_without_includes() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
col_path := '${test_base}/no_include_test'
|
||||
os.mkdir_all(col_path)!
|
||||
|
||||
@@ -118,7 +138,7 @@ fn test_export_without_includes() {
|
||||
a.add_collection(mut pathlib.get_dir(path: col_path)!)!
|
||||
|
||||
export_path := '${test_base}/export_no_include'
|
||||
a.export(destination: export_path, include: false)!
|
||||
a.export(destination: export_path, include: false, redis: false)!
|
||||
|
||||
// Verify exported page1 still has include action
|
||||
exported := os.read_file('${export_path}/content/test_col2/page1.md')!
|
||||
@@ -126,18 +146,28 @@ fn test_export_without_includes() {
|
||||
}
|
||||
|
||||
fn test_error_deduplication() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
mut a := new(name: 'test')!
|
||||
col_path := '${test_base}/err_dedup_col'
|
||||
os.mkdir_all(col_path)!
|
||||
mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)!
|
||||
cfile.write('name:err_dedup_col')!
|
||||
mut col := a.add_collection(mut pathlib.get_dir(path: col_path)!)!
|
||||
assert col.name == 'err_dedup_col' // Ensure collection is added correctly
|
||||
}
|
||||
|
||||
fn test_error_hash() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
// This test had no content, leaving it as a placeholder.
|
||||
}
|
||||
|
||||
fn test_find_links() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
col_path := '${test_base}/find_links_test'
|
||||
os.mkdir_all(col_path)!
|
||||
|
||||
@@ -157,7 +187,11 @@ fn test_find_links() {
|
||||
assert links.len >= 2
|
||||
}
|
||||
|
||||
fn test_validate_links() {
|
||||
// Test with a valid link to ensure no errors are reported
|
||||
fn test_find_links_valid_link() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Setup
|
||||
col_path := '${test_base}/link_test'
|
||||
os.mkdir_all(col_path)!
|
||||
@@ -176,15 +210,17 @@ fn test_validate_links() {
|
||||
mut a := new()!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_path)!)!
|
||||
|
||||
// Validate
|
||||
a.validate_links()!
|
||||
|
||||
// Should have no errors
|
||||
col := a.get_collection('test_col')!
|
||||
assert col.errors.len == 0
|
||||
|
||||
a.export(destination: '${test_base}/export_links', redis: false)!
|
||||
}
|
||||
|
||||
fn test_validate_broken_links() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Setup
|
||||
col_path := '${test_base}/broken_link_test'
|
||||
os.mkdir_all(col_path)!
|
||||
@@ -200,13 +236,17 @@ fn test_validate_broken_links() {
|
||||
a.add_collection(mut pathlib.get_dir(path: col_path)!)!
|
||||
|
||||
// Validate
|
||||
a.validate_links()!
|
||||
a.export(destination: '${test_base}/validate_broken_links', redis: false)!
|
||||
|
||||
// Should have error
|
||||
col := a.get_collection('test_col')!
|
||||
assert col.errors.len > 0
|
||||
}
|
||||
|
||||
fn test_fix_links() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Setup - all pages in same directory for simpler test
|
||||
col_path := '${test_base}/fix_link_test'
|
||||
os.mkdir_all(col_path)!
|
||||
@@ -229,20 +269,22 @@ fn test_fix_links() {
|
||||
mut p := col.page_get('page1')!
|
||||
|
||||
original := p.content()!
|
||||
println('Original: ${original}')
|
||||
assert original.contains('[Link](page2)')
|
||||
|
||||
fixed := p.content_with_fixed_links(FixLinksArgs{
|
||||
include: true
|
||||
cross_collection: true
|
||||
export_mode: false
|
||||
})!
|
||||
println('Fixed: ${fixed}')
|
||||
|
||||
// The fix_links should work on content
|
||||
assert fixed.contains('[Link](page2.md)')
|
||||
}
|
||||
|
||||
fn test_link_formats() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
col_path := '${test_base}/link_format_test'
|
||||
os.mkdir_all(col_path)!
|
||||
|
||||
@@ -268,6 +310,9 @@ fn test_link_formats() {
|
||||
}
|
||||
|
||||
fn test_cross_collection_links() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Setup two collections
|
||||
col1_path := '${test_base}/col1_cross'
|
||||
col2_path := '${test_base}/col2_cross'
|
||||
@@ -293,20 +338,19 @@ fn test_cross_collection_links() {
|
||||
a.add_collection(mut pathlib.get_dir(path: col1_path)!)!
|
||||
a.add_collection(mut pathlib.get_dir(path: col2_path)!)!
|
||||
|
||||
// Validate - should pass
|
||||
a.validate_links()!
|
||||
|
||||
col1 := a.get_collection('col1')!
|
||||
assert col1.errors.len == 0
|
||||
|
||||
// Fix links - cross-collection links should NOT be rewritten
|
||||
a.fix_links()!
|
||||
a.export(destination: '${test_base}/export_cross', redis: false)!
|
||||
|
||||
fixed := page1.read()!
|
||||
assert fixed.contains('[Link to col2](col2:page2)') // Unchanged
|
||||
}
|
||||
|
||||
fn test_save_and_load() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Setup
|
||||
col_path := '${test_base}/save_test'
|
||||
os.mkdir_all(col_path)!
|
||||
@@ -321,9 +365,13 @@ fn test_save_and_load() {
|
||||
mut a := new(name: 'test')!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_path)!)!
|
||||
col := a.get_collection('test_col')!
|
||||
assert col.name == 'test_col'
|
||||
}
|
||||
|
||||
fn test_save_with_errors() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
col_path := '${test_base}/error_save_test'
|
||||
os.mkdir_all(col_path)!
|
||||
|
||||
@@ -332,9 +380,13 @@ fn test_save_with_errors() {
|
||||
|
||||
mut a := new(name: 'test')!
|
||||
mut col := a.add_collection(mut pathlib.get_dir(path: col_path)!)!
|
||||
assert col.name == 'err_col' // Ensure collection is added correctly
|
||||
}
|
||||
|
||||
fn test_load_from_directory() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Setup multiple collections
|
||||
col1_path := '${test_base}/load_dir/col1'
|
||||
col2_path := '${test_base}/load_dir/col2'
|
||||
@@ -358,16 +410,21 @@ fn test_load_from_directory() {
|
||||
mut a := new(name: 'test')!
|
||||
a.add_collection(mut pathlib.get_dir(path: col1_path)!)!
|
||||
a.add_collection(mut pathlib.get_dir(path: col2_path)!)!
|
||||
|
||||
assert a.collections.len == 2
|
||||
}
|
||||
|
||||
fn test_get_edit_url() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Create a mock collection
|
||||
mut atlas := new(name: 'test_atlas')!
|
||||
mut doctree := new(name: 'test_doctree')!
|
||||
col_path := '${test_base}/git_test'
|
||||
os.mkdir_all(col_path)!
|
||||
mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)!
|
||||
cfile.write('name:git_test_col')!
|
||||
mut col := atlas.add_collection(mut pathlib.get_dir(path: col_path)!)!
|
||||
mut col := doctree.add_collection(mut pathlib.get_dir(path: col_path)!)!
|
||||
col.git_url = 'https://github.com/test/repo.git' // Assuming git_url is a field on Collection
|
||||
// Create a mock page
|
||||
mut page_path := pathlib.get_file(path: '${col_path}/test_page.md', create: true)!
|
||||
@@ -376,13 +433,14 @@ fn test_get_edit_url() {
|
||||
|
||||
// Get the page and collection edit URLs
|
||||
page := col.page_get('test_page')!
|
||||
// edit_url := page.get_edit_url()! // This method does not exist
|
||||
|
||||
// Assert the URLs are correct
|
||||
// assert edit_url == 'https://github.com/test/repo/edit/main/test_page.md'
|
||||
// No asserts in original, adding one for completeness
|
||||
assert page.name == 'test_page'
|
||||
}
|
||||
|
||||
fn test_export_recursive_links() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
// Create 3 collections with chained links
|
||||
col_a_path := '${test_base}/recursive_export/col_a'
|
||||
col_b_path := '${test_base}/recursive_export/col_b'
|
||||
@@ -392,37 +450,95 @@ fn test_export_recursive_links() {
|
||||
os.mkdir_all(col_b_path)!
|
||||
os.mkdir_all(col_c_path)!
|
||||
|
||||
// Collection A
|
||||
// Collection A: links to B
|
||||
mut cfile_a := pathlib.get_file(path: '${col_a_path}/.collection', create: true)!
|
||||
cfile_a.write('name:col_a')!
|
||||
mut page_a := pathlib.get_file(path: '${col_a_path}/page_a.md', create: true)!
|
||||
page_a.write('# Page A\n\n[Link to B](col_b:page_b)')!
|
||||
page_a.write('# Page A\n\nThis is page A.\n\n[Link to Page B](col_b:page_b)')!
|
||||
|
||||
// Collection B
|
||||
// Collection B: links to C
|
||||
mut cfile_b := pathlib.get_file(path: '${col_b_path}/.collection', create: true)!
|
||||
cfile_b.write('name:col_b')!
|
||||
mut page_b := pathlib.get_file(path: '${col_b_path}/page_b.md', create: true)!
|
||||
page_b.write('# Page B\n\n[Link to C](col_c:page_c)')!
|
||||
page_b.write('# Page B\n\nThis is page B with link to C.\n\n[Link to Page C](col_c:page_c)')!
|
||||
|
||||
// Collection C
|
||||
// Collection C: final page
|
||||
mut cfile_c := pathlib.get_file(path: '${col_c_path}/.collection', create: true)!
|
||||
cfile_c.write('name:col_c')!
|
||||
mut page_c := pathlib.get_file(path: '${col_c_path}/page_c.md', create: true)!
|
||||
page_c.write('# Page C\n\nFinal content')!
|
||||
page_c.write('# Page C\n\nThis is the final page in the chain.')!
|
||||
|
||||
// Export
|
||||
// Create DocTree and add all collections
|
||||
mut a := new()!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_a_path)!)!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_b_path)!)!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_c_path)!)!
|
||||
|
||||
// Export
|
||||
export_path := '${test_base}/export_recursive'
|
||||
a.export(destination: export_path)!
|
||||
a.export(destination: export_path, redis: false)!
|
||||
|
||||
// Verify all pages were exported
|
||||
assert os.exists('${export_path}/content/col_a/page_a.md')
|
||||
assert os.exists('${export_path}/content/col_a/page_b.md') // From Collection B
|
||||
assert os.exists('${export_path}/content/col_a/page_c.md') // From Collection C
|
||||
// Verify directory structure exists
|
||||
assert os.exists('${export_path}/content'), 'Export content directory should exist'
|
||||
assert os.exists('${export_path}/content/col_a'), 'Collection col_a directory should exist'
|
||||
assert os.exists('${export_path}/meta'), 'Export meta directory should exist'
|
||||
|
||||
// TODO: test not complete
|
||||
// Verify all pages exist in col_a export directory
|
||||
assert os.exists('${export_path}/content/col_a/page_a.md'), 'page_a.md should be exported'
|
||||
assert os.exists('${export_path}/content/col_a/page_b.md'), 'page_b.md from col_b should be included'
|
||||
assert os.exists('${export_path}/content/col_a/page_c.md'), 'page_c.md from col_c should be included'
|
||||
|
||||
// Verify metadata files exist
|
||||
assert os.exists('${export_path}/meta/col_a.json'), 'col_a metadata should exist'
|
||||
assert os.exists('${export_path}/meta/col_b.json'), 'col_b metadata should exist'
|
||||
assert os.exists('${export_path}/meta/col_c.json'), 'col_c metadata should exist'
|
||||
}
|
||||
|
||||
fn test_export_recursive_with_images() {
|
||||
setup_test()
|
||||
defer { cleanup_test() }
|
||||
|
||||
col_a_path := '${test_base}/recursive_img/col_a'
|
||||
col_b_path := '${test_base}/recursive_img/col_b'
|
||||
|
||||
os.mkdir_all(col_a_path)!
|
||||
os.mkdir_all(col_b_path)!
|
||||
os.mkdir_all('${col_a_path}/img')!
|
||||
os.mkdir_all('${col_b_path}/img')!
|
||||
|
||||
// Collection A with local image
|
||||
mut cfile_a := pathlib.get_file(path: '${col_a_path}/.collection', create: true)!
|
||||
cfile_a.write('name:col_a')!
|
||||
|
||||
mut page_a := pathlib.get_file(path: '${col_a_path}/page_a.md', create: true)!
|
||||
page_a.write('# Page A\n\n\n\n[Link to B](col_b:page_b)')!
|
||||
|
||||
// Create local image
|
||||
os.write_file('${col_a_path}/img/local.png', 'fake png data')!
|
||||
|
||||
// Collection B with image and linked page
|
||||
mut cfile_b := pathlib.get_file(path: '${col_b_path}/.collection', create: true)!
|
||||
cfile_b.write('name:col_b')!
|
||||
|
||||
mut page_b := pathlib.get_file(path: '${col_b_path}/page_b.md', create: true)!
|
||||
page_b.write('# Page B\n\n')!
|
||||
|
||||
// Create image in collection B
|
||||
os.write_file('${col_b_path}/img/b_image.jpg', 'fake jpg data')!
|
||||
|
||||
// Create DocTree
|
||||
mut a := new()!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_a_path)!)!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_b_path)!)!
|
||||
|
||||
export_path := '${test_base}/export_recursive_img'
|
||||
a.export(destination: export_path, redis: false)!
|
||||
|
||||
// Verify pages exported
|
||||
assert os.exists('${export_path}/content/col_a/page_a.md'), 'page_a should exist'
|
||||
assert os.exists('${export_path}/content/col_a/page_b.md'), 'page_b from col_b should be included'
|
||||
|
||||
// Verify images exported to col_a image directory
|
||||
assert os.exists('${export_path}/content/col_a/img/local.png'), 'Local image should exist'
|
||||
assert os.exists('${export_path}/content/col_a/img/b_image.jpg'), 'Image from cross-collection reference should be copied'
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# Atlas Module
|
||||
# DocTree Module
|
||||
|
||||
A lightweight document collection manager for V, inspired by doctree but simplified.
|
||||
|
||||
@@ -18,7 +18,7 @@ put in .hero file and execute with hero or but shebang line on top of .hero scri
|
||||
|
||||
**Scan Parameters:**
|
||||
|
||||
- `name` (optional, default: 'main') - Atlas instance name
|
||||
- `name` (optional, default: 'main') - DocTree instance name
|
||||
- `path` (required when git_url not provided) - Directory path to scan
|
||||
- `git_url` (alternative to path) - Git repository URL to clone/checkout
|
||||
- `git_root` (optional when using git_url, default: ~/code) - Base directory for cloning
|
||||
@@ -31,9 +31,9 @@ put in .hero file and execute with hero or but shebang line on top of .hero scri
|
||||
```heroscript
|
||||
#!/usr/bin/env hero
|
||||
|
||||
!!atlas.scan git_url:"https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/collections/tests"
|
||||
!!doctree.scan git_url:"https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/collections/tests"
|
||||
|
||||
!!atlas.export destination: '/tmp/atlas_export'
|
||||
!!doctree.export
|
||||
|
||||
```
|
||||
|
||||
@@ -42,10 +42,10 @@ put this in .hero file
|
||||
## usage in herolib
|
||||
|
||||
```v
|
||||
import incubaid.herolib.data.atlas
|
||||
import incubaid.herolib.web.doctree
|
||||
|
||||
// Create a new Atlas
|
||||
mut a := atlas.new(name: 'my_docs')!
|
||||
// Create a new DocTree
|
||||
mut a := doctree.new(name: 'my_docs')!
|
||||
|
||||
// Scan a directory for collections
|
||||
a.scan(path: '/path/to/docs')!
|
||||
@@ -94,7 +94,7 @@ file := a.file_get('guides:diagram')!
|
||||
### Scanning for Collections
|
||||
|
||||
```v
|
||||
mut a := atlas.new()!
|
||||
mut a := doctree.new()!
|
||||
a.scan(path: './docs')!
|
||||
```
|
||||
|
||||
@@ -191,7 +191,7 @@ for _, col in a.collections {
|
||||
|
||||
### Include Processing
|
||||
|
||||
Atlas supports simple include processing using `!!include` actions:
|
||||
DocTree supports simple include processing using `!!include` actions:
|
||||
|
||||
```v
|
||||
// Export with includes processed (default)
|
||||
@@ -241,11 +241,11 @@ content := page.content()!
|
||||
|
||||
## Git Integration
|
||||
|
||||
Atlas automatically detects the git repository URL for each collection and stores it for reference. This allows users to easily navigate to the source for editing.
|
||||
DocTree automatically detects the git repository URL for each collection and stores it for reference. This allows users to easily navigate to the source for editing.
|
||||
|
||||
### Automatic Detection
|
||||
|
||||
When scanning collections, Atlas walks up the directory tree to find the `.git` directory and captures:
|
||||
When scanning collections, DocTree walks up the directory tree to find the `.git` directory and captures:
|
||||
- **git_url**: The remote origin URL
|
||||
- **git_branch**: The current branch
|
||||
|
||||
@@ -254,7 +254,7 @@ When scanning collections, Atlas walks up the directory tree to find the `.git`
|
||||
You can scan collections directly from a git repository:
|
||||
|
||||
```heroscript
|
||||
!!atlas.scan
|
||||
!!doctree.scan
|
||||
name: 'my_docs'
|
||||
git_url: 'https://github.com/myorg/docs.git'
|
||||
git_root: '~/code' // optional, defaults to ~/code
|
||||
@@ -265,7 +265,7 @@ The repository will be automatically cloned if it doesn't exist locally.
|
||||
### Accessing Edit URLs
|
||||
|
||||
```v
|
||||
mut page := atlas.page_get('guides:intro')!
|
||||
mut page := doctree.page_get('guides:intro')!
|
||||
edit_url := page.get_edit_url()!
|
||||
println('Edit at: ${edit_url}')
|
||||
// Output: Edit at: https://github.com/myorg/docs/edit/main/guides.md
|
||||
@@ -282,7 +282,7 @@ Collection guides source: https://github.com/myorg/docs.git (branch: main)
|
||||
This allows published documentation to link back to the source repository for contributions.
|
||||
## Links
|
||||
|
||||
Atlas supports standard Markdown links with several formats for referencing pages within collections.
|
||||
DocTree supports standard Markdown links with several formats for referencing pages within collections.
|
||||
|
||||
### Link Formats
|
||||
|
||||
@@ -313,14 +313,14 @@ Link using a path - **only the filename is used** for matching:
|
||||
|
||||
#### Validation
|
||||
|
||||
Check all links in your Atlas:
|
||||
Check all links in your DocTree:
|
||||
|
||||
```v
|
||||
mut a := atlas.new()!
|
||||
mut a := doctree.new()!
|
||||
a.scan(path: './docs')!
|
||||
|
||||
// Validate all links
|
||||
a.validate_links()!
|
||||
a.find_links()!
|
||||
|
||||
// Check for errors
|
||||
for _, col in a.collections {
|
||||
@@ -335,7 +335,7 @@ for _, col in a.collections {
|
||||
Automatically rewrite links with correct relative paths:
|
||||
|
||||
```v
|
||||
mut a := atlas.new()!
|
||||
mut a := doctree.new()!
|
||||
a.scan(path: './docs')!
|
||||
|
||||
// Fix all links in place
|
||||
@@ -384,7 +384,7 @@ After fix (assuming pages are in subdirectories):
|
||||
|
||||
### Export Directory Structure
|
||||
|
||||
When you export an Atlas, the directory structure is organized as:
|
||||
When you export an DocTree, the directory structure is organized as:
|
||||
|
||||
$$\text{export\_dir}/
|
||||
\begin{cases}
|
||||
@@ -409,17 +409,17 @@ $$\text{export\_dir}/
|
||||
|
||||
## Redis Integration
|
||||
|
||||
Atlas uses Redis to store metadata about collections, pages, images, and files for fast lookups and caching.
|
||||
DocTree uses Redis to store metadata about collections, pages, images, and files for fast lookups and caching.
|
||||
|
||||
### Redis Data Structure
|
||||
|
||||
When `redis: true` is set during export, Atlas stores:
|
||||
When `redis: true` is set during export, DocTree stores:
|
||||
|
||||
1. **Collection Paths** - Hash: `atlas:path`
|
||||
1. **Collection Paths** - Hash: `doctree:path`
|
||||
- Key: collection name
|
||||
- Value: exported collection directory path
|
||||
|
||||
2. **Collection Contents** - Hash: `atlas:<collection_name>`
|
||||
2. **Collection Contents** - Hash: `doctree:<collection_name>`
|
||||
- Pages: `page_name` → `page_name.md`
|
||||
- Images: `image_name.ext` → `img/image_name.ext`
|
||||
- Files: `file_name.ext` → `files/file_name.ext`
|
||||
@@ -427,11 +427,11 @@ When `redis: true` is set during export, Atlas stores:
|
||||
### Redis Usage Examples
|
||||
|
||||
```v
|
||||
import incubaid.herolib.data.atlas
|
||||
import incubaid.herolib.web.doctree
|
||||
import incubaid.herolib.core.base
|
||||
|
||||
// Export with Redis metadata (default)
|
||||
mut a := atlas.new(name: 'docs')!
|
||||
mut a := doctree.new(name: 'docs')!
|
||||
a.scan(path: './docs')!
|
||||
a.export(
|
||||
destination: './output'
|
||||
@@ -443,15 +443,15 @@ mut context := base.context()!
|
||||
mut redis := context.redis()!
|
||||
|
||||
// Get collection path
|
||||
col_path := redis.hget('atlas:path', 'guides')!
|
||||
col_path := redis.hget('doctree:path', 'guides')!
|
||||
println('Guides collection exported to: ${col_path}')
|
||||
|
||||
// Get page location
|
||||
page_path := redis.hget('atlas:guides', 'introduction')!
|
||||
page_path := redis.hget('doctree:guides', 'introduction')!
|
||||
println('Introduction page: ${page_path}') // Output: introduction.md
|
||||
|
||||
// Get image location
|
||||
img_path := redis.hget('atlas:guides', 'logo.png')!
|
||||
img_path := redis.hget('doctree:guides', 'logo.png')!
|
||||
println('Logo image: ${img_path}') // Output: img/logo.png
|
||||
```
|
||||
|
||||
@@ -468,9 +468,9 @@ println('Logo image: ${img_path}') // Output: img/logo.png
|
||||
Save collection metadata to JSON files for archival or cross-tool compatibility:
|
||||
|
||||
```v
|
||||
import incubaid.herolib.data.atlas
|
||||
import incubaid.herolib.web.doctree
|
||||
|
||||
mut a := atlas.new(name: 'my_docs')!
|
||||
mut a := doctree.new(name: 'my_docs')!
|
||||
a.scan(path: './docs')!
|
||||
|
||||
// Save all collections to a specified directory
|
||||
@@ -497,32 +497,32 @@ save_path/
|
||||
|
||||
## HeroScript Integration
|
||||
|
||||
Atlas integrates with HeroScript, allowing you to define Atlas operations in `.vsh` or playbook files.
|
||||
DocTree integrates with HeroScript, allowing you to define DocTree operations in `.vsh` or playbook files.
|
||||
|
||||
### Using in V Scripts
|
||||
|
||||
Create a `.vsh` script to process Atlas operations:
|
||||
Create a `.vsh` script to process DocTree operations:
|
||||
|
||||
```v
|
||||
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import incubaid.herolib.core.playbook
|
||||
import incubaid.herolib.data.atlas
|
||||
import incubaid.herolib.web.doctree
|
||||
|
||||
// Define your HeroScript content
|
||||
heroscript := "
|
||||
!!atlas.scan path: './docs'
|
||||
!!doctree.scan path: './docs'
|
||||
|
||||
!!atlas.export destination: './output' include: true
|
||||
!!doctree.export destination: './output' include: true
|
||||
"
|
||||
|
||||
// Create playbook from text
|
||||
mut plbook := playbook.new(text: heroscript)!
|
||||
|
||||
// Execute atlas actions
|
||||
atlas.play(mut plbook)!
|
||||
// Execute doctree actions
|
||||
doctree.play(mut plbook)!
|
||||
|
||||
println('Atlas processing complete!')
|
||||
println('DocTree processing complete!')
|
||||
```
|
||||
|
||||
### Using in Playbook Files
|
||||
@@ -530,11 +530,11 @@ println('Atlas processing complete!')
|
||||
Create a `docs.play` file:
|
||||
|
||||
```heroscript
|
||||
!!atlas.scan
|
||||
!!doctree.scan
|
||||
name: 'main'
|
||||
path: '~/code/docs'
|
||||
|
||||
!!atlas.export
|
||||
!!doctree.export
|
||||
destination: '~/code/output'
|
||||
reset: true
|
||||
include: true
|
||||
@@ -565,11 +565,11 @@ playcmds.run(mut plbook)!
|
||||
Errors are automatically collected and reported:
|
||||
|
||||
```heroscript
|
||||
!!atlas.scan
|
||||
!!doctree.scan
|
||||
path: './docs'
|
||||
|
||||
# Errors will be printed during export
|
||||
!!atlas.export
|
||||
!!doctree.export
|
||||
destination: './output'
|
||||
```
|
||||
|
||||
@@ -583,13 +583,13 @@ Collection guides - Errors (2)
|
||||
|
||||
### Auto-Export Behavior
|
||||
|
||||
If you use `!!atlas.scan` **without** an explicit `!!atlas.export`, Atlas will automatically export to the default location (current directory).
|
||||
If you use `!!doctree.scan` **without** an explicit `!!doctree.export`, DocTree will automatically export to the default location (current directory).
|
||||
|
||||
To disable auto-export, include an explicit (empty) export action or simply don't include any scan actions.
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Always validate before export**: Use `!!atlas.validate` to catch broken links early
|
||||
1. **Always validate before export**: Use `!!doctree.validate` to catch broken links early
|
||||
2. **Use named instances**: When working with multiple documentation sets, use the `name` parameter
|
||||
3. **Enable Redis for production**: Use `redis: true` for web deployments to enable fast lookups
|
||||
4. **Process includes during export**: Keep `include: true` to embed referenced content in exported files
|
||||
@@ -599,7 +599,7 @@ The following features are planned but not yet available:
|
||||
|
||||
- [ ] Load collections from `.collection.json` files
|
||||
- [ ] Python API for reading collections
|
||||
- [ ] `atlas.validate` playbook action
|
||||
- [ ] `atlas.fix_links` playbook action
|
||||
- [ ] `doctree.validate` playbook action
|
||||
- [ ] `doctree.fix_links` playbook action
|
||||
- [ ] Auto-save on collection modifications
|
||||
- [ ] Collection version control
|
||||
187
lib/web/doctree/core/recursive_link_test.v
Normal file
187
lib/web/doctree/core/recursive_link_test.v
Normal file
@@ -0,0 +1,187 @@
|
||||
module core
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
import os
|
||||
import json
|
||||
const test_base = '/tmp/doctree_test'
|
||||
|
||||
// Test recursive export with chained cross-collection links
|
||||
// Setup: Collection A links to B, Collection B links to C
|
||||
// Expected: When exporting A, it should include pages from B and C
|
||||
fn test_export_recursive_links() {
|
||||
|
||||
os.rmdir_all('${test_base}') or { }
|
||||
|
||||
// Create 3 collections with chained links
|
||||
col_a_path := '${test_base}/recursive_export/col_a'
|
||||
col_b_path := '${test_base}/recursive_export/col_b'
|
||||
col_c_path := '${test_base}/recursive_export/col_c'
|
||||
|
||||
os.mkdir_all(col_a_path)!
|
||||
os.mkdir_all(col_b_path)!
|
||||
os.mkdir_all(col_c_path)!
|
||||
|
||||
// Collection A: links to B
|
||||
mut cfile_a := pathlib.get_file(path: '${col_a_path}/.collection', create: true)!
|
||||
cfile_a.write('name:col_a')!
|
||||
mut page_a := pathlib.get_file(path: '${col_a_path}/page_a.md', create: true)!
|
||||
page_a.write('# Page A\n\nThis is page A.\n\n[Link to Page B](col_b:page_b)')!
|
||||
|
||||
// Collection B: links to C
|
||||
mut cfile_b := pathlib.get_file(path: '${col_b_path}/.collection', create: true)!
|
||||
cfile_b.write('name:col_b')!
|
||||
mut page_b := pathlib.get_file(path: '${col_b_path}/page_b.md', create: true)!
|
||||
page_b.write('# Page B\n\nThis is page B with link to C.\n\n[Link to Page C](col_c:page_c)')!
|
||||
|
||||
// Collection C: final page
|
||||
mut cfile_c := pathlib.get_file(path: '${col_c_path}/.collection', create: true)!
|
||||
cfile_c.write('name:col_c')!
|
||||
mut page_c := pathlib.get_file(path: '${col_c_path}/page_c.md', create: true)!
|
||||
page_c.write('# Page C\n\nThis is the final page in the chain.')!
|
||||
|
||||
// Create DocTree and add all collections
|
||||
mut a := new()!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_a_path)!)!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_b_path)!)!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_c_path)!)!
|
||||
|
||||
// Export
|
||||
export_path := '${test_base}/export_recursive'
|
||||
a.export(destination: export_path)!
|
||||
|
||||
// ===== VERIFICATION PHASE =====
|
||||
|
||||
// 1. Verify directory structure exists
|
||||
assert os.exists('${export_path}/content'), 'Export content directory should exist'
|
||||
assert os.exists('${export_path}/content/col_a'), 'Collection col_a directory should exist'
|
||||
assert os.exists('${export_path}/meta'), 'Export meta directory should exist'
|
||||
|
||||
// 2. Verify all pages exist in col_a export directory
|
||||
// Note: Exported pages from other collections go to col_a directory
|
||||
assert os.exists('${export_path}/content/col_a/page_a.md'), 'page_a.md should be exported'
|
||||
assert os.exists('${export_path}/content/col_a/page_b.md'), 'page_b.md from col_b should be included'
|
||||
assert os.exists('${export_path}/content/col_a/page_c.md'), 'page_c.md from col_c should be included'
|
||||
|
||||
assert os.exists('${export_path}/content/col_b/page_a.md')==false, 'page_a.md should not be exported'
|
||||
assert os.exists('${export_path}/content/col_b/page_b.md'), 'page_b.md from col_b should be included'
|
||||
assert os.exists('${export_path}/content/col_a/page_c.md'), 'page_c.md from col_c should be included'
|
||||
|
||||
assert os.exists('${export_path}/content/col_c/page_a.md')==false, 'page_a.md should not be exported'
|
||||
assert os.exists('${export_path}/content/col_c/page_b.md')==false, 'page_b.md from col_b should not be included'
|
||||
assert os.exists('${export_path}/content/col_c/page_c.md'), 'page_c.md from col_c should be included'
|
||||
|
||||
|
||||
// 3. Verify page content is correct
|
||||
content_a := os.read_file('${export_path}/content/col_a/page_a.md')!
|
||||
assert content_a.contains('# Page A'), 'page_a content should have title'
|
||||
assert content_a.contains('This is page A'), 'page_a content should have expected text'
|
||||
assert content_a.contains('[Link to Page B]'), 'page_a should have link to page_b'
|
||||
|
||||
content_b := os.read_file('${export_path}/content/col_a/page_b.md')!
|
||||
assert content_b.contains('# Page B'), 'page_b content should have title'
|
||||
assert content_b.contains('This is page B'), 'page_b content should have expected text'
|
||||
assert content_b.contains('[Link to Page C]'), 'page_b should have link to page_c'
|
||||
|
||||
content_c := os.read_file('${export_path}/content/col_a/page_c.md')!
|
||||
assert content_c.contains('# Page C'), 'page_c content should have title'
|
||||
assert content_c.contains('This is the final page'), 'page_c content should have expected text'
|
||||
|
||||
// 4. Verify metadata exists and is valid
|
||||
assert os.exists('${export_path}/meta/col_a.json'), 'Metadata file for col_a should exist'
|
||||
|
||||
meta_content := os.read_file('${export_path}/meta/col_a.json')!
|
||||
assert meta_content.len > 0, 'Metadata file should not be empty'
|
||||
|
||||
// Parse metadata JSON and verify structure
|
||||
mut meta := json.decode(DocTree, meta_content) or {
|
||||
panic('Failed to parse metadata JSON: ${err}')
|
||||
}
|
||||
|
||||
assert meta.name != "", 'Metadata should have name field'
|
||||
|
||||
//check metadata for all collections exists
|
||||
assert os.exists('${export_path}/meta/col_a.json'), 'col_a metadata should exist'
|
||||
assert os.exists('${export_path}/meta/col_b.json'), 'col_b metadata should exist'
|
||||
assert os.exists('${export_path}/meta/col_c.json'), 'col_c metadata should exist'
|
||||
|
||||
// 6. Verify the recursive depth worked
|
||||
// All three pages should be accessible through the exported col_a
|
||||
assert os.exists('${export_path}/content/col_a/page_a.md'), 'Level 1 page should exist'
|
||||
assert os.exists('${export_path}/content/col_a/page_b.md'), 'Level 2 page (via A->B) should exist'
|
||||
assert os.exists('${export_path}/content/col_a/page_c.md'), 'Level 3 page (via A->B->C) should exist'
|
||||
|
||||
// 7. Verify that the link chain is properly documented
|
||||
// page_a links to page_b, page_b links to page_c
|
||||
// The links should be preserved in the exported content
|
||||
page_a_content := os.read_file('${export_path}/content/col_a/page_a.md')!
|
||||
page_b_content := os.read_file('${export_path}/content/col_a/page_b.md')!
|
||||
page_c_content := os.read_file('${export_path}/content/col_a/page_c.md')!
|
||||
|
||||
// Links are preserved with collection:page format
|
||||
assert page_a_content.contains('col_b:page_b') || page_a_content.contains('page_b'), 'page_a should reference page_b'
|
||||
|
||||
assert page_b_content.contains('col_c:page_c') || page_b_content.contains('page_c'), 'page_b should reference page_c'
|
||||
|
||||
println('✓ Recursive cross-collection export test passed')
|
||||
println(' - All 3 pages exported to col_a directory (A -> B -> C)')
|
||||
println(' - Content verified for all pages')
|
||||
println(' - Metadata validated')
|
||||
println(' - Link chain preserved')
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Test recursive export with cross-collection images
|
||||
// Setup: Collection A links to image in Collection B
|
||||
// Expected: Image should be copied to col_a export directory
|
||||
fn test_export_recursive_with_images() {
|
||||
os.rmdir_all('${test_base}') or { }
|
||||
|
||||
col_a_path := '${test_base}/recursive_img/col_a'
|
||||
col_b_path := '${test_base}/recursive_img/col_b'
|
||||
|
||||
os.mkdir_all(col_a_path)!
|
||||
os.mkdir_all(col_b_path)!
|
||||
os.mkdir_all('${col_a_path}/img')!
|
||||
os.mkdir_all('${col_b_path}/img')!
|
||||
|
||||
// Collection A with local image
|
||||
mut cfile_a := pathlib.get_file(path: '${col_a_path}/.collection', create: true)!
|
||||
cfile_a.write('name:col_a')!
|
||||
|
||||
mut page_a := pathlib.get_file(path: '${col_a_path}/page_a.md', create: true)!
|
||||
page_a.write('# Page A\n\n\n\n[Link to B](col_b:page_b)')!
|
||||
|
||||
// Create local image
|
||||
os.write_file('${col_a_path}/img/local.png', 'fake png data')!
|
||||
|
||||
// Collection B with image and linked page
|
||||
mut cfile_b := pathlib.get_file(path: '${col_b_path}/.collection', create: true)!
|
||||
cfile_b.write('name:col_b')!
|
||||
|
||||
mut page_b := pathlib.get_file(path: '${col_b_path}/page_b.md', create: true)!
|
||||
page_b.write('# Page B\n\n')!
|
||||
|
||||
// Create image in collection B
|
||||
os.write_file('${col_b_path}/img/b_image.jpg', 'fake jpg data')!
|
||||
|
||||
// Create DocTree
|
||||
mut a := new()!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_a_path)!)!
|
||||
a.add_collection(mut pathlib.get_dir(path: col_b_path)!)!
|
||||
|
||||
export_path := '${test_base}/export_recursive_img'
|
||||
a.export(destination: export_path)!
|
||||
|
||||
// Verify pages exported
|
||||
assert os.exists('${export_path}/content/col_a/page_a.md'), 'page_a should exist'
|
||||
assert os.exists('${export_path}/content/col_a/page_b.md'), 'page_b from col_b should be included'
|
||||
|
||||
|
||||
|
||||
// Verify images exported to col_a image directory
|
||||
assert os.exists('${export_path}/content/col_a/img/local.png'), 'Local image should exist'
|
||||
assert os.exists('${export_path}/content/col_a/img/b_image.jpg'), 'Image from cross-collection reference should be copied'
|
||||
|
||||
println('✓ Recursive cross-collection with images test passed')
|
||||
}
|
||||
62
lib/web/doctree/meta/factory.v
Normal file
62
lib/web/doctree/meta/factory.v
Normal file
@@ -0,0 +1,62 @@
|
||||
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{
|
||||
config: SiteConfig{
|
||||
name: name
|
||||
}
|
||||
root: Category{}
|
||||
}
|
||||
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 reset() {
|
||||
sites_global.clear()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
11
lib/web/doctree/meta/model_announcement.v
Normal file
11
lib/web/doctree/meta/model_announcement.v
Normal file
@@ -0,0 +1,11 @@
|
||||
module meta
|
||||
|
||||
// Announcement bar config structure
|
||||
pub struct Announcement {
|
||||
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']
|
||||
}
|
||||
7
lib/web/doctree/meta/model_builddest.v
Normal file
7
lib/web/doctree/meta/model_builddest.v
Normal file
@@ -0,0 +1,7 @@
|
||||
module meta
|
||||
|
||||
pub struct BuildDest {
|
||||
pub mut:
|
||||
path string
|
||||
ssh_name string
|
||||
}
|
||||
89
lib/web/doctree/meta/model_category.v
Normal file
89
lib/web/doctree/meta/model_category.v
Normal file
@@ -0,0 +1,89 @@
|
||||
module meta
|
||||
|
||||
@[heap]
|
||||
struct Category {
|
||||
pub mut:
|
||||
path string // e.g. Operations/Daily (means 2 levels deep, first level is Operations)
|
||||
collapsible bool = true
|
||||
collapsed bool
|
||||
items []CategoryItem
|
||||
}
|
||||
|
||||
// return the label of the category (last part of the path)
|
||||
pub fn (mut c Category) label() !string {
|
||||
if c.path.count('/') == 0 {
|
||||
return c.path
|
||||
}
|
||||
return c.path.all_after_last('/')
|
||||
}
|
||||
|
||||
type CategoryItem = Page | Link | Category
|
||||
|
||||
// return all items as CategoryItem references recursive
|
||||
pub fn (mut self Category) items_get() ![]&CategoryItem {
|
||||
mut result := []&CategoryItem{}
|
||||
for i in 0 .. self.items.len {
|
||||
mut c := self.items[i]
|
||||
match mut c {
|
||||
Category {
|
||||
result << c.items_get()!
|
||||
}
|
||||
else {
|
||||
result << &c
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
pub fn (mut self Category) page_get(src string) !&Page {
|
||||
for c in self.items_get()! {
|
||||
match c {
|
||||
Page {
|
||||
if c.src == src {
|
||||
return &c
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
return error('Page with src="${src}" not found in site.')
|
||||
}
|
||||
|
||||
pub fn (mut self Category) link_get(href string) !&Link {
|
||||
for c in self.items_get()! {
|
||||
match c {
|
||||
Link {
|
||||
if c.href == href {
|
||||
return &c
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
return error('Link with href="${href}" not found in site.')
|
||||
}
|
||||
|
||||
pub fn (mut self Category) category_get(path string) !&Category {
|
||||
for i in 0 .. self.items.len {
|
||||
mut c := self.items[i]
|
||||
match mut c {
|
||||
Category {
|
||||
if c.path == path {
|
||||
return &c
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
mut new_category := Category{
|
||||
path: path
|
||||
collapsible: true
|
||||
collapsed: true
|
||||
items: []CategoryItem{}
|
||||
}
|
||||
// Add the new category as a sum type variant
|
||||
self.items << new_category
|
||||
// Update current_category_ref to point to the newly added category in the slice
|
||||
return &new_category
|
||||
}
|
||||
76
lib/web/doctree/meta/model_category_str.v
Normal file
76
lib/web/doctree/meta/model_category_str.v
Normal file
@@ -0,0 +1,76 @@
|
||||
module meta
|
||||
|
||||
pub fn (mut self Category) str() string {
|
||||
mut result := []string{}
|
||||
|
||||
if self.items.len == 0 {
|
||||
return 'Sidebar is empty\n'
|
||||
}
|
||||
|
||||
result << '📑 SIDEBAR STRUCTURE'
|
||||
result << '━'.repeat(60)
|
||||
|
||||
for i, item in self.items {
|
||||
is_last := i == self.items.len - 1
|
||||
prefix := if is_last { '└── ' } else { '├── ' }
|
||||
|
||||
match item {
|
||||
Page {
|
||||
result << '${prefix}📄 ${item.label}'
|
||||
result << ' └─ src: ${item.src}'
|
||||
}
|
||||
Category {
|
||||
// Category header
|
||||
collapse_icon := if item.collapsed { '▶ ' } else { '▼ ' }
|
||||
result << '${prefix}${collapse_icon}📁 ${item.path}'
|
||||
|
||||
// Category metadata
|
||||
if !item.collapsed {
|
||||
result << ' ├─ collapsible: ${item.collapsible}'
|
||||
result << ' └─ items: ${item.items.len}'
|
||||
|
||||
// Sub-items
|
||||
for j, sub_item in item.items {
|
||||
is_last_sub := j == item.items.len - 1
|
||||
sub_prefix := if is_last_sub { ' └── ' } else { ' ├── ' }
|
||||
|
||||
match sub_item {
|
||||
Page {
|
||||
result << '${sub_prefix}📄 ${sub_item.label} [${sub_item.src}]'
|
||||
}
|
||||
Category {
|
||||
// Nested categories
|
||||
sub_collapse_icon := if sub_item.collapsed { '▶ ' } else { '▼ ' }
|
||||
result << '${sub_prefix}${sub_collapse_icon}📁 ${sub_item.path}'
|
||||
}
|
||||
Link {
|
||||
result << '${sub_prefix}🔗 ${sub_item.label}'
|
||||
if sub_item.description.len > 0 {
|
||||
result << ' └─ ${sub_item.description}'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Link {
|
||||
result << '${prefix}🔗 ${item.label}'
|
||||
result << ' └─ href: ${item.href}'
|
||||
if item.description.len > 0 {
|
||||
result << ' └─ desc: ${item.description}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add spacing between root items
|
||||
if i < self.items.len - 1 {
|
||||
result << ''
|
||||
}
|
||||
}
|
||||
|
||||
result << '━'.repeat(60)
|
||||
result << '📊 SUMMARY'
|
||||
result << ' Total items: ${self.items.len}'
|
||||
|
||||
return result.join('\n') + '\n'
|
||||
}
|
||||
11
lib/web/doctree/meta/model_import.v
Normal file
11
lib/web/doctree/meta/model_import.v
Normal file
@@ -0,0 +1,11 @@
|
||||
module meta
|
||||
|
||||
// is to import one 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:
|
||||
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
|
||||
}
|
||||
8
lib/web/doctree/meta/model_link.v
Normal file
8
lib/web/doctree/meta/model_link.v
Normal file
@@ -0,0 +1,8 @@
|
||||
module meta
|
||||
|
||||
struct Link {
|
||||
pub mut:
|
||||
label string
|
||||
href string
|
||||
description string
|
||||
}
|
||||
15
lib/web/doctree/meta/model_page.v
Normal file
15
lib/web/doctree/meta/model_page.v
Normal file
@@ -0,0 +1,15 @@
|
||||
module meta
|
||||
|
||||
// Page represents a single documentation page
|
||||
pub struct Page {
|
||||
pub mut:
|
||||
src string // Unique identifier: "collection:page_name" marks where the page is from. (is also name_fix'ed)
|
||||
label string // Display label in navigation e.g. "Getting Started"
|
||||
title string // Display title (optional, extracted from markdown if empty)
|
||||
description string // Brief description for metadata
|
||||
draft bool // Is this page a draft? Means only show in development mode
|
||||
hide_title bool // Should the title be hidden on the page?
|
||||
hide bool // Should the page be hidden from navigation?
|
||||
category_id int // Optional category ID this page belongs to, if 0 it means its at root level
|
||||
nav_path string // navigation path e.g. "Operations/Daily"
|
||||
}
|
||||
30
lib/web/doctree/meta/model_site.v
Normal file
30
lib/web/doctree/meta/model_site.v
Normal file
@@ -0,0 +1,30 @@
|
||||
module meta
|
||||
|
||||
@[heap]
|
||||
pub struct Site {
|
||||
pub mut:
|
||||
doctree_path string // path to the export of the doctree site
|
||||
config SiteConfig // Full site configuration
|
||||
root Category // The root category containing all top-level items
|
||||
announcements []Announcement // there can be more than 1 announcement
|
||||
imports []ImportItem
|
||||
build_dest []BuildDest // Production build destinations (from !!site.build_dest)
|
||||
build_dest_dev []BuildDest // Development build destinations (from !!site.build_dest_dev)
|
||||
}
|
||||
|
||||
pub fn (mut self Site) page_get(src string) !&Page {
|
||||
return self.root.page_get(src)!
|
||||
}
|
||||
|
||||
pub fn (mut self Site) link_get(href string) !&Link {
|
||||
return self.root.link_get(href)!
|
||||
}
|
||||
|
||||
pub fn (mut self Site) category_get(path string) !&Category {
|
||||
return self.root.category_get(path)!
|
||||
}
|
||||
|
||||
// sidebar returns the root category for building the sidebar navigation
|
||||
pub fn (mut self Site) sidebar() !&Category {
|
||||
return &self.root
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
module site
|
||||
module meta
|
||||
|
||||
import os
|
||||
// Combined config structure
|
||||
@@ -15,7 +15,6 @@ pub mut:
|
||||
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:)
|
||||
@@ -24,21 +23,6 @@ pub mut:
|
||||
|
||||
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
|
||||
@@ -78,20 +62,3 @@ pub mut:
|
||||
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.config
|
||||
|
||||
// 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 website)!
|
||||
|
||||
// ============================================================
|
||||
// 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 website)!
|
||||
|
||||
// ============================================================
|
||||
// STEP 7: Configure publish destinations
|
||||
// ============================================================
|
||||
console.print_item('Step 7: Configuring publish destinations')
|
||||
play_publishing(mut plbook, mut website)!
|
||||
|
||||
// ============================================================
|
||||
// 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')
|
||||
}
|
||||
30
lib/web/doctree/meta/play_announcement.v
Normal file
30
lib/web/doctree/meta/play_announcement.v
Normal file
@@ -0,0 +1,30 @@
|
||||
module meta
|
||||
|
||||
import incubaid.herolib.core.playbook { PlayBook }
|
||||
|
||||
// ============================================================
|
||||
// ANNOUNCEMENT: Process announcement bar (optional)
|
||||
// ============================================================
|
||||
fn play_announcement(mut plbook PlayBook, mut site Site) ! {
|
||||
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"')
|
||||
}
|
||||
|
||||
site.announcements << Announcement{
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
50
lib/web/doctree/meta/play_imports.v
Normal file
50
lib/web/doctree/meta/play_imports.v
Normal file
@@ -0,0 +1,50 @@
|
||||
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 site Site) ! {
|
||||
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{
|
||||
url: p.get_default('url', '')!
|
||||
path: import_path
|
||||
dest: p.get_default('dest', '')!
|
||||
replace: replace_map
|
||||
visible: p.get_default_false('visible')
|
||||
}
|
||||
|
||||
site.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
|
||||
}
|
||||
}
|
||||
116
lib/web/doctree/meta/play_pages_categories.v
Normal file
116
lib/web/doctree/meta/play_pages_categories.v
Normal file
@@ -0,0 +1,116 @@
|
||||
module meta
|
||||
|
||||
import incubaid.herolib.core.playbook { PlayBook }
|
||||
import incubaid.herolib.web.doctree as doctreetools
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
// ============================================================
|
||||
// PAGES & CATEGORIES: Process pages and build navigation structure
|
||||
// ============================================================
|
||||
fn play_pages(mut plbook PlayBook, mut website Site) ! {
|
||||
mut collection_current := ''
|
||||
mut category_current := &website.root // start at root category, this is basically the navigation tree root
|
||||
|
||||
// ============================================================
|
||||
// PASS 1: Process all page_category and page actions
|
||||
// ============================================================
|
||||
mut all_actions := plbook.find(filter: 'site.')!
|
||||
|
||||
for mut action in all_actions {
|
||||
if action.done {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip actions that are not page or page_category
|
||||
if action.name != 'page_category' && action.name != 'page' {
|
||||
continue
|
||||
}
|
||||
|
||||
// ========== PAGE CATEGORY ==========
|
||||
if action.name == 'page_category' {
|
||||
mut p := action.params
|
||||
|
||||
category_path := p.get_default('path', '')!
|
||||
if category_path.len == 0 {
|
||||
return error('!!site.page_category: must specify "path"')
|
||||
}
|
||||
|
||||
// Navigate/create category structure
|
||||
category_current = category_current.category_get(category_path)!
|
||||
category_current.collapsible = p.get_default_true('collapsible')
|
||||
category_current.collapsed = p.get_default_false('collapsed')
|
||||
|
||||
console.print_item('Created page category: "${category_current.path}"')
|
||||
|
||||
action.done = true
|
||||
println(category_current)
|
||||
|
||||
// $dbg();
|
||||
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(':') {
|
||||
page_collection, page_name = doctreetools.key_parse(page_src)!
|
||||
} else {
|
||||
// Use previously specified collection if available
|
||||
if collection_current.len > 0 {
|
||||
page_collection = collection_current
|
||||
page_name = doctreetools.name_fix(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
|
||||
|
||||
// Get optional page metadata
|
||||
mut page_label := p.get_default('label', '')! // CHANGED: added mut
|
||||
if page_label.len == 0 {
|
||||
page_label = p.get_default('title', '')!
|
||||
}
|
||||
|
||||
page_title := p.get_default('title', '')!
|
||||
page_description := p.get_default('description', '')!
|
||||
|
||||
// Create page object
|
||||
mut page := Page{
|
||||
src: '${page_collection}:${page_name}'
|
||||
label: page_label
|
||||
title: page_title
|
||||
description: page_description
|
||||
draft: p.get_default_false('draft')
|
||||
hide_title: p.get_default_false('hide_title')
|
||||
hide: p.get_default_false('hide')
|
||||
nav_path: category_current.path
|
||||
}
|
||||
|
||||
// Add page to current category
|
||||
category_current.items << page
|
||||
|
||||
console.print_item('Added page: "${page.src}" (label: "${page.label}")')
|
||||
|
||||
action.done = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
console.print_green('Pages and categories processing complete')
|
||||
}
|
||||
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 website Site) ! {
|
||||
// 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', '')!
|
||||
}
|
||||
website.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', '')!
|
||||
}
|
||||
website.build_dest_dev << dest
|
||||
action.done = true
|
||||
}
|
||||
}
|
||||
676
lib/web/doctree/meta/readme.md
Normal file
676
lib/web/doctree/meta/readme.md
Normal file
@@ -0,0 +1,676 @@
|
||||
# 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"
|
||||
label: "Getting Started"
|
||||
title: "Getting Started"
|
||||
|
||||
!!site.page src: "setup"
|
||||
label: "Installation"
|
||||
title: "Installation"
|
||||
```
|
||||
|
||||
### Processing with V Code
|
||||
|
||||
```v
|
||||
import incubaid.herolib.core.playbook
|
||||
import incubaid.herolib.web.doctree.meta as site_module
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
// Process HeroScript file
|
||||
mut plbook := playbook.new(path: './site_config.heroscript')!
|
||||
|
||||
// Execute site configuration
|
||||
site_module.play(mut plbook)!
|
||||
|
||||
// Access the configured site
|
||||
mut mysite := site_module.get(name: 'my_docs')!
|
||||
|
||||
// Print available pages
|
||||
for page in mysite.pages {
|
||||
console.print_item('Page: "${page.src}" - "${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_module.new(name: 'my_docs')!
|
||||
|
||||
// Get existing site
|
||||
mut mysite := site_module.get(name: 'my_docs')!
|
||||
|
||||
// Check if site exists
|
||||
if site_module.exists(name: 'my_docs') {
|
||||
println('Site exists')
|
||||
}
|
||||
|
||||
// Get all site names
|
||||
site_names := site_module.list() // Returns []string
|
||||
|
||||
// Get default site (creates if needed)
|
||||
mut default := site_module.default()!
|
||||
```
|
||||
|
||||
### Site Object Structure
|
||||
|
||||
```v
|
||||
@[heap]
|
||||
pub struct Site {
|
||||
pub mut:
|
||||
doctree_path string // path to the export of the doctree site
|
||||
config SiteConfig // Full site configuration
|
||||
pages []Page // Array of pages
|
||||
links []Link // Array of links
|
||||
categories []Category // Array of categories
|
||||
announcements []Announcement // Array of announcements (can be multiple)
|
||||
imports []ImportItem // Array of imports
|
||||
build_dest []BuildDest // Production build destinations
|
||||
build_dest_dev []BuildDest // Development build destinations
|
||||
}
|
||||
```
|
||||
|
||||
### Accessing Pages
|
||||
|
||||
```v
|
||||
// Access all pages
|
||||
pages := mysite.pages // []Page
|
||||
|
||||
// Access specific page by index
|
||||
page := mysite.pages[0]
|
||||
|
||||
// Page structure
|
||||
pub struct Page {
|
||||
pub mut:
|
||||
src string // "collection:page_name" format (unique identifier)
|
||||
label string // Display label in navigation
|
||||
title string // Display title on page (extracted from markdown if empty)
|
||||
description string // SEO metadata
|
||||
draft bool // Hide from navigation if true
|
||||
hide_title bool // Don't show title on page
|
||||
hide bool // Hide page completely
|
||||
category_id int // Optional category ID (0 = root level)
|
||||
}
|
||||
```
|
||||
|
||||
### Categories and Navigation
|
||||
|
||||
```v
|
||||
// Access all categories
|
||||
categories := mysite.categories // []Category
|
||||
|
||||
// Category structure
|
||||
pub struct Category {
|
||||
pub mut:
|
||||
path string // e.g., "Getting Started" or "Operations/Daily"
|
||||
collapsible bool = true
|
||||
collapsed bool
|
||||
}
|
||||
|
||||
// Generate sidebar navigation
|
||||
sidebar := mysite.sidebar()! // Returns SideBar
|
||||
|
||||
// Sidebar structure
|
||||
pub struct SideBar {
|
||||
pub mut:
|
||||
my_sidebar []NavItem
|
||||
}
|
||||
|
||||
pub type NavItem = NavDoc | NavCat | NavLink
|
||||
|
||||
pub struct NavDoc {
|
||||
pub:
|
||||
path string // path is $collection/$name without .md
|
||||
label string
|
||||
}
|
||||
|
||||
pub struct NavCat {
|
||||
pub mut:
|
||||
label string
|
||||
collapsible bool = true
|
||||
collapsed bool
|
||||
items []NavItem // nested NavDoc/NavCat/NavLink
|
||||
}
|
||||
|
||||
pub struct NavLink {
|
||||
pub:
|
||||
label string
|
||||
href string
|
||||
description string
|
||||
}
|
||||
|
||||
// Example: iterate navigation
|
||||
sidebar := mysite.sidebar()!
|
||||
for item in sidebar.my_sidebar {
|
||||
match item {
|
||||
NavDoc {
|
||||
println('Page: ${item.label} (${item.path})')
|
||||
}
|
||||
NavCat {
|
||||
println('Category: ${item.label} (${item.items.len} items)')
|
||||
}
|
||||
NavLink {
|
||||
println('Link: ${item.label} -> ${item.href}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print formatted sidebar
|
||||
println(mysite.str())
|
||||
```
|
||||
|
||||
### Site Configuration
|
||||
|
||||
```v
|
||||
@[heap]
|
||||
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
|
||||
|
||||
// Navigation & Footer
|
||||
footer Footer
|
||||
menu Menu
|
||||
|
||||
// Publishing
|
||||
build_dest []BuildDest // Production destinations
|
||||
build_dest_dev []BuildDest // Development destinations
|
||||
|
||||
// Imports
|
||||
imports []ImportItem
|
||||
}
|
||||
|
||||
pub struct BuildDest {
|
||||
pub mut:
|
||||
path string
|
||||
ssh_name string
|
||||
}
|
||||
|
||||
pub struct Menu {
|
||||
pub mut:
|
||||
title string
|
||||
items []MenuItem
|
||||
logo_alt string
|
||||
logo_src string
|
||||
logo_src_dark string
|
||||
}
|
||||
|
||||
pub struct MenuItem {
|
||||
pub mut:
|
||||
href string
|
||||
to string
|
||||
label string
|
||||
position string // "left" or "right"
|
||||
}
|
||||
|
||||
pub struct Footer {
|
||||
pub mut:
|
||||
style string // e.g., "dark" or "light"
|
||||
links []FooterLink
|
||||
}
|
||||
|
||||
pub struct FooterLink {
|
||||
pub mut:
|
||||
title string
|
||||
items []FooterItem
|
||||
}
|
||||
|
||||
pub struct FooterItem {
|
||||
pub mut:
|
||||
label string
|
||||
to string
|
||||
href string
|
||||
}
|
||||
|
||||
pub struct Announcement {
|
||||
pub mut:
|
||||
content string
|
||||
background_color string
|
||||
text_color string
|
||||
is_closeable bool
|
||||
}
|
||||
|
||||
pub struct ImportItem {
|
||||
pub mut:
|
||||
url string // http or git url
|
||||
path string
|
||||
dest string // location in docs folder
|
||||
replace map[string]string
|
||||
visible bool = true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Site
|
||||
A website configuration that contains pages, navigation structure, and metadata. Each site is registered globally and can be retrieved by name.
|
||||
|
||||
### Page
|
||||
A single documentation page with:
|
||||
- **src**: `collection:page_name` format (unique identifier)
|
||||
- **label**: Display name in sidebar
|
||||
- **title**: Display name on page (extracted from markdown if empty)
|
||||
- **description**: SEO metadata
|
||||
- **draft**: Hidden from navigation if true
|
||||
- **category_id**: Links page to a category (0 = root level)
|
||||
|
||||
### Category (Section)
|
||||
Groups related pages together in the navigation sidebar. Categories can be nested and are automatically collapsed/expandable.
|
||||
|
||||
```heroscript
|
||||
!!site.page_category
|
||||
path: "Getting Started"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "tech:intro"
|
||||
category_id: 1 // Links to the category above
|
||||
```
|
||||
|
||||
### 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)
|
||||
|
||||
Multiple announcements are supported and stored in an array:
|
||||
|
||||
```heroscript
|
||||
!!site.announcement
|
||||
content: "🎉 Version 2.0 is now available!"
|
||||
background_color: "#20232a"
|
||||
text_color: "#fff"
|
||||
is_closeable: true
|
||||
```
|
||||
|
||||
**Note:** Each `!!site.announcement` block adds to the `announcements[]` array. Only the first is typically displayed, but all are stored.
|
||||
|
||||
### 6. Pages and Categories
|
||||
|
||||
#### Simple: Pages Without Categories
|
||||
|
||||
```heroscript
|
||||
!!site.page src: "guides:introduction"
|
||||
label: "Getting Started"
|
||||
title: "Getting Started"
|
||||
description: "Introduction to the platform"
|
||||
|
||||
!!site.page src: "installation"
|
||||
label: "Installation"
|
||||
title: "Installation"
|
||||
```
|
||||
|
||||
#### Advanced: Pages With Categories
|
||||
|
||||
```heroscript
|
||||
!!site.page_category
|
||||
path: "Getting Started"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "guides:introduction"
|
||||
label: "Introduction"
|
||||
title: "Introduction"
|
||||
description: "Learn the basics"
|
||||
|
||||
!!site.page src: "installation"
|
||||
label: "Installation"
|
||||
title: "Installation"
|
||||
|
||||
!!site.page src: "configuration"
|
||||
label: "Configuration"
|
||||
title: "Configuration"
|
||||
|
||||
!!site.page_category
|
||||
path: "Advanced Topics"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "advanced:performance"
|
||||
label: "Performance Tuning"
|
||||
title: "Performance Tuning"
|
||||
|
||||
!!site.page src: "scaling"
|
||||
label: "Scaling Guide"
|
||||
title: "Scaling Guide"
|
||||
```
|
||||
|
||||
**Page Parameters:**
|
||||
- `src` - Source as `collection:page_name` (first page) or just `page_name` (reuse collection)
|
||||
- `label` - Display label in sidebar (required)
|
||||
- `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)
|
||||
- `hide` - Hide page completely (default: false)
|
||||
|
||||
**Category Parameters:**
|
||||
- `path` - Category path/label (required)
|
||||
- `collapsible` - Allow collapsing (default: true)
|
||||
- `collapsed` - Initially collapsed (default: false)
|
||||
|
||||
### 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
|
||||
path: "Getting Started"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "docs:intro"
|
||||
label: "Introduction"
|
||||
title: "Introduction"
|
||||
|
||||
!!site.page src: "installation"
|
||||
label: "Installation"
|
||||
title: "Installation"
|
||||
|
||||
!!site.page_category
|
||||
path: "Core Concepts"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "concepts:architecture"
|
||||
label: "Architecture"
|
||||
title: "Architecture"
|
||||
|
||||
!!site.page src: "components"
|
||||
label: "Components"
|
||||
title: "Components"
|
||||
|
||||
!!site.page_category
|
||||
path: "API Reference"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "api:rest"
|
||||
label: "REST API"
|
||||
title: "REST API"
|
||||
|
||||
!!site.page src: "graphql"
|
||||
label: "GraphQL"
|
||||
title: "GraphQL"
|
||||
```
|
||||
|
||||
### Pattern 2: Simple Blog/Knowledge Base
|
||||
|
||||
```heroscript
|
||||
!!site.config
|
||||
name: "blog"
|
||||
title: "Knowledge Base"
|
||||
|
||||
!!site.page src: "articles:first_post"
|
||||
label: "Welcome to Our Blog"
|
||||
title: "Welcome to Our Blog"
|
||||
|
||||
!!site.page src: "second_post"
|
||||
label: "Understanding the Basics"
|
||||
title: "Understanding the Basics"
|
||||
|
||||
!!site.page src: "third_post"
|
||||
label: "Advanced Techniques"
|
||||
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
|
||||
path: "Product Guide"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "docs:overview"
|
||||
label: "Overview"
|
||||
title: "Overview"
|
||||
|
||||
!!site.page src: "features"
|
||||
label: "Features"
|
||||
title: "Features"
|
||||
|
||||
!!site.page_category
|
||||
path: "Shared Resources"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "shared:common"
|
||||
label: "Common Patterns"
|
||||
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
|
||||
```
|
||||
746
lib/web/doctree/meta2/site_nav_test.v
Normal file
746
lib/web/doctree/meta2/site_nav_test.v
Normal file
@@ -0,0 +1,746 @@
|
||||
module meta
|
||||
|
||||
import incubaid.herolib.core.playbook
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
// Comprehensive HeroScript for testing multi-level navigation depths
|
||||
const test_heroscript_nav_depth = '
|
||||
!!site.config
|
||||
name: "nav_depth_test"
|
||||
title: "Navigation Depth Test Site"
|
||||
description: "Testing multi-level nested navigation"
|
||||
tagline: "Deep navigation structures"
|
||||
|
||||
!!site.navbar
|
||||
title: "Nav Depth Test"
|
||||
|
||||
!!site.navbar_item
|
||||
label: "Home"
|
||||
to: "/"
|
||||
position: "left"
|
||||
|
||||
// ============================================================
|
||||
// LEVEL 1: Simple top-level category
|
||||
// ============================================================
|
||||
!!site.page_category
|
||||
path: "Why"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
//COLLECTION WILL BE REPEATED, HAS NO INFLUENCE ON NAVIGATION LEVELS
|
||||
!!site.page src: "mycollection:intro"
|
||||
label: "Why Choose Us"
|
||||
title: "Why Choose Us"
|
||||
description: "Reasons to use this platform"
|
||||
|
||||
!!site.page src: "benefits"
|
||||
label: "Key Benefits"
|
||||
title: "Key Benefits"
|
||||
description: "Main benefits overview"
|
||||
|
||||
// ============================================================
|
||||
// LEVEL 1: Simple top-level category
|
||||
// ============================================================
|
||||
!!site.page_category
|
||||
path: "Tutorials"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "getting_started"
|
||||
label: "Getting Started"
|
||||
title: "Getting Started"
|
||||
description: "Basic tutorial to get started"
|
||||
|
||||
!!site.page src: "first_steps"
|
||||
label: "First Steps"
|
||||
title: "First Steps"
|
||||
description: "Your first steps with the platform"
|
||||
|
||||
// ============================================================
|
||||
// LEVEL 3: Three-level nested category (Tutorials > Operations > Urgent)
|
||||
// ============================================================
|
||||
!!site.page_category
|
||||
path: "Tutorials/Operations/Urgent"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "emergency_restart"
|
||||
label: "Emergency Restart"
|
||||
title: "Emergency Restart"
|
||||
description: "How to emergency restart the system"
|
||||
|
||||
!!site.page src: "critical_fixes"
|
||||
label: "Critical Fixes"
|
||||
title: "Critical Fixes"
|
||||
description: "Apply critical fixes immediately"
|
||||
|
||||
!!site.page src: "incident_response"
|
||||
label: "Incident Response"
|
||||
title: "Incident Response"
|
||||
description: "Handle incidents in real-time"
|
||||
|
||||
// ============================================================
|
||||
// LEVEL 2: Two-level nested category (Tutorials > Operations)
|
||||
// ============================================================
|
||||
!!site.page_category
|
||||
path: "Tutorials/Operations"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "daily_checks"
|
||||
label: "Daily Checks"
|
||||
title: "Daily Checks"
|
||||
description: "Daily maintenance checklist"
|
||||
|
||||
!!site.page src: "monitoring"
|
||||
label: "Monitoring"
|
||||
title: "Monitoring"
|
||||
description: "System monitoring procedures"
|
||||
|
||||
!!site.page src: "backups"
|
||||
label: "Backups"
|
||||
title: "Backups"
|
||||
description: "Backup and restore procedures"
|
||||
|
||||
// ============================================================
|
||||
// LEVEL 1: One-to-two level (Tutorials)
|
||||
// ============================================================
|
||||
// Note: This creates a sibling at the Tutorials level (not nested deeper)
|
||||
!!site.page src: "advanced_concepts"
|
||||
label: "Advanced Concepts"
|
||||
title: "Advanced Concepts"
|
||||
description: "Deep dive into advanced concepts"
|
||||
|
||||
!!site.page src: "troubleshooting"
|
||||
label: "Troubleshooting"
|
||||
title: "Troubleshooting"
|
||||
description: "Troubleshooting guide"
|
||||
|
||||
// ============================================================
|
||||
// LEVEL 2: Two-level nested category (Why > FAQ)
|
||||
// ============================================================
|
||||
!!site.page_category
|
||||
path: "Why/FAQ"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "general"
|
||||
label: "General Questions"
|
||||
title: "General Questions"
|
||||
description: "Frequently asked questions"
|
||||
|
||||
!!site.page src: "pricing_questions"
|
||||
label: "Pricing"
|
||||
title: "Pricing Questions"
|
||||
description: "Questions about pricing"
|
||||
|
||||
!!site.page src: "technical_faq"
|
||||
label: "Technical FAQ"
|
||||
title: "Technical FAQ"
|
||||
description: "Technical frequently asked questions"
|
||||
|
||||
!!site.page src: "support_faq"
|
||||
label: "Support"
|
||||
title: "Support FAQ"
|
||||
description: "Support-related FAQ"
|
||||
|
||||
// ============================================================
|
||||
// LEVEL 4: Four-level nested category (Tutorials > Operations > Database > Optimization)
|
||||
// ============================================================
|
||||
!!site.page_category
|
||||
path: "Tutorials/Operations/Database/Optimization"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "query_optimization"
|
||||
label: "Query Optimization"
|
||||
title: "Query Optimization"
|
||||
description: "Optimize your database queries"
|
||||
|
||||
!!site.page src: "indexing_strategy"
|
||||
label: "Indexing Strategy"
|
||||
title: "Indexing Strategy"
|
||||
description: "Effective indexing strategies"
|
||||
|
||||
!!site.page_category
|
||||
path: "Tutorials/Operations/Database"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "configuration"
|
||||
label: "Configuration"
|
||||
title: "Database Configuration"
|
||||
description: "Configure your database"
|
||||
|
||||
!!site.page src: "replication"
|
||||
label: "Replication"
|
||||
title: "Database Replication"
|
||||
description: "Set up database replication"
|
||||
|
||||
'
|
||||
|
||||
fn check(s2 Site) {
|
||||
mut s := Site{
|
||||
doctree_path: ''
|
||||
config: SiteConfig{
|
||||
name: 'nav_depth_test'
|
||||
title: 'Navigation Depth Test Site'
|
||||
description: 'Testing multi-level nested navigation'
|
||||
tagline: 'Deep navigation structures'
|
||||
favicon: 'img/favicon.png'
|
||||
image: 'img/tf_graph.png'
|
||||
copyright: '© 2025 Example Organization'
|
||||
footer: Footer{
|
||||
style: 'dark'
|
||||
links: []
|
||||
}
|
||||
menu: Menu{
|
||||
title: 'Nav Depth Test'
|
||||
items: [
|
||||
MenuItem{
|
||||
href: ''
|
||||
to: '/'
|
||||
label: 'Home'
|
||||
position: 'left'
|
||||
},
|
||||
]
|
||||
logo_alt: ''
|
||||
logo_src: ''
|
||||
logo_src_dark: ''
|
||||
}
|
||||
url: ''
|
||||
base_url: '/'
|
||||
url_home: ''
|
||||
meta_title: ''
|
||||
meta_image: ''
|
||||
}
|
||||
pages: [
|
||||
Page{
|
||||
src: 'mycollection:intro'
|
||||
label: 'Why Choose Us'
|
||||
title: 'Why Choose Us'
|
||||
description: 'Reasons to use this platform'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 0
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:benefits'
|
||||
label: 'Key Benefits'
|
||||
title: 'Key Benefits'
|
||||
description: 'Main benefits overview'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 0
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:getting_started'
|
||||
label: 'Getting Started'
|
||||
title: 'Getting Started'
|
||||
description: 'Basic tutorial to get started'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 1
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:first_steps'
|
||||
label: 'First Steps'
|
||||
title: 'First Steps'
|
||||
description: 'Your first steps with the platform'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 1
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:emergency_restart'
|
||||
label: 'Emergency Restart'
|
||||
title: 'Emergency Restart'
|
||||
description: 'How to emergency restart the system'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 2
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:critical_fixes'
|
||||
label: 'Critical Fixes'
|
||||
title: 'Critical Fixes'
|
||||
description: 'Apply critical fixes immediately'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 2
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:incident_response'
|
||||
label: 'Incident Response'
|
||||
title: 'Incident Response'
|
||||
description: 'Handle incidents in real-time'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 2
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:daily_checks'
|
||||
label: 'Daily Checks'
|
||||
title: 'Daily Checks'
|
||||
description: 'Daily maintenance checklist'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 3
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:monitoring'
|
||||
label: 'Monitoring'
|
||||
title: 'Monitoring'
|
||||
description: 'System monitoring procedures'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 3
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:backups'
|
||||
label: 'Backups'
|
||||
title: 'Backups'
|
||||
description: 'Backup and restore procedures'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 3
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:advanced_concepts'
|
||||
label: 'Advanced Concepts'
|
||||
title: 'Advanced Concepts'
|
||||
description: 'Deep dive into advanced concepts'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 3
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:troubleshooting'
|
||||
label: 'Troubleshooting'
|
||||
title: 'Troubleshooting'
|
||||
description: 'Troubleshooting guide'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 3
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:general'
|
||||
label: 'General Questions'
|
||||
title: 'General Questions'
|
||||
description: 'Frequently asked questions'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 4
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:pricing_questions'
|
||||
label: 'Pricing'
|
||||
title: 'Pricing Questions'
|
||||
description: 'Questions about pricing'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 4
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:technical_faq'
|
||||
label: 'Technical FAQ'
|
||||
title: 'Technical FAQ'
|
||||
description: 'Technical frequently asked questions'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 4
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:support_faq'
|
||||
label: 'Support'
|
||||
title: 'Support FAQ'
|
||||
description: 'Support-related FAQ'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 4
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:query_optimization'
|
||||
label: 'Query Optimization'
|
||||
title: 'Query Optimization'
|
||||
description: 'Optimize your database queries'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 5
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:indexing_strategy'
|
||||
label: 'Indexing Strategy'
|
||||
title: 'Indexing Strategy'
|
||||
description: 'Effective indexing strategies'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 5
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:configuration'
|
||||
label: 'Configuration'
|
||||
title: 'Database Configuration'
|
||||
description: 'Configure your database'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 6
|
||||
},
|
||||
Page{
|
||||
src: 'mycollection:replication'
|
||||
label: 'Replication'
|
||||
title: 'Database Replication'
|
||||
description: 'Set up database replication'
|
||||
draft: false
|
||||
hide_title: false
|
||||
hide: false
|
||||
category_id: 6
|
||||
},
|
||||
]
|
||||
links: []
|
||||
categories: [
|
||||
Category{
|
||||
path: 'Why'
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
},
|
||||
Category{
|
||||
path: 'Tutorials'
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
},
|
||||
Category{
|
||||
path: 'Tutorials/Operations/Urgent'
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
},
|
||||
Category{
|
||||
path: 'Tutorials/Operations'
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
},
|
||||
Category{
|
||||
path: 'Why/FAQ'
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
},
|
||||
Category{
|
||||
path: 'Tutorials/Operations/Database/Optimization'
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
},
|
||||
Category{
|
||||
path: 'Tutorials/Operations/Database'
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
},
|
||||
]
|
||||
announcements: []
|
||||
imports: []
|
||||
build_dest: []
|
||||
build_dest_dev: []
|
||||
}
|
||||
assert s == s2
|
||||
}
|
||||
|
||||
pub fn test_navigation_depth() ! {
|
||||
console.print_header('🧭 Navigation Depth Multi-Level Test')
|
||||
console.lf()
|
||||
|
||||
// ========================================================
|
||||
// SETUP: Create and process playbook
|
||||
// ========================================================
|
||||
console.print_item('Creating playbook from HeroScript')
|
||||
mut plbook := playbook.new(text: test_heroscript_nav_depth)!
|
||||
console.print_green('✓ Playbook created')
|
||||
console.lf()
|
||||
|
||||
console.print_item('Processing site configuration')
|
||||
play(mut plbook)!
|
||||
console.print_green('✓ Site processed')
|
||||
console.lf()
|
||||
|
||||
console.print_item('Retrieving configured site')
|
||||
mut nav_site := get(name: 'nav_depth_test')!
|
||||
console.print_green('✓ Site retrieved')
|
||||
console.lf()
|
||||
|
||||
check(nav_site)
|
||||
|
||||
// ========================================================
|
||||
// TEST 1: Validate Categories Structure
|
||||
// ========================================================
|
||||
console.print_header('TEST 1: Validate Categories Structure')
|
||||
console.print_item('Total categories: ${nav_site.categories.len}')
|
||||
|
||||
for i, category in nav_site.categories {
|
||||
depth := calculate_category_depth(category.path)
|
||||
console.print_debug(' [${i}] Path: "${category.path}" (Depth: ${depth})')
|
||||
}
|
||||
|
||||
// Assertions for category structure
|
||||
mut all_paths := nav_site.categories.map(it.path)
|
||||
|
||||
assert all_paths.contains('Why'), 'Missing "Why" category'
|
||||
console.print_green('✓ Level 1: "Why" found')
|
||||
|
||||
assert all_paths.contains('Tutorials'), 'Missing "Tutorials" category'
|
||||
console.print_green('✓ Level 1: "Tutorials" found')
|
||||
|
||||
assert all_paths.contains('Why/FAQ'), 'Missing "Why/FAQ" category'
|
||||
console.print_green('✓ Level 2: "Why/FAQ" found')
|
||||
|
||||
assert all_paths.contains('Tutorials/Operations'), 'Missing "Tutorials/Operations" category'
|
||||
console.print_green('✓ Level 2: "Tutorials/Operations" found')
|
||||
|
||||
assert all_paths.contains('Tutorials/Operations/Urgent'), 'Missing "Tutorials/Operations/Urgent" category'
|
||||
console.print_green('✓ Level 3: "Tutorials/Operations/Urgent" found')
|
||||
|
||||
assert all_paths.contains('Tutorials/Operations/Database'), 'Missing "Tutorials/Operations/Database" category'
|
||||
console.print_green('✓ Level 3: "Tutorials/Operations/Database" found')
|
||||
|
||||
assert all_paths.contains('Tutorials/Operations/Database/Optimization'), 'Missing "Tutorials/Operations/Database/Optimization" category'
|
||||
|
||||
console.print_green('✓ Level 4: "Tutorials/Operations/Database/Optimization" found')
|
||||
|
||||
console.lf()
|
||||
|
||||
// ========================================================
|
||||
// TEST 2: Validate Pages Distribution
|
||||
// ========================================================
|
||||
console.print_header('TEST 2: Validate Pages Distribution')
|
||||
console.print_item('Total pages: ${nav_site.pages.len}')
|
||||
|
||||
mut pages_by_category := map[int]int{}
|
||||
for page in nav_site.pages {
|
||||
cat_id := page.category_id
|
||||
if cat_id !in pages_by_category {
|
||||
pages_by_category[cat_id] = 0
|
||||
}
|
||||
pages_by_category[cat_id]++
|
||||
}
|
||||
|
||||
console.print_debug('Pages per category:')
|
||||
for cat_id, count in pages_by_category {
|
||||
mut cat_name := 'Root (Uncategorized)'
|
||||
// category_id is 1-based, index is 0-based
|
||||
if cat_id > 0 && cat_id <= nav_site.categories.len {
|
||||
cat_name = nav_site.categories[cat_id - 1].path
|
||||
}
|
||||
console.print_debug(' Category ${cat_id} [${cat_name}]: ${count} pages')
|
||||
}
|
||||
|
||||
// Validate we have pages in multiple categories
|
||||
assert pages_by_category.len >= 5, 'Should have pages in at least 5 categories'
|
||||
console.print_green('✓ Pages distributed across multiple category levels')
|
||||
|
||||
console.lf()
|
||||
|
||||
// ========================================================
|
||||
// TEST 3: Validate Navigation Structure (Sidebar)
|
||||
// ========================================================
|
||||
console.print_header('TEST 3: Navigation Structure Analysis')
|
||||
|
||||
mut sidebar := nav_site.sidebar()!
|
||||
console.print_item('Sidebar root items: ${sidebar.my_sidebar.len}')
|
||||
console.lf()
|
||||
|
||||
// Analyze structure
|
||||
mut stats := analyze_sidebar_structure(sidebar.my_sidebar)
|
||||
console.print_debug('Structure Analysis:')
|
||||
console.print_debug(' Total root items: ${stats.root_items}')
|
||||
console.print_debug(' Categories: ${stats.categories}')
|
||||
console.print_debug(' Pages: ${stats.pages}')
|
||||
console.print_debug(' Links: ${stats.links}')
|
||||
console.print_debug(' Max nesting depth: ${stats.max_depth}')
|
||||
|
||||
println(nav_site.sidebar_str())
|
||||
println(sidebar)
|
||||
|
||||
assert stats.categories >= 6, 'Should have at least 6 categories'
|
||||
console.print_green('✓ Multiple category levels present')
|
||||
|
||||
assert stats.max_depth >= 4, 'Should have nesting depth of at least 4 levels (0-indexed root, so 3+1)'
|
||||
console.print_green('✓ Deep nesting verified (depth: ${stats.max_depth})')
|
||||
|
||||
console.lf()
|
||||
|
||||
// ========================================================
|
||||
// TEST 4: Validate Specific Path Hierarchies
|
||||
// ========================================================
|
||||
console.print_header('TEST 4: Path Hierarchy Validation')
|
||||
|
||||
// Find categories and check parent-child relationships
|
||||
let_test_hierarchy(nav_site.categories)
|
||||
|
||||
console.lf()
|
||||
|
||||
// ========================================================
|
||||
// TEST 5: Print Sidebar Structure
|
||||
// ========================================================
|
||||
console.print_header('📑 COMPLETE SIDEBAR STRUCTURE')
|
||||
console.lf()
|
||||
println(nav_site.sidebar_str())
|
||||
|
||||
console.print_header('✅ All Navigation Depth Tests Passed!')
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Helper Structures
|
||||
// ============================================================
|
||||
|
||||
struct SidebarStats {
|
||||
pub mut:
|
||||
root_items int
|
||||
categories int
|
||||
pages int
|
||||
links int
|
||||
max_depth int // Max nesting depth including root as depth 1
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Helper Functions
|
||||
// ============================================================
|
||||
|
||||
fn calculate_category_depth(path string) int {
|
||||
if path.len == 0 {
|
||||
return 0 // Or handle as an error/special case
|
||||
}
|
||||
// Count slashes + 1 for the depth
|
||||
// "Why" -> 1
|
||||
// "Why/FAQ" -> 2
|
||||
return path.split('/').len
|
||||
}
|
||||
|
||||
fn analyze_sidebar_structure(items []NavItem) SidebarStats {
|
||||
mut stats := SidebarStats{}
|
||||
|
||||
stats.root_items = items.len
|
||||
|
||||
for item in items {
|
||||
// Calculate depth for the current item and update max_depth
|
||||
// The calculate_nav_item_depth function correctly handles recursion for NavCat
|
||||
// and returns current_depth for leaf nodes (NavDoc, NavLink).
|
||||
// We start at depth 1 for root-level items.
|
||||
depth := calculate_nav_item_depth(item, 1)
|
||||
if depth > stats.max_depth {
|
||||
stats.max_depth = depth
|
||||
}
|
||||
|
||||
// Now categorize and count based on item type
|
||||
if item is NavCat {
|
||||
stats.categories++
|
||||
// Recursively count pages and categories within this NavCat
|
||||
stats.pages += count_nested_pages_in_navcat(item)
|
||||
stats.categories += count_nested_categories_in_navcat(item)
|
||||
} else if item is NavDoc {
|
||||
stats.pages++
|
||||
} else if item is NavLink {
|
||||
stats.links++
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
fn calculate_nav_item_depth(item NavItem, current_depth int) int {
|
||||
mut max_depth_in_branch := current_depth
|
||||
|
||||
if item is NavCat {
|
||||
for sub_item in item.items {
|
||||
depth := calculate_nav_item_depth(sub_item, current_depth + 1)
|
||||
if depth > max_depth_in_branch {
|
||||
max_depth_in_branch = depth
|
||||
}
|
||||
}
|
||||
}
|
||||
// NavDoc and NavLink are leaf nodes, their depth is current_depth
|
||||
return max_depth_in_branch
|
||||
}
|
||||
|
||||
fn count_nested_pages_in_navcat(cat NavCat) int {
|
||||
mut count := 0
|
||||
for item in cat.items {
|
||||
if item is NavDoc {
|
||||
count++
|
||||
} else if item is NavCat {
|
||||
count += count_nested_pages_in_navcat(item)
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
fn count_nested_categories_in_navcat(cat NavCat) int {
|
||||
mut count := 0
|
||||
for item in cat.items {
|
||||
if item is NavCat {
|
||||
count++
|
||||
count += count_nested_categories_in_navcat(item)
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
fn let_test_hierarchy(categories []Category) {
|
||||
console.print_item('Validating path hierarchies:')
|
||||
|
||||
// Group by depth
|
||||
mut by_depth := map[int][]string{}
|
||||
for category in categories {
|
||||
depth := calculate_category_depth(category.path)
|
||||
if depth !in by_depth {
|
||||
by_depth[depth] = []string{}
|
||||
}
|
||||
by_depth[depth] << category.path
|
||||
}
|
||||
|
||||
// Print organized by depth
|
||||
// Assuming max depth is 4 based on the script
|
||||
for depth := 1; depth <= 4; depth++ {
|
||||
if depth in by_depth {
|
||||
console.print_debug(' Depth ${depth}:')
|
||||
for path in by_depth[depth] {
|
||||
console.print_debug(' └─ ${path}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate specific hierarchies
|
||||
mut all_paths := categories.map(it.path)
|
||||
|
||||
// Hierarchy: Why -> Why/FAQ
|
||||
if all_paths.contains('Why') && all_paths.contains('Why/FAQ') {
|
||||
console.print_green('✓ Hierarchy verified: Why → Why/FAQ')
|
||||
}
|
||||
|
||||
// Hierarchy: Tutorials -> Tutorials/Operations -> Tutorials/Operations/Urgent
|
||||
if all_paths.contains('Tutorials') && all_paths.contains('Tutorials/Operations')
|
||||
&& all_paths.contains('Tutorials/Operations/Urgent') {
|
||||
console.print_green('✓ Hierarchy verified: Tutorials → Operations → Urgent')
|
||||
}
|
||||
|
||||
// Hierarchy: Tutorials/Operations -> Tutorials/Operations/Database -> Tutorials/Operations/Database/Optimization
|
||||
if all_paths.contains('Tutorials/Operations/Database')
|
||||
&& all_paths.contains('Tutorials/Operations/Database/Optimization') {
|
||||
console.print_green('✓ Hierarchy verified: Operations → Database → Optimization')
|
||||
}
|
||||
}
|
||||
500
lib/web/doctree/meta2/siteplay_test.v
Normal file
500
lib/web/doctree/meta2/siteplay_test.v
Normal file
@@ -0,0 +1,500 @@
|
||||
module meta
|
||||
|
||||
import incubaid.herolib.core.playbook
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
// 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
|
||||
path: "Getting Started"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "guides:introduction"
|
||||
label: "Introduction to Test Docs"
|
||||
title: "Introduction to Test Docs"
|
||||
description: "Learn what this project is about"
|
||||
|
||||
!!site.page src: "installation"
|
||||
label: "Installation Guide"
|
||||
title: "Installation Guide"
|
||||
description: "How to install and setup"
|
||||
|
||||
!!site.page src: "quick_start"
|
||||
label: "Quick Start"
|
||||
title: "Quick Start"
|
||||
description: "5 minute quick start guide"
|
||||
|
||||
!!site.page_category
|
||||
path: "Core Concepts"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "concepts:architecture"
|
||||
label: "Architecture Overview"
|
||||
title: "Architecture Overview"
|
||||
description: "Understanding the system architecture"
|
||||
|
||||
!!site.page src: "components"
|
||||
label: "Key Components"
|
||||
title: "Key Components"
|
||||
description: "Learn about the main components"
|
||||
|
||||
!!site.page src: "workflow"
|
||||
label: "Typical Workflow"
|
||||
title: "Typical Workflow"
|
||||
description: "How to use the system"
|
||||
|
||||
!!site.page_category
|
||||
path: "API Reference"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "api:rest"
|
||||
label: "REST API"
|
||||
title: "REST API"
|
||||
description: "Complete REST API reference"
|
||||
|
||||
!!site.page src: "graphql"
|
||||
label: "GraphQL API"
|
||||
title: "GraphQL API"
|
||||
description: "GraphQL API documentation"
|
||||
|
||||
!!site.page src: "webhooks"
|
||||
label: "Webhooks"
|
||||
title: "Webhooks"
|
||||
description: "Webhook configuration and examples"
|
||||
|
||||
!!site.page_category
|
||||
path: "Advanced Topics"
|
||||
collapsible: true
|
||||
collapsed: false
|
||||
|
||||
!!site.page src: "advanced:performance"
|
||||
label: "Performance Optimization"
|
||||
title: "Performance Optimization"
|
||||
description: "Tips for optimal performance"
|
||||
|
||||
!!site.page src: "scaling"
|
||||
label: "Scaling Guide"
|
||||
title: "Scaling Guide"
|
||||
|
||||
!!site.page src: "security"
|
||||
label: "Security Best Practices"
|
||||
title: "Security Best Practices"
|
||||
description: "Security considerations and best practices"
|
||||
|
||||
!!site.page src: "troubleshooting"
|
||||
label: "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 - Part 1')
|
||||
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 := 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.config
|
||||
|
||||
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 test_site.build_dest.len == 1, 'Should have 1 production build destination'
|
||||
console.print_green('✓ Production build dest: ${test_site.build_dest[0].path}')
|
||||
|
||||
assert test_site.build_dest_dev.len == 1, 'Should have 1 dev build destination'
|
||||
console.print_green('✓ Dev build dest: ${test_site.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')
|
||||
assert test_site.announcements.len == 1, 'Should have 1 announcement, got ${test_site.announcements.len}'
|
||||
console.print_green('✓ Announcement bar present')
|
||||
|
||||
mut announcement := test_site.announcements[0]
|
||||
|
||||
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 - Part 2')
|
||||
console.lf()
|
||||
|
||||
reset()
|
||||
|
||||
mut plbook := playbook.new(text: test_heroscript)!
|
||||
play(mut plbook)!
|
||||
mut test_site := get(name: 'test_docs')!
|
||||
|
||||
// ========================================================
|
||||
// TEST 8: Validate Pages
|
||||
// ========================================================
|
||||
console.print_header('Validating Pages')
|
||||
|
||||
println(test_site)
|
||||
|
||||
assert test_site.pages.len == 13, 'Should have 13 pages, got ${test_site.pages.len}'
|
||||
console.print_green('✓ Total pages: ${test_site.pages.len}')
|
||||
|
||||
// List and validate pages
|
||||
for i, page in test_site.pages {
|
||||
console.print_debug(' Page ${i}: "${page.src}" - "${page.label}"')
|
||||
}
|
||||
|
||||
// Validate specific pages exist by src
|
||||
mut src_exists := false
|
||||
for page in test_site.pages {
|
||||
if page.src == 'guides:introduction' {
|
||||
src_exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert src_exists, 'guides:introduction page not found'
|
||||
console.print_green('✓ Found guides:introduction')
|
||||
|
||||
src_exists = false
|
||||
for page in test_site.pages {
|
||||
if page.src == 'concepts:architecture' {
|
||||
src_exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert src_exists, 'concepts:architecture page not found'
|
||||
console.print_green('✓ Found concepts:architecture')
|
||||
|
||||
src_exists = false
|
||||
for page in test_site.pages {
|
||||
if page.src == 'api:rest' {
|
||||
src_exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert src_exists, 'api:rest page not found'
|
||||
console.print_green('✓ Found api:rest')
|
||||
|
||||
console.lf()
|
||||
|
||||
// ========================================================
|
||||
// TEST 9: Validate Categories
|
||||
// ========================================================
|
||||
console.print_header('Validating Categories')
|
||||
|
||||
assert test_site.categories.len == 4, 'Should have 4 categories, got ${test_site.categories.len}'
|
||||
console.print_green('✓ Total categories: ${test_site.categories.len}')
|
||||
|
||||
for i, category in test_site.categories {
|
||||
console.print_debug(' Category ${i}: "${category.path}" (collapsible: ${category.collapsible}, collapsed: ${category.collapsed})')
|
||||
}
|
||||
|
||||
// Validate category paths
|
||||
mut category_paths := test_site.categories.map(it.path)
|
||||
assert category_paths.contains('Getting Started'), 'Missing "Getting Started" category'
|
||||
console.print_green('✓ Found "Getting Started" category')
|
||||
|
||||
assert category_paths.contains('Core Concepts'), 'Missing "Core Concepts" category'
|
||||
console.print_green('✓ Found "Core Concepts" category')
|
||||
|
||||
assert category_paths.contains('API Reference'), 'Missing "API Reference" category'
|
||||
console.print_green('✓ Found "API Reference" category')
|
||||
|
||||
assert category_paths.contains('Advanced Topics'), 'Missing "Advanced Topics" category'
|
||||
console.print_green('✓ Found "Advanced Topics" category')
|
||||
|
||||
console.lf()
|
||||
|
||||
// ========================================================
|
||||
// TEST 10: Validate Navigation Structure (Sidebar)
|
||||
// ========================================================
|
||||
console.print_header('Validating Navigation Structure (Sidebar)')
|
||||
|
||||
mut sidebar := test_site.sidebar()!
|
||||
|
||||
console.print_item('Sidebar has ${sidebar.my_sidebar.len} root items')
|
||||
assert sidebar.my_sidebar.len > 0, 'Sidebar should not be empty'
|
||||
console.print_green('✓ Sidebar generated successfully')
|
||||
|
||||
// Count categories in sidebar
|
||||
mut sidebar_category_count := 0
|
||||
mut sidebar_doc_count := 0
|
||||
|
||||
for item in sidebar.my_sidebar {
|
||||
match item {
|
||||
NavCat {
|
||||
sidebar_category_count++
|
||||
}
|
||||
NavDoc {
|
||||
sidebar_doc_count++
|
||||
}
|
||||
else {
|
||||
// Other types
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.print_item('Sidebar contains: ${sidebar_category_count} categories, ${sidebar_doc_count} docs')
|
||||
|
||||
// Detailed sidebar validation
|
||||
for i, item in sidebar.my_sidebar {
|
||||
match item {
|
||||
NavCat {
|
||||
console.print_debug(' Category ${i}: "${item.label}" (${item.items.len} items)')
|
||||
for sub_item in item.items {
|
||||
match sub_item {
|
||||
NavDoc {
|
||||
console.print_debug(' └─ Doc: "${sub_item.label}" (${sub_item.path})')
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
}
|
||||
NavDoc {
|
||||
console.print_debug(' Doc ${i}: "${item.label}" (${item.path})')
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
console.lf()
|
||||
|
||||
// ========================================================
|
||||
// TEST 11: 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()
|
||||
|
||||
// ========================================================
|
||||
// TEST 12: Validate Print Output
|
||||
// ========================================================
|
||||
console.print_header('Site Sidebar String Output')
|
||||
println(test_site.sidebar_str())
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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}"')
|
||||
}
|
||||
49
lib/web/doctree/utils.v
Normal file
49
lib/web/doctree/utils.v
Normal file
@@ -0,0 +1,49 @@
|
||||
module doctree
|
||||
|
||||
import incubaid.herolib.core.texttools
|
||||
|
||||
// returns collection and file name from "collection:file" format
|
||||
// works for file, image, page keys
|
||||
pub fn key_parse(key string) !(string, string) {
|
||||
parts := key.split(':')
|
||||
if parts.len != 2 {
|
||||
return error('Invalid key format. Use "collection:file"')
|
||||
}
|
||||
col := texttools.name_fix(parts[0])
|
||||
file := texttools.name_fix(parts[1])
|
||||
return col, file
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Helper function: normalize name while preserving .md extension handling
|
||||
// ============================================================
|
||||
pub fn name_fix(name string) string {
|
||||
mut result := name
|
||||
// Remove .md extension if present for processing
|
||||
result = result.replace('/', '_')
|
||||
if result.ends_with('.md') {
|
||||
result = result[0..result.len - 3]
|
||||
}
|
||||
// Apply name fixing
|
||||
result = strip_numeric_prefix(result)
|
||||
return texttools.name_fix(result)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -2,100 +2,256 @@
|
||||
|
||||
This module allows you to build and manage Docusaurus websites using a generic configuration layer provided by `lib/web/site`.
|
||||
|
||||
### Workflow
|
||||
|
||||
1. **Configure Your Site**: Define your site's metadata, navigation, footer, pages, and content sources using `!!site.*` actions in a `.heroscript` file. This creates a generic site definition.
|
||||
2. **Define Docusaurus Build**: Use `!!docusaurus.define` to specify build paths and other factory-level settings.
|
||||
3. **Link Site to Docusaurus**: Use `!!docusaurus.add` to link your generic site configuration to the Docusaurus factory. This tells HeroLib to build this specific site using Docusaurus.
|
||||
4. **Run Actions**: Use actions like `!!docusaurus.dev` or `!!docusaurus.build` to generate and serve your site.
|
||||
|
||||
### Hero Command (Recommended)
|
||||
|
||||
For quick setup and development, use the hero command:
|
||||
|
||||
```bash
|
||||
# Start development server
|
||||
hero docs -d -path /path/to/your/site
|
||||
hero docs -d -p /path/to/your/ebook
|
||||
|
||||
# Build for production
|
||||
hero docs -b -path /path/to/your/site
|
||||
hero docs -p /path/to/your/ebook
|
||||
|
||||
# Build and publish
|
||||
hero docs -bp -path /path/to/your/site
|
||||
hero docs -bp -p /path/to/your/ebook
|
||||
```
|
||||
|
||||
### Example HeroScript
|
||||
---
|
||||
|
||||
## Ebook Directory Structure
|
||||
|
||||
The recommended structure for an ebook follows this pattern:
|
||||
|
||||
```
|
||||
my_ebook/
|
||||
├── scan.hero # DocTree collection scanning
|
||||
├── config.hero # Site configuration
|
||||
├── menus.hero # Navbar and footer configuration
|
||||
├── include.hero # Docusaurus define and doctree export
|
||||
├── 1_intro.heroscript # Page definitions (numbered for ordering)
|
||||
├── 2_concepts.heroscript # More page definitions
|
||||
└── 3_advanced.heroscript # Additional pages
|
||||
```
|
||||
|
||||
### File Descriptions
|
||||
|
||||
#### `scan.hero` - Scan Collections
|
||||
|
||||
Defines which collections to scan for content:
|
||||
|
||||
```heroscript
|
||||
// Scan local collections
|
||||
!!doctree.scan path:"../../collections/my_collection"
|
||||
|
||||
// Define the Docusaurus build environment, is optional
|
||||
// Scan remote collections from git
|
||||
!!doctree.scan git_url:"https://git.example.com/org/repo/src/branch/main/collections/docs"
|
||||
```
|
||||
|
||||
#### `config.hero` - Site Configuration
|
||||
|
||||
Core site settings:
|
||||
|
||||
```heroscript
|
||||
!!site.config
|
||||
name:"my_ebook"
|
||||
title:"My Awesome Ebook"
|
||||
tagline:"Documentation made easy"
|
||||
url:"https://docs.example.com"
|
||||
url_home:"docs/"
|
||||
base_url:"/my_ebook/"
|
||||
favicon:"img/favicon.png"
|
||||
copyright:"© 2024 My Organization"
|
||||
default_collection:"my_collection"
|
||||
|
||||
!!site.config_meta
|
||||
description:"Comprehensive documentation for my project"
|
||||
title:"My Ebook - Documentation"
|
||||
keywords:"docs, ebook, tutorial"
|
||||
```
|
||||
|
||||
**Note:** When `url_home` ends with `/` (e.g., `docs/`), the first page in the sidebar automatically becomes the landing page. This means both `/docs/` and `/docs/intro` will work.
|
||||
|
||||
#### `menus.hero` - Navigation Configuration
|
||||
|
||||
```heroscript
|
||||
!!site.navbar
|
||||
title:"My Ebook"
|
||||
|
||||
!!site.navbar_item
|
||||
label:"Documentation"
|
||||
to:"docs/"
|
||||
position:"left"
|
||||
|
||||
!!site.navbar_item
|
||||
label:"GitHub"
|
||||
href:"https://github.com/myorg/myrepo"
|
||||
position:"right"
|
||||
|
||||
!!site.footer
|
||||
style:"dark"
|
||||
|
||||
!!site.footer_item
|
||||
title:"Docs"
|
||||
label:"Getting Started"
|
||||
to:"docs/"
|
||||
|
||||
!!site.footer_item
|
||||
title:"Community"
|
||||
label:"GitHub"
|
||||
href:"https://github.com/myorg/myrepo"
|
||||
```
|
||||
|
||||
#### `include.hero` - Docusaurus Setup
|
||||
|
||||
Links to shared configuration or defines docusaurus directly:
|
||||
|
||||
```heroscript
|
||||
// Option 1: Include shared configuration with variable replacement
|
||||
!!play.include path:'../../heroscriptall' replace:'SITENAME:my_ebook'
|
||||
|
||||
// Option 2: Define directly
|
||||
!!docusaurus.define name:'my_ebook'
|
||||
|
||||
!!doctree.export include:true
|
||||
```
|
||||
|
||||
#### Page Definition Files (`*.heroscript`)
|
||||
|
||||
Define pages and categories:
|
||||
|
||||
```heroscript
|
||||
// Define a category
|
||||
!!site.page_category name:'getting_started' label:"Getting Started"
|
||||
|
||||
// Define pages (first page specifies collection, subsequent pages reuse it)
|
||||
!!site.page src:"my_collection:intro"
|
||||
title:"Introduction"
|
||||
|
||||
!!site.page src:"installation"
|
||||
title:"Installation Guide"
|
||||
|
||||
!!site.page src:"configuration"
|
||||
title:"Configuration"
|
||||
|
||||
// New category
|
||||
!!site.page_category name:'advanced' label:"Advanced Topics"
|
||||
|
||||
!!site.page src:"my_collection:performance"
|
||||
title:"Performance Tuning"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collections
|
||||
|
||||
Collections are directories containing markdown files. They're scanned by DocTree and referenced in page definitions.
|
||||
|
||||
```
|
||||
collections/
|
||||
├── my_collection/
|
||||
│ ├── .collection # Marker file (empty)
|
||||
│ ├── intro.md
|
||||
│ ├── installation.md
|
||||
│ └── configuration.md
|
||||
└── another_collection/
|
||||
├── .collection
|
||||
└── overview.md
|
||||
```
|
||||
|
||||
Pages reference collections using `collection:page` format:
|
||||
|
||||
```heroscript
|
||||
!!site.page src:"my_collection:intro" # Specifies collection
|
||||
!!site.page src:"installation" # Reuses previous collection
|
||||
!!site.page src:"another_collection:overview" # Switches collection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Legacy Configuration
|
||||
|
||||
The older approach using `!!docusaurus.add` is still supported but not recommended:
|
||||
|
||||
```heroscript
|
||||
!!docusaurus.define
|
||||
path_build: "/tmp/docusaurus_build"
|
||||
path_publish: "/tmp/docusaurus_publish"
|
||||
reset: 1
|
||||
install: 1
|
||||
template_update: 1
|
||||
|
||||
!!docusaurus.add
|
||||
sitename:"my_site"
|
||||
path:"./path/to/my/site/source"
|
||||
path_publish: "/tmp/docusaurus_publish" //optional
|
||||
git_url:"https://git.threefold.info/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech" //optional: can use git to pull the site source
|
||||
git_root:"/tmp/code" //optional: where to clone git repo
|
||||
git_reset:1 //optional: reset git repo
|
||||
git_pull:1 //optional: pull latest changes
|
||||
play:true //required when using git_url: process heroscript files from source path
|
||||
|
||||
|
||||
// Run the development server
|
||||
!!docusaurus.dev site:"my_site" open:true watch_changes:true
|
||||
path:"./path/to/site"
|
||||
|
||||
!!docusaurus.dev site:"my_site" open:true
|
||||
```
|
||||
|
||||
## see sites to define a site
|
||||
---
|
||||
|
||||
the site needs to be defined following the generic site definition, see the `lib/web/site` module for more details.
|
||||
## HeroScript Actions Reference
|
||||
|
||||
```heroscript
|
||||
### `!!doctree.scan`
|
||||
|
||||
//Configure the site using the generic 'site' module
|
||||
!!site.config
|
||||
name: "my_site"
|
||||
title: "My Awesome Docs"
|
||||
tagline: "The best docs ever"
|
||||
url: "https://docs.example.com"
|
||||
base_url: "/"
|
||||
copyright: "Example Corp"
|
||||
Scans a directory for markdown collections:
|
||||
|
||||
!!site.menu_item
|
||||
label: "Homepage"
|
||||
href: "https://example.com"
|
||||
position: "right"
|
||||
- `path` (string): Local path to scan
|
||||
- `git_url` (string): Git URL to clone and scan
|
||||
- `name` (string): DocTree instance name (default: `main`)
|
||||
- `ignore` (list): Directory names to skip
|
||||
|
||||
// ... add footer, pages, etc. using !!site.* actions ...
|
||||
### `!!doctree.export`
|
||||
|
||||
```
|
||||
Exports scanned collections:
|
||||
|
||||
### Heroscript Actions
|
||||
- `include` (bool): Include content in export (default: `true`)
|
||||
- `destination` (string): Export directory
|
||||
|
||||
- `!!docusaurus.define`: Configures a Docusaurus factory instance.
|
||||
- `name` (string): Name of the factory (default: `default`).
|
||||
- `path_build` (string): Path to build the site.
|
||||
- `path_publish` (string): Path to publish the final build.
|
||||
- `reset` (bool): If `true`, clean the build directory before starting.
|
||||
- `template_update` (bool): If `true`, update the Docusaurus template.
|
||||
- `install` (bool): If `true`, run `bun install`.
|
||||
### `!!docusaurus.define`
|
||||
|
||||
- `!!docusaurus.add`: Links a configured site to the Docusaurus factory.
|
||||
- `site` (string, required): The name of the site defined in `!!site.config`.
|
||||
- `path` (string, required): The local filesystem path to the site's source directory (e.g., for `static/` folder).
|
||||
Configures the Docusaurus build environment:
|
||||
|
||||
- `!!docusaurus.dev`: Runs the Docusaurus development server.
|
||||
- `site` (string, required): The name of the site to run.
|
||||
- `host` (string): Host to bind to (default: `localhost`).
|
||||
- `port` (int): Port to use (default: `3000`).
|
||||
- `open` (bool): Open the site in a browser.
|
||||
- `watch_changes` (bool): Watch for source file changes and auto-reload.
|
||||
- `name` (string, required): Site name (must match `!!site.config` name)
|
||||
- `path_build` (string): Build directory path
|
||||
- `path_publish` (string): Publish directory path
|
||||
- `reset` (bool): Clean build directory before starting
|
||||
- `template_update` (bool): Update Docusaurus template
|
||||
- `install` (bool): Run `bun install`
|
||||
- `doctree_dir` (string): DocTree export directory
|
||||
|
||||
- `!!docusaurus.build`: Builds the static site for production.
|
||||
- `site` (string, required): The name of the site to build.
|
||||
### `!!site.config`
|
||||
|
||||
Core site configuration:
|
||||
|
||||
- `name` (string, required): Unique site identifier
|
||||
- `title` (string): Site title
|
||||
- `tagline` (string): Site tagline
|
||||
- `url` (string): Full site URL
|
||||
- `base_url` (string): Base URL path (e.g., `/my_ebook/`)
|
||||
- `url_home` (string): Home page path (e.g., `docs/`)
|
||||
- `default_collection` (string): Default collection for pages
|
||||
- `favicon` (string): Favicon path
|
||||
- `copyright` (string): Copyright notice
|
||||
|
||||
### `!!site.page`
|
||||
|
||||
Defines a documentation page:
|
||||
|
||||
- `src` (string, required): Source as `collection:page` or just `page` (reuses previous collection)
|
||||
- `title` (string): Page title
|
||||
- `description` (string): Page description
|
||||
- `draft` (bool): Hide from navigation
|
||||
- `hide_title` (bool): Don't show title on page
|
||||
|
||||
### `!!site.page_category`
|
||||
|
||||
Defines a sidebar category:
|
||||
|
||||
- `name` (string, required): Category identifier
|
||||
- `label` (string): Display label
|
||||
- `position` (int): Sort order
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- `lib/web/site` - Generic site configuration module
|
||||
- `lib/data/doctree` - DocTree collection management
|
||||
|
||||
@@ -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
|
||||
doctree_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
|
||||
doctree_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.doctree_dir == '' {
|
||||
return error('doctree_dir is not set')
|
||||
}
|
||||
if args.path_build == '' {
|
||||
args.path_build = '${os.home_dir()}/hero/var/docusaurus/build'
|
||||
@@ -62,8 +58,7 @@ 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
|
||||
doctree_dir: args.doctree_dir
|
||||
}
|
||||
if c.install {
|
||||
install(c)!
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module docusaurus
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.web.site
|
||||
import incubaid.herolib.web.doctree.meta
|
||||
import incubaid.herolib.osal.core as osal
|
||||
import incubaid.herolib.ui.console
|
||||
|
||||
@@ -15,7 +15,7 @@ pub mut:
|
||||
path_build pathlib.Path
|
||||
errors []SiteError
|
||||
config Configuration
|
||||
website site.Site
|
||||
website meta.Site
|
||||
generated bool
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn (mut s DocSite) build_publish() ! {
|
||||
'
|
||||
retry: 0
|
||||
)!
|
||||
for item in s.website.siteconfig.build_dest {
|
||||
for item in s.build_dest {
|
||||
if item.path.trim_space().trim('/ ') == '' {
|
||||
$if debug {
|
||||
print_backtrace()
|
||||
@@ -71,9 +71,9 @@ pub struct DevArgs {
|
||||
pub mut:
|
||||
host string = 'localhost'
|
||||
port int = 3000
|
||||
open bool = true // whether to open the browser automatically
|
||||
watch_changes bool = false // whether to watch for changes in docs and rebuild automatically
|
||||
skip_generate bool = false // whether to skip generation (useful when docs are pre-generated, e.g., from atlas)
|
||||
open bool = true // whether to open the browser automatically
|
||||
watch_changes bool // whether to watch for changes in docs and rebuild automatically
|
||||
skip_generate bool // whether to skip generation (useful when docs are pre-generated, e.g., from doctree)
|
||||
}
|
||||
|
||||
pub fn (mut s DocSite) open(args DevArgs) ! {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
module docusaurus
|
||||
|
||||
import incubaid.herolib.web.site
|
||||
import incubaid.herolib.web.doctree.meta
|
||||
|
||||
// IS THE ONE AS USED BY DOCUSAURUS
|
||||
|
||||
pub struct Configuration {
|
||||
pub mut:
|
||||
main Main
|
||||
navbar Navbar
|
||||
footer Footer
|
||||
announcement AnnouncementBar
|
||||
main Main
|
||||
navbar Navbar
|
||||
footer Footer
|
||||
sidebar_json_txt string // will hold the sidebar.json content
|
||||
announcement AnnouncementBar
|
||||
}
|
||||
|
||||
pub struct Main {
|
||||
@@ -78,18 +79,17 @@ 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']
|
||||
is_closeable bool @[json: 'isCloseable']
|
||||
}
|
||||
|
||||
// ... (struct definitions remain the same) ...
|
||||
|
||||
// This function is now a pure transformer: site.SiteConfig -> docusaurus.Configuration
|
||||
fn new_configuration(site_cfg site.SiteConfig) !Configuration {
|
||||
// This function is a pure transformer: site.SiteConfig -> docusaurus.Configuration
|
||||
fn new_configuration(mysite meta.Site) !Configuration {
|
||||
// Transform site.SiteConfig to docusaurus.Configuration
|
||||
mut site_cfg := mysite.config
|
||||
mut nav_items := []NavbarItem{}
|
||||
for item in site_cfg.menu.items {
|
||||
nav_items << NavbarItem{
|
||||
@@ -116,8 +116,10 @@ fn new_configuration(site_cfg site.SiteConfig) !Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
sidebar_json_txt := mysite.nav.sidebar_to_json()!
|
||||
|
||||
cfg := Configuration{
|
||||
main: Main{
|
||||
main: Main{
|
||||
title: site_cfg.title
|
||||
tagline: site_cfg.tagline
|
||||
favicon: site_cfg.favicon
|
||||
@@ -147,7 +149,7 @@ fn new_configuration(site_cfg site.SiteConfig) !Configuration {
|
||||
copyright: site_cfg.copyright
|
||||
name: site_cfg.name
|
||||
}
|
||||
navbar: Navbar{
|
||||
navbar: Navbar{
|
||||
title: site_cfg.menu.title
|
||||
logo: Logo{
|
||||
alt: site_cfg.menu.logo_alt
|
||||
@@ -156,18 +158,20 @@ fn new_configuration(site_cfg site.SiteConfig) !Configuration {
|
||||
}
|
||||
items: nav_items
|
||||
}
|
||||
footer: Footer{
|
||||
footer: Footer{
|
||||
style: site_cfg.footer.style
|
||||
links: footer_links
|
||||
}
|
||||
announcement: AnnouncementBar{
|
||||
id: site_cfg.announcement.id
|
||||
announcement: AnnouncementBar{
|
||||
// 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)!
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ 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))!
|
||||
|
||||
// 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.generate_docs()!
|
||||
|
||||
docsite.import()!
|
||||
|
||||
@@ -1,438 +1,163 @@
|
||||
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.web.doctree.client as doctree_client
|
||||
import incubaid.herolib.data.markdown.tools as markdowntools
|
||||
import incubaid.herolib.ui.console
|
||||
import incubaid.herolib.web.site
|
||||
import os
|
||||
|
||||
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
|
||||
// ============================================================================
|
||||
// Doc Linking - Generate Docusaurus docs from DocTree collections
|
||||
// ============================================================================
|
||||
|
||||
// get_first_doc_from_sidebar recursively finds the first doc ID in the sidebar.
|
||||
// Used to determine which page should get slug: / in frontmatter when url_home ends with "/".
|
||||
fn get_first_doc_from_sidebar(items []site.NavItem) string {
|
||||
for item in items {
|
||||
match item {
|
||||
site.NavDoc {
|
||||
return site.extract_page_id(item.id)
|
||||
}
|
||||
site.NavCat {
|
||||
// Recursively search in category items
|
||||
doc := get_first_doc_from_sidebar(item.items)
|
||||
if doc.len > 0 {
|
||||
return doc
|
||||
}
|
||||
}
|
||||
site.NavLink {
|
||||
// Skip links, we want docs
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// Generate docs from site configuration
|
||||
// generate_docs generates markdown files from site page definitions.
|
||||
// Pages are fetched from DocTree collections and written with frontmatter.
|
||||
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)
|
||||
reset_docs_dir(docs_path)!
|
||||
console.print_header('Write doc: ${docs_path}')
|
||||
|
||||
mut gen := SiteGenerator{
|
||||
path: pathlib.get_dir(path: docs_path, create: true)!
|
||||
client: client
|
||||
flat: true
|
||||
site: docsite.website
|
||||
}
|
||||
mut client := doctree_client.new(export_dir: c.doctree_dir)!
|
||||
mut errors := []string{}
|
||||
|
||||
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_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}"
|
||||
}
|
||||
}'
|
||||
// Determine if we need to set a docs landing page (when url_home ends with "/")
|
||||
first_doc_page := if docsite.website.siteconfig.url_home.ends_with('/') {
|
||||
get_first_doc_from_sidebar(docsite.website.nav.my_sidebar)
|
||||
} 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)!
|
||||
for _, page in docsite.website.pages {
|
||||
process_page(mut client, docs_path, page, first_doc_page, mut errors)
|
||||
}
|
||||
|
||||
catfile.write(c)!
|
||||
if errors.len > 0 {
|
||||
report_errors(mut client, errors)!
|
||||
}
|
||||
|
||||
console.print_green('Successfully linked ${docsite.website.pages.len} pages to docs folder')
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
fn reset_docs_dir(docs_path string) ! {
|
||||
if os.exists(docs_path) {
|
||||
os.rmdir_all(docs_path) or {}
|
||||
}
|
||||
return name
|
||||
os.mkdir_all(docs_path)!
|
||||
}
|
||||
|
||||
// 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('/')
|
||||
fn report_errors(mut client doctree_client.DocTreeClient, errors []string) ! {
|
||||
available := client.list_markdown() or { 'Could not list available pages' }
|
||||
console.print_stderr('Available pages:\n${available}')
|
||||
return error('Errors during doc generation:\n${errors.join('\n\n')}')
|
||||
}
|
||||
|
||||
// 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
|
||||
// ============================================================================
|
||||
// Page Processing
|
||||
// ============================================================================
|
||||
|
||||
// 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 = ''
|
||||
fn process_page(mut client doctree_client.DocTreeClient, docs_path string, page site.Page, first_doc_page string, mut errors []string) {
|
||||
collection, page_name := parse_page_src(page.src) or {
|
||||
errors << err.msg()
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
content := client.get_page_content(collection, page_name) or {
|
||||
errors << "Page not found: '${collection}:${page_name}'"
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// Check if this page is the docs landing page
|
||||
is_landing_page := first_doc_page.len > 0 && page_name == first_doc_page
|
||||
|
||||
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}/')
|
||||
write_page(docs_path, page_name, page, content, is_landing_page) or {
|
||||
errors << "Failed to write page '${page_name}': ${err.msg()}"
|
||||
return
|
||||
}
|
||||
|
||||
// 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  to  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
|
||||
copy_page_assets(mut client, docs_path, collection, page_name)
|
||||
console.print_item('Generated: ${page_name}.md')
|
||||
}
|
||||
|
||||
fn parse_page_src(src string) !(string, string) {
|
||||
parts := src.split(':')
|
||||
if parts.len != 2 {
|
||||
return error("Invalid src format '${src}' - expected 'collection:page_name'")
|
||||
}
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
|
||||
fn write_page(docs_path string, page_name string, page site.Page, content string, is_landing_page bool) ! {
|
||||
frontmatter := build_frontmatter(page, content, is_landing_page)
|
||||
final_content := frontmatter + '\n\n' + content
|
||||
|
||||
output_path := '${docs_path}/${page_name}.md'
|
||||
mut file := pathlib.get_file(path: output_path, create: true)!
|
||||
file.write(final_content)!
|
||||
}
|
||||
|
||||
fn copy_page_assets(mut client doctree_client.DocTreeClient, docs_path string, collection string, page_name string) {
|
||||
client.copy_images(collection, page_name, docs_path) or {}
|
||||
client.copy_files(collection, page_name, docs_path) or {}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Frontmatter Generation
|
||||
// ============================================================================
|
||||
|
||||
fn build_frontmatter(page site.Page, content string, is_landing_page bool) string {
|
||||
title := get_title(page, content)
|
||||
|
||||
description := get_description(page, title)
|
||||
|
||||
mut lines := ['---']
|
||||
lines << "title: '${title}'"
|
||||
lines << "description: '${description}'"
|
||||
|
||||
// if page.id.contains('tfhowto_tools'){
|
||||
// println('extracted title: ${title}')
|
||||
// println('page.src: ${lines}')
|
||||
// $dbg;
|
||||
// }
|
||||
|
||||
// Add slug: / for the docs landing page so /docs/ works directly
|
||||
if is_landing_page {
|
||||
lines << 'slug: /'
|
||||
}
|
||||
|
||||
if page.draft {
|
||||
lines << 'draft: true'
|
||||
}
|
||||
if page.hide_title {
|
||||
lines << 'hide_title: true'
|
||||
}
|
||||
|
||||
lines << '---'
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
442
lib/web/docusaurus/dsite_generate_docs__.v
Normal file
442
lib/web/docusaurus/dsite_generate_docs__.v
Normal file
@@ -0,0 +1,442 @@
|
||||
module docusaurus
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
// import incubaid.herolib.web.doctree.client as doctree_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 := doctree_client.new(export_dir: c.doctree_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 doctree self-contained exports)
|
||||
// // DocTree 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  to  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
|
||||
// }
|
||||
@@ -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)!
|
||||
// }
|
||||
// }
|
||||
89
lib/web/docusaurus/dsite_to_sidebar_json.v
Normal file
89
lib/web/docusaurus/dsite_to_sidebar_json.v
Normal file
@@ -0,0 +1,89 @@
|
||||
module doc
|
||||
|
||||
import incubaid.herolib.web.doctree.meta as site
|
||||
import json
|
||||
|
||||
// this is the logic to create docusaurus sidebar.json from site.NavItems
|
||||
|
||||
struct Sidebar {
|
||||
pub mut:
|
||||
items []NavItem
|
||||
}
|
||||
|
||||
type NavItem = NavDoc | NavCat | NavLink
|
||||
|
||||
struct SidebarItem {
|
||||
typ string @[json: 'type']
|
||||
id string @[omitempty]
|
||||
label string
|
||||
href string @[omitempty]
|
||||
description string @[omitempty]
|
||||
collapsible bool @[json: 'collapsible'; omitempty]
|
||||
collapsed bool @[json: 'collapsed'; omitempty]
|
||||
items []SidebarItem @[omitempty]
|
||||
}
|
||||
|
||||
pub struct NavDoc {
|
||||
pub mut:
|
||||
id string
|
||||
label string
|
||||
}
|
||||
|
||||
pub struct NavCat {
|
||||
pub mut:
|
||||
label string
|
||||
collapsible bool = true
|
||||
collapsed bool
|
||||
items []NavItem
|
||||
}
|
||||
|
||||
pub struct NavLink {
|
||||
pub mut:
|
||||
label string
|
||||
href string
|
||||
description string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JSON Serialization
|
||||
// ============================================================================
|
||||
|
||||
pub fn sidebar_to_json(sb site.SideBar) !string {
|
||||
items := sb.my_sidebar.map(to_sidebar_item(it))
|
||||
return json.encode_pretty(items)
|
||||
}
|
||||
|
||||
fn to_sidebar_item(item site.NavItem) SidebarItem {
|
||||
return match item {
|
||||
NavDoc { from_doc(item) }
|
||||
NavLink { from_link(item) }
|
||||
NavCat { from_category(item) }
|
||||
}
|
||||
}
|
||||
|
||||
fn from_doc(doc site.NavDoc) SidebarItem {
|
||||
return SidebarItem{
|
||||
typ: 'doc'
|
||||
id: doc.id
|
||||
label: doc.label
|
||||
}
|
||||
}
|
||||
|
||||
fn from_link(link site.NavLink) SidebarItem {
|
||||
return SidebarItem{
|
||||
typ: 'link'
|
||||
label: link.label
|
||||
href: link.href
|
||||
description: link.description
|
||||
}
|
||||
}
|
||||
|
||||
fn from_category(cat site.NavCat) SidebarItem {
|
||||
return SidebarItem{
|
||||
typ: 'category'
|
||||
label: cat.label
|
||||
collapsible: cat.collapsible
|
||||
collapsed: cat.collapsed
|
||||
items: cat.items.map(to_sidebar_item(it))
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ pub fn dsite_define(sitename string) ! {
|
||||
console.print_header('Add Docusaurus Site: ${sitename}')
|
||||
mut c := config()!
|
||||
|
||||
path_publish := '${c.path_publish.path}/${sitename}'
|
||||
_ := '${c.path_publish.path}/${sitename}'
|
||||
path_build_ := '${c.path_build.path}'
|
||||
|
||||
// Get the site object after processing, this is the website which is a generic definition of a site
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
82
lib/web/docusaurus/for_testing/README.md
Normal file
82
lib/web/docusaurus/for_testing/README.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Docusaurus Link Resolution Test
|
||||
|
||||
This directory contains a comprehensive test for the herolib documentation linking mechanism.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
for_testing/
|
||||
├── README.md # This file
|
||||
├── collections/
|
||||
│ └── test_collection/ # Markdown source files
|
||||
│ ├── .collection # Collection metadata
|
||||
│ ├── page1.md # Introduction
|
||||
│ ├── page2.md # Basic Concepts
|
||||
│ ├── page3.md # Configuration
|
||||
│ ├── page4.md # Advanced Features
|
||||
│ ├── page5.md # Troubleshooting
|
||||
│ ├── page6.md # Best Practices
|
||||
│ └── page7.md # Conclusion
|
||||
└── ebooks/
|
||||
└── test_site/ # Heroscript configuration
|
||||
├── heroscriptall # Master configuration (entry point)
|
||||
├── config.heroscript # Site configuration
|
||||
├── pages.heroscript # Page definitions
|
||||
└── docusaurus.heroscript # Docusaurus settings
|
||||
```
|
||||
|
||||
## What This Tests
|
||||
|
||||
1. **Link Resolution** - Each page contains links using the `[text](collection:page)` format
|
||||
2. **Navigation Chain** - Pages link sequentially: 1 → 2 → 3 → 4 → 5 → 6 → 7
|
||||
3. **Sidebar Generation** - All 7 pages should appear in the sidebar
|
||||
4. **Category Support** - Pages are organized into categories (root, basics, advanced, reference)
|
||||
|
||||
## Running the Test
|
||||
|
||||
From the herolib root directory:
|
||||
|
||||
```bash
|
||||
# Build herolib first
|
||||
./cli/compile.vsh
|
||||
|
||||
# Run the test site
|
||||
/Users/mahmoud/hero/bin/hero docs -d -p lib/web/docusaurus/for_testing/ebooks/test_site
|
||||
```
|
||||
|
||||
## Expected Results
|
||||
|
||||
When the test runs successfully:
|
||||
|
||||
1. ✅ All 7 pages are generated in `~/hero/var/docusaurus/build/docs/`
|
||||
2. ✅ Sidebar shows all pages organized by category
|
||||
3. ✅ Clicking navigation links works (page1 → page2 → ... → page7)
|
||||
4. ✅ No broken links or 404 errors
|
||||
5. ✅ Back-links also work (e.g., page7 → page1)
|
||||
|
||||
## Link Syntax Being Tested
|
||||
|
||||
```markdown
|
||||
[Next Page](test_collection:page2)
|
||||
```
|
||||
|
||||
This should resolve to a proper Docusaurus link when the site is built.
|
||||
|
||||
## Verification
|
||||
|
||||
After running the test:
|
||||
|
||||
1. Open http://localhost:3000/test/ in your browser
|
||||
2. Click through all navigation links from Page 1 to Page 7
|
||||
3. Verify the back-link on Page 7 returns to Page 1
|
||||
4. Check the sidebar displays all pages correctly
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If links don't resolve:
|
||||
|
||||
1. Check that the collection is registered in the doctree
|
||||
2. Verify page names match (no typos)
|
||||
3. Run with debug flag (`-d`) to see detailed output
|
||||
4. Check `~/hero/var/docusaurus/build/docs/` for generated files
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
name: test_collection
|
||||
description: Test collection for link resolution testing
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Page 1: Introduction
|
||||
|
||||
Welcome to the documentation linking test. This page serves as the entry point for testing herolib's link resolution mechanism.
|
||||
|
||||
## Overview
|
||||
|
||||
This test suite consists of 7 interconnected pages that form a chain. Each page links to the next, demonstrating that the `collection:page_name` link syntax works correctly across multiple layers.
|
||||
|
||||
## What We're Testing
|
||||
|
||||
- Link resolution using `collection:page_name` format
|
||||
- Proper generation of Docusaurus-compatible links
|
||||
- Navigation chain integrity from page 1 through page 7
|
||||
- Sidebar generation with all pages
|
||||
|
||||
## Navigation
|
||||
|
||||
Continue to the next section to learn about the basic concepts.
|
||||
|
||||
**Next:** [Page 2: Basic Concepts](test_collection:page2)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Page 2: Basic Concepts
|
||||
|
||||
This page covers the basic concepts of the documentation system.
|
||||
|
||||
## Link Syntax
|
||||
|
||||
In herolib, links between pages use the format:
|
||||
|
||||
```
|
||||
[Link Text](collection_name:page_name)
|
||||
```
|
||||
|
||||
For example, to link to `page3` in `test_collection`:
|
||||
|
||||
```markdown
|
||||
[Go to Page 3](test_collection:page3)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The parser identifies links with the `collection:page` format
|
||||
2. During site generation, these are resolved to actual file paths
|
||||
3. Docusaurus receives properly formatted relative links
|
||||
|
||||
## Navigation
|
||||
|
||||
**Previous:** [Page 1: Introduction](test_collection:page1)
|
||||
|
||||
**Next:** [Page 3: Configuration](test_collection:page3)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Page 3: Configuration
|
||||
|
||||
This page explains configuration options for the documentation system.
|
||||
|
||||
## Site Configuration
|
||||
|
||||
The site is configured using heroscript files:
|
||||
|
||||
```heroscript
|
||||
!!site.config
|
||||
name:"test_site"
|
||||
title:"Test Documentation"
|
||||
base_url:"/test/"
|
||||
url_home:"docs/page1"
|
||||
```
|
||||
|
||||
## Page Definitions
|
||||
|
||||
Each page is defined using the `!!site.page` action:
|
||||
|
||||
```heroscript
|
||||
!!site.page src:"test_collection:page1"
|
||||
title:"Introduction"
|
||||
```
|
||||
|
||||
## Important Settings
|
||||
|
||||
| Setting | Description |
|
||||
|---------|-------------|
|
||||
| `name` | Unique page identifier |
|
||||
| `collection` | Source collection name |
|
||||
| `title` | Display title in sidebar |
|
||||
|
||||
## Navigation
|
||||
|
||||
**Previous:** [Page 2: Basic Concepts](test_collection:page2)
|
||||
|
||||
**Next:** [Page 4: Advanced Features](test_collection:page4)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Page 4: Advanced Features
|
||||
|
||||
This page covers advanced features of the linking mechanism.
|
||||
|
||||
## Cross-Collection Links
|
||||
|
||||
You can link to pages in different collections:
|
||||
|
||||
```markdown
|
||||
[Link to other collection](other_collection:some_page)
|
||||
```
|
||||
|
||||
## Categories
|
||||
|
||||
Pages can be organized into categories:
|
||||
|
||||
```heroscript
|
||||
!!site.page_category name:'advanced' label:"Advanced Topics"
|
||||
|
||||
!!site.page name:'page4' collection:'test_collection'
|
||||
title:"Advanced Features"
|
||||
```
|
||||
|
||||
## Multiple Link Formats
|
||||
|
||||
The system supports various link formats:
|
||||
|
||||
1. **Collection links:** `[text](collection:page)`
|
||||
2. **Relative links:** `[text](./other_page.md)`
|
||||
3. **External links:** `[text](https://example.com)`
|
||||
|
||||
## Navigation
|
||||
|
||||
**Previous:** [Page 3: Configuration](test_collection:page3)
|
||||
|
||||
**Next:** [Page 5: Troubleshooting](test_collection:page5)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Page 5: Troubleshooting
|
||||
|
||||
This page helps you troubleshoot common issues.
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Broken Links
|
||||
|
||||
If links appear broken, check:
|
||||
|
||||
1. The collection name is correct
|
||||
2. The page name matches the markdown filename (without `.md`)
|
||||
3. The collection is properly registered in the doctree
|
||||
|
||||
### Page Not Found
|
||||
|
||||
Ensure the page is defined in your heroscript:
|
||||
|
||||
```heroscript
|
||||
!!site.page name:'page5' collection:'test_collection'
|
||||
title:"Troubleshooting"
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
- Run with debug flag: `hero docs -d -p .`
|
||||
- Check the generated `sidebar.json`
|
||||
- Verify the docs output in `~/hero/var/docusaurus/build/docs/`
|
||||
|
||||
## Error Messages
|
||||
|
||||
| Error | Solution |
|
||||
| ------------------------ | ---------------------------- |
|
||||
| "Page not found" | Check page name spelling |
|
||||
| "Collection not found" | Verify doctree configuration |
|
||||
| "Link resolution failed" | Check link syntax |
|
||||
|
||||
## Navigation
|
||||
|
||||
**Previous:** [Page 4: Advanced Features](test_collection:page4)
|
||||
|
||||
**Next:** [Page 6: Best Practices](test_collection:page6)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Page 6: Best Practices
|
||||
|
||||
This page outlines best practices for documentation.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- Use lowercase for page names: `page_name.md`
|
||||
- Use underscores for multi-word names: `my_long_page_name.md`
|
||||
- Keep names short but descriptive
|
||||
|
||||
## Link Organization
|
||||
|
||||
### Do This ✓
|
||||
|
||||
```markdown
|
||||
See the [configuration guide](test_collection:page3) for details.
|
||||
```
|
||||
|
||||
### Avoid This ✗
|
||||
|
||||
```markdown
|
||||
Click [here](test_collection:page3) for more.
|
||||
```
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
A well-organized documentation site should:
|
||||
|
||||
1. **Start with an introduction** - Explain what the documentation covers
|
||||
2. **Progress logically** - Each page builds on the previous
|
||||
3. **End with reference material** - API docs, troubleshooting, etc.
|
||||
|
||||
## Content Guidelines
|
||||
|
||||
- Keep paragraphs short
|
||||
- Use code blocks for examples
|
||||
- Include navigation links at the bottom of each page
|
||||
|
||||
## Navigation
|
||||
|
||||
**Previous:** [Page 5: Troubleshooting](test_collection:page5)
|
||||
|
||||
**Next:** [Page 7: Conclusion](test_collection:page7)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Page 7: Conclusion
|
||||
|
||||
Congratulations! You've reached the final page of the documentation linking test.
|
||||
|
||||
## Summary
|
||||
|
||||
This test suite demonstrated:
|
||||
|
||||
- ✅ Link resolution using `collection:page_name` format
|
||||
- ✅ Navigation chain across 7 pages
|
||||
- ✅ Proper sidebar generation
|
||||
- ✅ Docusaurus-compatible output
|
||||
|
||||
## Test Verification
|
||||
|
||||
If you've reached this page by clicking through all the navigation links, the linking mechanism is working correctly!
|
||||
|
||||
### Link Chain Verified
|
||||
|
||||
1. [Page 1: Introduction](test_collection:page1) → Entry point
|
||||
2. [Page 2: Basic Concepts](test_collection:page2) → Link syntax
|
||||
3. [Page 3: Configuration](test_collection:page3) → Site setup
|
||||
4. [Page 4: Advanced Features](test_collection:page4) → Cross-collection links
|
||||
5. [Page 5: Troubleshooting](test_collection:page5) → Common issues
|
||||
6. [Page 6: Best Practices](test_collection:page6) → Guidelines
|
||||
7. **Page 7: Conclusion** → You are here!
|
||||
|
||||
## What's Next
|
||||
|
||||
You can now use the herolib documentation system with confidence that links will resolve correctly across your entire documentation site.
|
||||
|
||||
## Navigation
|
||||
|
||||
**Previous:** [Page 6: Best Practices](test_collection:page6)
|
||||
|
||||
**Back to Start:** [Page 1: Introduction](test_collection:page1)
|
||||
|
||||
16
lib/web/docusaurus/for_testing/ebooks/test_site/config.hero
Normal file
16
lib/web/docusaurus/for_testing/ebooks/test_site/config.hero
Normal file
@@ -0,0 +1,16 @@
|
||||
!!site.config
|
||||
name:"test_site"
|
||||
title:"Link Resolution Test"
|
||||
tagline:"Testing herolib documentation linking mechanism"
|
||||
url:"http://localhost:3000"
|
||||
url_home:"docs/"
|
||||
base_url:"/test/"
|
||||
favicon:"img/favicon.png"
|
||||
copyright:"© 2024 Herolib Test"
|
||||
default_collection:"test_collection"
|
||||
|
||||
!!site.config_meta
|
||||
description:"Test suite for verifying herolib documentation link resolution across multiple pages"
|
||||
title:"Link Resolution Test - Herolib"
|
||||
keywords:"herolib, docusaurus, testing, links, documentation"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
!!docusaurus.define name:'test_site'
|
||||
|
||||
!!doctree.export include:true
|
||||
|
||||
33
lib/web/docusaurus/for_testing/ebooks/test_site/menus.hero
Normal file
33
lib/web/docusaurus/for_testing/ebooks/test_site/menus.hero
Normal file
@@ -0,0 +1,33 @@
|
||||
// Navbar configuration
|
||||
!!site.navbar
|
||||
title:"Link Test"
|
||||
|
||||
!!site.navbar_item
|
||||
label:"Documentation"
|
||||
to:"docs/"
|
||||
position:"left"
|
||||
|
||||
!!site.navbar_item
|
||||
label:"GitHub"
|
||||
href:"https://github.com/incubaid/herolib"
|
||||
position:"right"
|
||||
|
||||
// Footer configuration
|
||||
!!site.footer
|
||||
style:"dark"
|
||||
|
||||
!!site.footer_item
|
||||
title:"Docs"
|
||||
label:"Introduction"
|
||||
to:"docs/"
|
||||
|
||||
!!site.footer_item
|
||||
title:"Docs"
|
||||
label:"Configuration"
|
||||
to:"docs/page3"
|
||||
|
||||
!!site.footer_item
|
||||
title:"Community"
|
||||
label:"GitHub"
|
||||
href:"https://github.com/incubaid/herolib"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Page Definitions for Link Resolution Test
|
||||
// Each page maps to a markdown file in the test_collection
|
||||
|
||||
// Root pages (no category)
|
||||
!!site.page src:"test_collection:page1"
|
||||
title:"Introduction"
|
||||
|
||||
!!site.page src:"page2"
|
||||
title:"Basic Concepts"
|
||||
|
||||
// Basics category
|
||||
!!site.page_category name:'basics' label:"Getting Started"
|
||||
|
||||
!!site.page src:"page3"
|
||||
title:"Configuration"
|
||||
|
||||
!!site.page src:"page4"
|
||||
title:"Advanced Features"
|
||||
|
||||
// Advanced category
|
||||
!!site.page_category name:'advanced' label:"Advanced Topics"
|
||||
|
||||
!!site.page src:"page5"
|
||||
title:"Troubleshooting"
|
||||
|
||||
!!site.page src:"page6"
|
||||
title:"Best Practices"
|
||||
|
||||
// Reference category
|
||||
!!site.page_category name:'reference' label:"Reference"
|
||||
|
||||
!!site.page src:"page7"
|
||||
title:"Conclusion"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
!!doctree.scan path:"../../collections/test_collection"
|
||||
|
||||
@@ -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.')
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
module docusaurus
|
||||
|
||||
import incubaid.herolib.core.playbook { PlayBook }
|
||||
import incubaid.herolib.web.doctree
|
||||
import incubaid.herolib.ui.console
|
||||
import os
|
||||
|
||||
pub fn play(mut plbook PlayBook) ! {
|
||||
@@ -8,62 +10,78 @@ pub fn play(mut plbook PlayBook) ! {
|
||||
return
|
||||
}
|
||||
|
||||
// there should be 1 define section
|
||||
mut action_define := plbook.ensure_once(filter: 'docusaurus.define')!
|
||||
mut param_define := action_define.params
|
||||
|
||||
config_set(
|
||||
path_build: param_define.get_default('path_build', '')!
|
||||
path_publish: param_define.get_default('path_publish', '')!
|
||||
reset: param_define.get_default_false('reset')
|
||||
template_update: param_define.get_default_false('template_update')
|
||||
install: param_define.get_default_false('install')
|
||||
atlas_dir: param_define.get_default('atlas_dir', '${os.home_dir()}/hero/var/atlas_export')!
|
||||
use_atlas: param_define.get_default_false('use_atlas')
|
||||
)!
|
||||
|
||||
site_name := param_define.get('name') or {
|
||||
return error('In docusaurus.define, param "name" is required.')
|
||||
}
|
||||
|
||||
dsite_define(site_name)!
|
||||
|
||||
action_define.done = true
|
||||
mut dsite := dsite_get(site_name)!
|
||||
|
||||
mut dsite := process_define(mut plbook)!
|
||||
dsite.generate()!
|
||||
|
||||
mut actions_build := plbook.find(filter: 'docusaurus.build')!
|
||||
if actions_build.len > 1 {
|
||||
return error('Multiple "docusaurus.build" actions found. Only one is allowed.')
|
||||
}
|
||||
for mut action in actions_build {
|
||||
dsite.build()!
|
||||
action.done = true
|
||||
}
|
||||
|
||||
mut actions_export := plbook.find(filter: 'docusaurus.publish')!
|
||||
if actions_export.len > 1 {
|
||||
return error('Multiple "docusaurus.publish" actions found. Only one is allowed.')
|
||||
}
|
||||
for mut action in actions_export {
|
||||
dsite.build_publish()!
|
||||
action.done = true
|
||||
}
|
||||
|
||||
mut actions_dev := plbook.find(filter: 'docusaurus.dev')!
|
||||
if actions_dev.len > 1 {
|
||||
return error('Multiple "docusaurus.dev" actions found. Only one is allowed.')
|
||||
}
|
||||
for mut action in actions_dev {
|
||||
mut p := action.params
|
||||
dsite.dev(
|
||||
host: p.get_default('host', 'localhost')!
|
||||
port: p.get_int_default('port', 3000)!
|
||||
open: p.get_default_false('open')
|
||||
)!
|
||||
action.done = true
|
||||
}
|
||||
process_build(mut plbook, mut dsite)!
|
||||
process_publish(mut plbook, mut dsite)!
|
||||
process_dev(mut plbook, mut dsite)!
|
||||
|
||||
plbook.ensure_processed(filter: 'docusaurus.')!
|
||||
}
|
||||
|
||||
fn process_define(mut plbook PlayBook) !&DocSite {
|
||||
mut action := plbook.ensure_once(filter: 'docusaurus.define')!
|
||||
p := action.params
|
||||
|
||||
doctree_dir := p.get_default('doctree_dir', '${os.home_dir()}/hero/var/doctree_export')!
|
||||
|
||||
config_set(
|
||||
path_build: p.get_default('path_build', '')!
|
||||
path_publish: p.get_default('path_publish', '')!
|
||||
reset: p.get_default_false('reset')
|
||||
template_update: p.get_default_false('template_update')
|
||||
install: p.get_default_false('install')
|
||||
doctree_dir: doctree_dir
|
||||
)!
|
||||
|
||||
site_name := p.get('name') or { return error('docusaurus.define: "name" is required') }
|
||||
doctree_name := p.get_default('doctree', 'main')!
|
||||
|
||||
export_doctree(doctree_name, doctree_dir)!
|
||||
dsite_define(site_name)!
|
||||
action.done = true
|
||||
|
||||
return dsite_get(site_name)!
|
||||
}
|
||||
|
||||
fn process_build(mut plbook PlayBook, mut dsite DocSite) ! {
|
||||
if !plbook.max_once(filter: 'docusaurus.build')! {
|
||||
return
|
||||
}
|
||||
mut action := plbook.get(filter: 'docusaurus.build')!
|
||||
dsite.build()!
|
||||
action.done = true
|
||||
}
|
||||
|
||||
fn process_publish(mut plbook PlayBook, mut dsite DocSite) ! {
|
||||
if !plbook.max_once(filter: 'docusaurus.publish')! {
|
||||
return
|
||||
}
|
||||
mut action := plbook.get(filter: 'docusaurus.publish')!
|
||||
dsite.build_publish()!
|
||||
action.done = true
|
||||
}
|
||||
|
||||
fn process_dev(mut plbook PlayBook, mut dsite DocSite) ! {
|
||||
if !plbook.max_once(filter: 'docusaurus.dev')! {
|
||||
return
|
||||
}
|
||||
mut action := plbook.get(filter: 'docusaurus.dev')!
|
||||
p := action.params
|
||||
dsite.dev(
|
||||
host: p.get_default('host', 'localhost')!
|
||||
port: p.get_int_default('port', 3000)!
|
||||
open: p.get_default_false('open')
|
||||
)!
|
||||
action.done = true
|
||||
}
|
||||
|
||||
fn export_doctree(name string, dir string) ! {
|
||||
if !doctree.exists(name) {
|
||||
return
|
||||
}
|
||||
console.print_debug('Auto-exporting DocTree "${name}" to ${dir}')
|
||||
mut a := doctree.get(name)!
|
||||
a.export(destination: dir, reset: true, include: true, redis: false)!
|
||||
}
|
||||
|
||||
@@ -1,536 +0,0 @@
|
||||
# AI Instructions for Site Module HeroScript
|
||||
|
||||
This document provides comprehensive instructions for AI agents working with the Site module's HeroScript format.
|
||||
|
||||
## HeroScript Format Overview
|
||||
|
||||
HeroScript is a declarative configuration language with the following characteristics:
|
||||
|
||||
### Basic Syntax
|
||||
|
||||
```heroscript
|
||||
!!actor.action
|
||||
param1: "value1"
|
||||
param2: "value2"
|
||||
multiline_param: "
|
||||
This is a multiline value.
|
||||
It can span multiple lines.
|
||||
"
|
||||
arg1 arg2 // Arguments without keys
|
||||
```
|
||||
|
||||
**Key Rules:**
|
||||
1. Actions start with `!!` followed by `actor.action` format
|
||||
2. Parameters are indented and use `key: "value"` or `key: value` format
|
||||
3. Values with spaces must be quoted
|
||||
4. Multiline values are supported with quotes
|
||||
5. Arguments without keys are space-separated
|
||||
6. Comments start with `//`
|
||||
|
||||
## Site Module Actions
|
||||
|
||||
### 1. Site Configuration (`!!site.config`)
|
||||
|
||||
**Purpose:** Define the main site configuration including title, description, and metadata.
|
||||
|
||||
**Required Parameters:**
|
||||
- `name`: Site identifier (will be normalized to snake_case)
|
||||
|
||||
**Optional Parameters:**
|
||||
- `title`: Site title (default: "Documentation Site")
|
||||
- `description`: Site description
|
||||
- `tagline`: Site tagline
|
||||
- `favicon`: Path to favicon (default: "img/favicon.png")
|
||||
- `image`: Default site image (default: "img/tf_graph.png")
|
||||
- `copyright`: Copyright text
|
||||
- `url`: Main site URL
|
||||
- `base_url`: Base URL path (default: "/")
|
||||
- `url_home`: Home page path
|
||||
|
||||
**Example:**
|
||||
```heroscript
|
||||
!!site.config
|
||||
name: "my_documentation"
|
||||
title: "My Documentation Site"
|
||||
description: "Comprehensive technical documentation"
|
||||
tagline: "Learn everything you need"
|
||||
url: "https://docs.example.com"
|
||||
base_url: "/"
|
||||
```
|
||||
|
||||
**AI Guidelines:**
|
||||
- Always include `name` parameter
|
||||
- Use descriptive titles and descriptions
|
||||
- Ensure URLs are properly formatted with protocol
|
||||
|
||||
### 2. Metadata Configuration (`!!site.config_meta`)
|
||||
|
||||
**Purpose:** Override specific metadata for SEO purposes.
|
||||
|
||||
**Optional Parameters:**
|
||||
- `title`: SEO-specific title (overrides site.config title for meta tags)
|
||||
- `image`: SEO-specific image (overrides site.config image for og:image)
|
||||
- `description`: SEO-specific description
|
||||
|
||||
**Example:**
|
||||
```heroscript
|
||||
!!site.config_meta
|
||||
title: "My Docs - Complete Guide"
|
||||
image: "img/social-preview.png"
|
||||
description: "The ultimate guide to using our platform"
|
||||
```
|
||||
|
||||
**AI Guidelines:**
|
||||
- Use only when SEO metadata needs to differ from main config
|
||||
- Keep titles concise for social media sharing
|
||||
- Use high-quality images for social previews
|
||||
|
||||
### 3. Navigation Bar (`!!site.navbar` or `!!site.menu`)
|
||||
|
||||
**Purpose:** Configure the main navigation bar.
|
||||
|
||||
**Optional Parameters:**
|
||||
- `title`: Navigation title (defaults to site.config title)
|
||||
- `logo_alt`: Logo alt text
|
||||
- `logo_src`: Logo image path
|
||||
- `logo_src_dark`: Dark mode logo path
|
||||
|
||||
**Example:**
|
||||
```heroscript
|
||||
!!site.navbar
|
||||
title: "My Site"
|
||||
logo_alt: "My Site Logo"
|
||||
logo_src: "img/logo.svg"
|
||||
logo_src_dark: "img/logo-dark.svg"
|
||||
```
|
||||
|
||||
**AI Guidelines:**
|
||||
- Use `!!site.navbar` for modern syntax (preferred)
|
||||
- `!!site.menu` is supported for backward compatibility
|
||||
- Provide both light and dark logos when possible
|
||||
|
||||
### 4. Navigation Items (`!!site.navbar_item` or `!!site.menu_item`)
|
||||
|
||||
**Purpose:** Add items to the navigation bar.
|
||||
|
||||
**Required Parameters (one of):**
|
||||
- `to`: Internal link path
|
||||
- `href`: External URL
|
||||
|
||||
**Optional Parameters:**
|
||||
- `label`: Display text (required in practice)
|
||||
- `position`: "left" or "right" (default: "right")
|
||||
|
||||
**Example:**
|
||||
```heroscript
|
||||
!!site.navbar_item
|
||||
label: "Documentation"
|
||||
to: "docs/intro"
|
||||
position: "left"
|
||||
|
||||
!!site.navbar_item
|
||||
label: "GitHub"
|
||||
href: "https://github.com/myorg/repo"
|
||||
position: "right"
|
||||
```
|
||||
|
||||
**AI Guidelines:**
|
||||
- Use `to` for internal navigation
|
||||
- Use `href` for external links
|
||||
- Position important items on the left, secondary items on the right
|
||||
|
||||
### 5. Footer Configuration (`!!site.footer`)
|
||||
|
||||
**Purpose:** Configure footer styling.
|
||||
|
||||
**Optional Parameters:**
|
||||
- `style`: "dark" or "light" (default: "dark")
|
||||
|
||||
**Example:**
|
||||
```heroscript
|
||||
!!site.footer
|
||||
style: "dark"
|
||||
```
|
||||
|
||||
### 6. Footer Items (`!!site.footer_item`)
|
||||
|
||||
**Purpose:** Add links to the footer, grouped by title.
|
||||
|
||||
**Required Parameters:**
|
||||
- `title`: Group title (items with same title are grouped together)
|
||||
- `label`: Link text
|
||||
|
||||
**Required Parameters (one of):**
|
||||
- `to`: Internal link path
|
||||
- `href`: External URL
|
||||
|
||||
**Example:**
|
||||
```heroscript
|
||||
!!site.footer_item
|
||||
title: "Docs"
|
||||
label: "Introduction"
|
||||
to: "intro"
|
||||
|
||||
!!site.footer_item
|
||||
title: "Docs"
|
||||
label: "API Reference"
|
||||
to: "api"
|
||||
|
||||
!!site.footer_item
|
||||
title: "Community"
|
||||
label: "Discord"
|
||||
href: "https://discord.gg/example"
|
||||
```
|
||||
|
||||
**AI Guidelines:**
|
||||
- Group related links under the same title
|
||||
- Use consistent title names across related items
|
||||
- Provide both internal and external links as appropriate
|
||||
|
||||
### 7. Page Categories (`!!site.page_category`)
|
||||
|
||||
**Purpose:** Create a section/category to organize pages.
|
||||
|
||||
**Required Parameters:**
|
||||
- `name`: Category identifier (snake_case)
|
||||
|
||||
**Optional Parameters:**
|
||||
- `label`: Display name (auto-generated from name if not provided)
|
||||
- `position`: Manual sort order (auto-incremented if not specified)
|
||||
- `path`: URL path segment (defaults to normalized label)
|
||||
|
||||
**Example:**
|
||||
```heroscript
|
||||
!!site.page_category
|
||||
name: "getting_started"
|
||||
label: "Getting Started"
|
||||
position: 100
|
||||
|
||||
!!site.page_category
|
||||
name: "advanced_topics"
|
||||
label: "Advanced Topics"
|
||||
```
|
||||
|
||||
**AI Guidelines:**
|
||||
- Use descriptive snake_case names
|
||||
- Let label be auto-generated when possible (name_fix converts to Title Case)
|
||||
- Categories persist for all subsequent pages until a new category is declared
|
||||
- Position values should leave gaps (100, 200, 300) for future insertions
|
||||
|
||||
### 8. Pages (`!!site.page`)
|
||||
|
||||
**Purpose:** Define individual pages in the site.
|
||||
|
||||
**Required Parameters:**
|
||||
- `src`: Source reference as `collection:page_name` (required for first page in a collection)
|
||||
|
||||
**Optional Parameters:**
|
||||
- `name`: Page identifier (extracted from src if not provided)
|
||||
- `title`: Page title (extracted from markdown if not provided)
|
||||
- `description`: Page description for metadata
|
||||
- `slug`: Custom URL slug
|
||||
- `position`: Manual sort order (auto-incremented if not specified)
|
||||
- `draft`: Mark as draft (default: false)
|
||||
- `hide_title`: Hide title in rendering (default: false)
|
||||
- `path`: Custom path (defaults to current category name)
|
||||
- `category`: Override current category
|
||||
- `title_nr`: Title numbering level
|
||||
|
||||
**Example:**
|
||||
```heroscript
|
||||
!!site.page src: "docs:introduction"
|
||||
description: "Introduction to the platform"
|
||||
slug: "/"
|
||||
|
||||
!!site.page src: "quickstart"
|
||||
description: "Get started in 5 minutes"
|
||||
|
||||
!!site.page src: "installation"
|
||||
title: "Installation Guide"
|
||||
description: "How to install and configure"
|
||||
position: 10
|
||||
```
|
||||
|
||||
**AI Guidelines:**
|
||||
- **Collection Persistence:** Specify collection once (e.g., `docs:introduction`), then subsequent pages only need page name (e.g., `quickstart`)
|
||||
- **Category Persistence:** Pages belong to the most recently declared category
|
||||
- **Title Extraction:** Prefer extracting titles from markdown files
|
||||
- **Position Management:** Use automatic positioning unless specific order is required
|
||||
- **Description Required:** Always provide descriptions for SEO
|
||||
- **Slug Usage:** Use slug for special pages like homepage (`slug: "/"`)
|
||||
|
||||
### 9. Import External Content (`!!site.import`)
|
||||
|
||||
**Purpose:** Import content from external sources.
|
||||
|
||||
**Optional Parameters:**
|
||||
- `name`: Import identifier
|
||||
- `url`: Git URL or HTTP URL
|
||||
- `path`: Local file system path
|
||||
- `dest`: Destination path in site
|
||||
- `replace`: Comma-separated key:value pairs for variable replacement
|
||||
- `visible`: Whether imported content is visible (default: true)
|
||||
|
||||
**Example:**
|
||||
```heroscript
|
||||
!!site.import
|
||||
url: "https://github.com/example/docs"
|
||||
dest: "external"
|
||||
replace: "VERSION:1.0.0,PROJECT:MyProject"
|
||||
visible: true
|
||||
```
|
||||
|
||||
**AI Guidelines:**
|
||||
- Use for shared documentation across multiple sites
|
||||
- Replace variables using `${VARIABLE}` syntax in source content
|
||||
- Set `visible: false` for imported templates or partials
|
||||
|
||||
### 10. Publish Destinations (`!!site.publish` and `!!site.publish_dev`)
|
||||
|
||||
**Purpose:** Define where to publish the built site.
|
||||
|
||||
**Optional Parameters:**
|
||||
- `path`: File system path or URL
|
||||
- `ssh_name`: SSH connection name for remote deployment
|
||||
|
||||
**Example:**
|
||||
```heroscript
|
||||
!!site.publish
|
||||
path: "/var/www/html/docs"
|
||||
ssh_name: "production_server"
|
||||
|
||||
!!site.publish_dev
|
||||
path: "/tmp/docs-preview"
|
||||
```
|
||||
|
||||
**AI Guidelines:**
|
||||
- Use `!!site.publish` for production deployments
|
||||
- Use `!!site.publish_dev` for development/preview deployments
|
||||
- Can specify multiple destinations
|
||||
|
||||
## File Organization Best Practices
|
||||
|
||||
### Naming Convention
|
||||
|
||||
Use numeric prefixes to control execution order:
|
||||
|
||||
```
|
||||
0_config.heroscript # Site configuration
|
||||
1_navigation.heroscript # Menu and footer
|
||||
2_intro.heroscript # Introduction pages
|
||||
3_guides.heroscript # User guides
|
||||
4_reference.heroscript # API reference
|
||||
```
|
||||
|
||||
**AI Guidelines:**
|
||||
- Always use numeric prefixes (0_, 1_, 2_, etc.)
|
||||
- Leave gaps in numbering (0, 10, 20) for future insertions
|
||||
- Group related configurations in the same file
|
||||
- Process order matters: config → navigation → pages
|
||||
|
||||
### Execution Order Rules
|
||||
|
||||
1. **Configuration First:** `!!site.config` must be processed before other actions
|
||||
2. **Categories Before Pages:** Declare `!!site.page_category` before pages in that category
|
||||
3. **Collection Persistence:** First page in a collection must specify `collection:page_name`
|
||||
4. **Category Persistence:** Pages inherit the most recent category declaration
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Simple Documentation Site
|
||||
|
||||
```heroscript
|
||||
!!site.config
|
||||
name: "simple_docs"
|
||||
title: "Simple Documentation"
|
||||
|
||||
!!site.navbar
|
||||
title: "Simple Docs"
|
||||
|
||||
!!site.page src: "docs:index"
|
||||
description: "Welcome page"
|
||||
slug: "/"
|
||||
|
||||
!!site.page src: "getting-started"
|
||||
description: "Getting started guide"
|
||||
|
||||
!!site.page src: "api"
|
||||
description: "API reference"
|
||||
```
|
||||
|
||||
### Pattern 2: Multi-Section Documentation
|
||||
|
||||
```heroscript
|
||||
!!site.config
|
||||
name: "multi_section_docs"
|
||||
title: "Complete Documentation"
|
||||
|
||||
!!site.page_category
|
||||
name: "introduction"
|
||||
label: "Introduction"
|
||||
|
||||
!!site.page src: "docs:welcome"
|
||||
description: "Welcome to our documentation"
|
||||
|
||||
!!site.page src: "overview"
|
||||
description: "Platform overview"
|
||||
|
||||
!!site.page_category
|
||||
name: "tutorials"
|
||||
label: "Tutorials"
|
||||
|
||||
!!site.page src: "tutorial_basics"
|
||||
description: "Basic tutorial"
|
||||
|
||||
!!site.page src: "tutorial_advanced"
|
||||
description: "Advanced tutorial"
|
||||
```
|
||||
|
||||
### Pattern 3: Complex Site with External Links
|
||||
|
||||
```heroscript
|
||||
!!site.config
|
||||
name: "complex_site"
|
||||
title: "Complex Documentation Site"
|
||||
url: "https://docs.example.com"
|
||||
|
||||
!!site.navbar
|
||||
title: "My Platform"
|
||||
logo_src: "img/logo.svg"
|
||||
|
||||
!!site.navbar_item
|
||||
label: "Docs"
|
||||
to: "docs/intro"
|
||||
position: "left"
|
||||
|
||||
!!site.navbar_item
|
||||
label: "API"
|
||||
to: "api"
|
||||
position: "left"
|
||||
|
||||
!!site.navbar_item
|
||||
label: "GitHub"
|
||||
href: "https://github.com/example/repo"
|
||||
position: "right"
|
||||
|
||||
!!site.footer
|
||||
style: "dark"
|
||||
|
||||
!!site.footer_item
|
||||
title: "Documentation"
|
||||
label: "Getting Started"
|
||||
to: "docs/intro"
|
||||
|
||||
!!site.footer_item
|
||||
title: "Community"
|
||||
label: "Discord"
|
||||
href: "https://discord.gg/example"
|
||||
|
||||
!!site.page_category
|
||||
name: "getting_started"
|
||||
|
||||
!!site.page src: "docs:introduction"
|
||||
description: "Introduction to the platform"
|
||||
slug: "/"
|
||||
|
||||
!!site.page src: "installation"
|
||||
description: "Installation guide"
|
||||
```
|
||||
|
||||
## Error Prevention
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
|
||||
1. **Missing Collection on First Page:**
|
||||
```heroscript
|
||||
# WRONG - no collection specified
|
||||
!!site.page src: "introduction"
|
||||
|
||||
# CORRECT
|
||||
!!site.page src: "docs:introduction"
|
||||
```
|
||||
|
||||
2. **Category Without Name:**
|
||||
```heroscript
|
||||
# WRONG - missing name
|
||||
!!site.page_category
|
||||
label: "Getting Started"
|
||||
|
||||
# CORRECT
|
||||
!!site.page_category
|
||||
name: "getting_started"
|
||||
label: "Getting Started"
|
||||
```
|
||||
|
||||
3. **Missing Description:**
|
||||
```heroscript
|
||||
# WRONG - no description
|
||||
!!site.page src: "docs:intro"
|
||||
|
||||
# CORRECT
|
||||
!!site.page src: "docs:intro"
|
||||
description: "Introduction to the platform"
|
||||
```
|
||||
|
||||
4. **Incorrect File Ordering:**
|
||||
```
|
||||
# WRONG - pages before config
|
||||
pages.heroscript
|
||||
config.heroscript
|
||||
|
||||
# CORRECT - config first
|
||||
0_config.heroscript
|
||||
1_pages.heroscript
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
When generating HeroScript for the Site module, verify:
|
||||
|
||||
- [ ] `!!site.config` includes `name` parameter
|
||||
- [ ] All pages have `description` parameter
|
||||
- [ ] First page in each collection specifies `collection:page_name`
|
||||
- [ ] Categories are declared before their pages
|
||||
- [ ] Files use numeric prefixes for ordering
|
||||
- [ ] Navigation items have either `to` or `href`
|
||||
- [ ] Footer items are grouped by `title`
|
||||
- [ ] External URLs include protocol (https://)
|
||||
- [ ] Paths don't have trailing slashes unless intentional
|
||||
- [ ] Draft pages are marked with `draft: true`
|
||||
|
||||
## Integration with V Code
|
||||
|
||||
When working with the Site module in V code:
|
||||
|
||||
```v
|
||||
import incubaid.herolib.web.site
|
||||
import incubaid.herolib.core.playbook
|
||||
|
||||
// Process HeroScript files
|
||||
mut plbook := playbook.new(path: '/path/to/heroscripts')!
|
||||
site.play(mut plbook)!
|
||||
|
||||
// Access configured site
|
||||
mut mysite := site.get(name: 'my_site')!
|
||||
|
||||
// Iterate through pages
|
||||
for page in mysite.pages {
|
||||
println('Page: ${page.name} - ${page.description}')
|
||||
}
|
||||
|
||||
// Iterate through sections
|
||||
for section in mysite.sections {
|
||||
println('Section: ${section.label}')
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The Site module's HeroScript format provides a declarative way to configure websites with:
|
||||
- Clear separation of concerns (config, navigation, content)
|
||||
- Automatic ordering and organization
|
||||
- Collection and category persistence for reduced repetition
|
||||
- Flexible metadata and SEO configuration
|
||||
- Support for both internal and external content
|
||||
|
||||
Always follow the execution order rules, use numeric file prefixes, and provide complete metadata for best results.
|
||||
@@ -1,48 +0,0 @@
|
||||
module site
|
||||
|
||||
import incubaid.herolib.core.texttools
|
||||
|
||||
__global (
|
||||
websites map[string]&Site
|
||||
)
|
||||
|
||||
@[params]
|
||||
pub struct FactoryArgs {
|
||||
pub mut:
|
||||
name string = 'default'
|
||||
}
|
||||
|
||||
pub fn new(args FactoryArgs) !&Site {
|
||||
name := texttools.name_fix(args.name)
|
||||
|
||||
websites[name] = &Site{
|
||||
siteconfig: SiteConfig{
|
||||
name: name
|
||||
}
|
||||
}
|
||||
return get(name: name)!
|
||||
}
|
||||
|
||||
pub fn get(args FactoryArgs) !&Site {
|
||||
name := texttools.name_fix(args.name)
|
||||
mut sc := websites[name] or { return error('siteconfig with name "${name}" does not exist') }
|
||||
return sc
|
||||
}
|
||||
|
||||
pub fn exists(args FactoryArgs) bool {
|
||||
name := texttools.name_fix(args.name)
|
||||
mut sc := websites[name] or { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
pub fn default() !&Site {
|
||||
if websites.len == 0 {
|
||||
return new(name: 'default')!
|
||||
}
|
||||
return get()!
|
||||
}
|
||||
|
||||
// list returns all site names that have been created
|
||||
pub fn list() []string {
|
||||
return websites.keys()
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
module site
|
||||
|
||||
pub struct Page {
|
||||
pub mut:
|
||||
name string
|
||||
title string
|
||||
description string
|
||||
draft bool
|
||||
position int
|
||||
hide_title bool
|
||||
src string @[required] // always in format collection:page_name, can use the default collection if no : specified
|
||||
path string @[required] // is without the page name, so just the path to the folder where the page is in
|
||||
section_name string
|
||||
title_nr int
|
||||
slug string
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
module site
|
||||
|
||||
@[heap]
|
||||
pub struct Site {
|
||||
pub mut:
|
||||
pages []Page
|
||||
sections []Section
|
||||
siteconfig SiteConfig
|
||||
}
|
||||
|
||||
pub struct Section {
|
||||
pub mut:
|
||||
name string
|
||||
position int
|
||||
path string
|
||||
label string
|
||||
description string
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
module site
|
||||
|
||||
import os
|
||||
import incubaid.herolib.core.playbook { PlayBook }
|
||||
import incubaid.herolib.core.texttools
|
||||
import time
|
||||
|
||||
pub fn play(mut plbook PlayBook) ! {
|
||||
if !plbook.exists(filter: 'site.') {
|
||||
return
|
||||
}
|
||||
|
||||
mut config_action := plbook.ensure_once(filter: 'site.config')!
|
||||
|
||||
mut p := config_action.params
|
||||
name := p.get_default('name', 'default')! // Use 'default' as fallback name
|
||||
|
||||
// configure the website
|
||||
mut website := new(name: name)!
|
||||
mut config := &website.siteconfig
|
||||
|
||||
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.str() +
|
||||
' Example Organization')!
|
||||
config.url = p.get_default('url', '')!
|
||||
config.base_url = p.get_default('base_url', '/')!
|
||||
config.url_home = p.get_default('url_home', '')!
|
||||
|
||||
// Process !!site.config_meta for specific metadata overrides
|
||||
mut meta_action := plbook.ensure_once(filter: 'site.config_meta')!
|
||||
mut p_meta := meta_action.params
|
||||
|
||||
// If 'title' is present in site.config_meta, it overrides. Otherwise, meta_title remains empty or uses site.config.title logic in docusaurus model.
|
||||
config.meta_title = p_meta.get_default('title', config.title)!
|
||||
// If 'image' is present in site.config_meta, it overrides. Otherwise, meta_image remains empty or uses site.config.image logic.
|
||||
config.meta_image = p_meta.get_default('image', config.image)!
|
||||
// If 'description' is present in site.config_meta, it overrides the main description
|
||||
if p_meta.exists('description') {
|
||||
config.description = p_meta.get('description')!
|
||||
}
|
||||
|
||||
config_action.done = true // Mark the action as done
|
||||
meta_action.done = true
|
||||
|
||||
play_import(mut plbook, mut config)!
|
||||
play_menu(mut plbook, mut config)!
|
||||
play_footer(mut plbook, mut config)!
|
||||
play_announcement(mut plbook, mut config)!
|
||||
play_publish(mut plbook, mut config)!
|
||||
play_publish_dev(mut plbook, mut config)!
|
||||
play_pages(mut plbook, mut website)!
|
||||
}
|
||||
|
||||
fn play_import(mut plbook PlayBook, mut config SiteConfig) ! {
|
||||
mut import_actions := plbook.find(filter: 'site.import')!
|
||||
// println('import_actions: ${import_actions}')
|
||||
|
||||
for mut action in import_actions {
|
||||
mut p := action.params
|
||||
mut replace_map := map[string]string{}
|
||||
if replace_str := p.get_default('replace', '') {
|
||||
parts := replace_str.split(',')
|
||||
for part in parts {
|
||||
kv := part.split(':')
|
||||
if kv.len == 2 {
|
||||
replace_map[kv[0].trim_space()] = kv[1].trim_space()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mut importpath := p.get_default('path', '')!
|
||||
if importpath != '' {
|
||||
if !importpath.starts_with('/') {
|
||||
importpath = os.abs_path('${plbook.path}/${importpath}')
|
||||
}
|
||||
}
|
||||
|
||||
mut import_ := ImportItem{
|
||||
name: p.get_default('name', '')!
|
||||
url: p.get_default('url', '')!
|
||||
path: importpath
|
||||
dest: p.get_default('dest', '')!
|
||||
replace: replace_map
|
||||
visible: p.get_default_false('visible')
|
||||
}
|
||||
config.imports << import_
|
||||
|
||||
action.done = true // Mark the action as done
|
||||
}
|
||||
}
|
||||
|
||||
fn play_menu(mut plbook PlayBook, mut config SiteConfig) ! {
|
||||
mut navbar_actions := plbook.find(filter: 'site.navbar')!
|
||||
if navbar_actions.len > 0 {
|
||||
for mut action in navbar_actions { // Should ideally be one, but loop for safety
|
||||
mut p := action.params
|
||||
config.menu.title = p.get_default('title', config.title)! // Use existing config.title as ultimate fallback
|
||||
config.menu.logo_alt = p.get_default('logo_alt', '')!
|
||||
config.menu.logo_src = p.get_default('logo_src', '')!
|
||||
config.menu.logo_src_dark = p.get_default('logo_src_dark', '')!
|
||||
action.done = true // Mark the action as done
|
||||
}
|
||||
} else {
|
||||
// Fallback to site.menu for title if site.navbar is not found
|
||||
mut menu_actions := plbook.find(filter: 'site.menu')!
|
||||
for mut action in menu_actions {
|
||||
mut p := action.params
|
||||
config.menu.title = p.get_default('title', config.title)!
|
||||
config.menu.logo_alt = p.get_default('logo_alt', '')!
|
||||
config.menu.logo_src = p.get_default('logo_src', '')!
|
||||
config.menu.logo_src_dark = p.get_default('logo_src_dark', '')!
|
||||
action.done = true // Mark the action as done
|
||||
}
|
||||
}
|
||||
|
||||
mut menu_item_actions := plbook.find(filter: 'site.navbar_item')!
|
||||
if menu_item_actions.len == 0 {
|
||||
// Fallback to site.menu_item if site.navbar_item is not found
|
||||
menu_item_actions = plbook.find(filter: 'site.menu_item')!
|
||||
}
|
||||
|
||||
// Clear existing menu items to prevent duplication
|
||||
config.menu.items = []MenuItem{}
|
||||
|
||||
for mut action in menu_item_actions {
|
||||
mut p := action.params
|
||||
mut item := MenuItem{
|
||||
label: p.get_default('label', 'Documentation')!
|
||||
href: p.get_default('href', '')!
|
||||
to: p.get_default('to', '')!
|
||||
position: p.get_default('position', 'right')!
|
||||
}
|
||||
config.menu.items << item
|
||||
action.done = true // Mark the action as done
|
||||
}
|
||||
}
|
||||
|
||||
fn play_footer(mut plbook PlayBook, mut config SiteConfig) ! {
|
||||
mut footer_actions := plbook.find(filter: 'site.footer')!
|
||||
for mut action in footer_actions {
|
||||
mut p := action.params
|
||||
config.footer.style = p.get_default('style', 'dark')!
|
||||
action.done = true // Mark the action as done
|
||||
}
|
||||
|
||||
mut footer_item_actions := plbook.find(filter: 'site.footer_item')!
|
||||
mut links_map := map[string][]FooterItem{}
|
||||
|
||||
// Clear existing footer links to prevent duplication
|
||||
config.footer.links = []FooterLink{}
|
||||
|
||||
for mut action in footer_item_actions {
|
||||
mut p := action.params
|
||||
title := p.get_default('title', 'Docs')!
|
||||
mut item := FooterItem{
|
||||
label: p.get_default('label', 'Introduction')!
|
||||
href: p.get_default('href', '')!
|
||||
to: p.get_default('to', '')!
|
||||
}
|
||||
|
||||
if title !in links_map {
|
||||
links_map[title] = []FooterItem{}
|
||||
}
|
||||
links_map[title] << item
|
||||
action.done = true // Mark the action as done
|
||||
}
|
||||
|
||||
// Convert map to footer links array
|
||||
for title, items in links_map {
|
||||
config.footer.links << FooterLink{
|
||||
title: title
|
||||
items: items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn play_announcement(mut plbook PlayBook, mut config SiteConfig) ! {
|
||||
mut announcement_actions := plbook.find(filter: 'site.announcement')!
|
||||
if announcement_actions.len > 0 {
|
||||
// Only process the first announcement action
|
||||
mut action := announcement_actions[0]
|
||||
mut p := action.params
|
||||
|
||||
config.announcement = AnnouncementBar{
|
||||
id: p.get_default('id', 'announcement')!
|
||||
content: p.get_default('content', '')!
|
||||
background_color: p.get_default('background_color', '#20232a')!
|
||||
text_color: p.get_default('text_color', '#fff')!
|
||||
is_closeable: p.get_default_true('is_closeable')
|
||||
}
|
||||
|
||||
action.done = true // Mark the action as done
|
||||
}
|
||||
}
|
||||
|
||||
fn play_publish(mut plbook PlayBook, mut config SiteConfig) ! {
|
||||
mut build_dest_actions := plbook.find(filter: 'site.publish')!
|
||||
for mut action in build_dest_actions {
|
||||
mut p := action.params
|
||||
mut dest := BuildDest{
|
||||
path: p.get_default('path', '')! // can be url
|
||||
ssh_name: p.get_default('ssh_name', '')!
|
||||
}
|
||||
config.build_dest << dest
|
||||
action.done = true // Mark the action as done
|
||||
}
|
||||
}
|
||||
|
||||
fn play_publish_dev(mut plbook PlayBook, mut config SiteConfig) ! {
|
||||
mut build_dest_actions := plbook.find(filter: 'site.publish_dev')!
|
||||
for mut action in build_dest_actions {
|
||||
mut p := action.params
|
||||
mut dest := BuildDest{
|
||||
path: p.get_default('path', '')! // can be url
|
||||
ssh_name: p.get_default('ssh_name', '')!
|
||||
}
|
||||
config.build_dest_dev << dest
|
||||
action.done = true // Mark the action as done
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
module site
|
||||
|
||||
import incubaid.herolib.core.playbook { PlayBook }
|
||||
import incubaid.herolib.core.texttools
|
||||
|
||||
// plays the sections & pages
|
||||
fn play_pages(mut plbook PlayBook, mut site Site) ! {
|
||||
// mut siteconfig := &site.siteconfig
|
||||
|
||||
// if only 1 doctree is specified, then we use that as the default doctree name
|
||||
// mut doctreename := 'main' // Not used for now, keep commented for future doctree integration
|
||||
// if plbook.exists(filter: 'site.doctree') {
|
||||
// if plbook.exists_once(filter: 'site.doctree') {
|
||||
// mut action := plbook.get(filter: 'site.doctree')!
|
||||
// mut p := action.params
|
||||
// doctreename = p.get('name') or { return error('need to specify name in site.doctree') }
|
||||
// } else {
|
||||
// return error("can't have more than one site.doctree")
|
||||
// }
|
||||
// }
|
||||
|
||||
mut section_current := Section{} // is the category
|
||||
mut position_section := 1
|
||||
mut position_category := 100 // Start categories at position 100
|
||||
mut collection_current := '' // current collection we are working on
|
||||
|
||||
mut all_actions := plbook.find(filter: 'site.')!
|
||||
|
||||
for mut action in all_actions {
|
||||
if action.done {
|
||||
continue
|
||||
}
|
||||
|
||||
mut p := action.params
|
||||
|
||||
if action.name == 'page_category' {
|
||||
mut section := Section{}
|
||||
section.name = p.get('name') or {
|
||||
return error('need to specify name in site.page_category. Action: ${action}')
|
||||
}
|
||||
position_section = 1 // go back to default position for pages in the category
|
||||
section.position = p.get_int_default('position', position_category)!
|
||||
if section.position == position_category {
|
||||
position_category += 100 // Increment for next category
|
||||
}
|
||||
section.label = p.get_default('label', texttools.name_fix_snake_to_pascal(section.name))!
|
||||
section.path = p.get_default('path', texttools.name_fix(section.label))!
|
||||
section.description = p.get_default('description', '')!
|
||||
|
||||
site.sections << section
|
||||
action.done = true // Mark the action as done
|
||||
section_current = section
|
||||
continue // next action
|
||||
}
|
||||
|
||||
if action.name == 'page' {
|
||||
mut pagesrc := p.get_default('src', '')!
|
||||
mut pagename := p.get_default('name', '')!
|
||||
mut pagecollection := ''
|
||||
|
||||
if pagesrc.contains(':') {
|
||||
pagecollection = pagesrc.split(':')[0]
|
||||
pagename = pagesrc.split(':')[1]
|
||||
} else {
|
||||
if collection_current.len > 0 {
|
||||
pagecollection = collection_current
|
||||
pagename = pagesrc // ADD THIS LINE - use pagesrc as the page name
|
||||
} else {
|
||||
return error('need to specify collection in page.src path as collection:page_name or make sure someone before you did. Got src="${pagesrc}" with no collection set. Action: ${action}')
|
||||
}
|
||||
}
|
||||
|
||||
pagecollection = texttools.name_fix(pagecollection)
|
||||
collection_current = pagecollection
|
||||
pagename = texttools.name_fix_keepext(pagename)
|
||||
if pagename.ends_with('.md') {
|
||||
pagename = pagename.replace('.md', '')
|
||||
}
|
||||
|
||||
if pagename == '' {
|
||||
return error('need to specify name in page.src or specify in path as collection:page_name. Action: ${action}')
|
||||
}
|
||||
if pagecollection == '' {
|
||||
return error('need to specify collection in page.src or specify in path as collection:page_name. Action: ${action}')
|
||||
}
|
||||
|
||||
// recreate the pagepath
|
||||
pagesrc = '${pagecollection}:${pagename}'
|
||||
|
||||
// get sectionname from category, page_category or section, if not specified use current section
|
||||
section_name := p.get_default('category', p.get_default('page_category', p.get_default('section',
|
||||
section_current.name)!)!)!
|
||||
mut pagepath := p.get_default('path', section_current.path)!
|
||||
pagepath = pagepath.trim_space().trim('/')
|
||||
// Only apply name_fix if it's a simple name (no path separators)
|
||||
// For paths like 'appendix/internet_today', preserve the structure
|
||||
if !pagepath.contains('/') {
|
||||
pagepath = texttools.name_fix(pagepath)
|
||||
}
|
||||
// Ensure pagepath ends with / to indicate it's a directory path
|
||||
if pagepath.len > 0 && !pagepath.ends_with('/') {
|
||||
pagepath += '/'
|
||||
}
|
||||
|
||||
mut mypage := Page{
|
||||
section_name: section_name
|
||||
name: pagename
|
||||
path: pagepath
|
||||
src: pagesrc
|
||||
}
|
||||
|
||||
mypage.position = p.get_int_default('position', 0)!
|
||||
if mypage.position == 0 {
|
||||
mypage.position = section_current.position + position_section
|
||||
position_section += 1
|
||||
}
|
||||
mypage.title = p.get_default('title', '')!
|
||||
|
||||
mypage.description = p.get_default('description', '')!
|
||||
mypage.slug = p.get_default('slug', '')!
|
||||
mypage.draft = p.get_default_false('draft')
|
||||
mypage.hide_title = p.get_default_false('hide_title')
|
||||
mypage.title_nr = p.get_int_default('title_nr', 0)!
|
||||
|
||||
site.pages << mypage
|
||||
|
||||
action.done = true // Mark the action as done
|
||||
}
|
||||
|
||||
// println(action)
|
||||
// println(section_current)
|
||||
// println(site.pages.last())
|
||||
// $dbg;
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## Purpose
|
||||
|
||||
The Site module allows you to:
|
||||
|
||||
- Define website structure and configuration in a declarative way using HeroScript
|
||||
- Organize pages into sections/categories
|
||||
- Configure navigation menus and footers
|
||||
- Manage page metadata (title, description, slug, etc.)
|
||||
- Support multiple content collections
|
||||
- Define build and publish destinations
|
||||
|
||||
## Quick Start
|
||||
|
||||
```v
|
||||
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import incubaid.herolib.develop.gittools
|
||||
import incubaid.herolib.web.site
|
||||
import incubaid.herolib.core.playcmds
|
||||
|
||||
// Clone or use existing repository with HeroScript files
|
||||
mysitepath := gittools.path(
|
||||
git_url: 'https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech'
|
||||
git_pull: true
|
||||
)!
|
||||
|
||||
// Process all HeroScript files in the path
|
||||
playcmds.run(heroscript_path: mysitepath.path)!
|
||||
|
||||
// Get the configured site
|
||||
mut mysite := site.get(name: 'tfgrid_tech')!
|
||||
println(mysite)
|
||||
```
|
||||
|
||||
## HeroScript Syntax
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```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: "/"
|
||||
```
|
||||
|
||||
### Navigation Menu
|
||||
|
||||
```heroscript
|
||||
!!site.navbar
|
||||
title: "My Site"
|
||||
logo_alt: "Site Logo"
|
||||
logo_src: "img/logo.svg"
|
||||
logo_src_dark: "img/logo-dark.svg"
|
||||
|
||||
!!site.navbar_item
|
||||
label: "Documentation"
|
||||
to: "docs/intro"
|
||||
position: "left"
|
||||
|
||||
!!site.navbar_item
|
||||
label: "GitHub"
|
||||
href: "https://github.com/myorg/myrepo"
|
||||
position: "right"
|
||||
```
|
||||
|
||||
### Footer Configuration
|
||||
|
||||
```heroscript
|
||||
!!site.footer
|
||||
style: "dark"
|
||||
|
||||
!!site.footer_item
|
||||
title: "Docs"
|
||||
label: "Introduction"
|
||||
to: "intro"
|
||||
|
||||
!!site.footer_item
|
||||
title: "Docs"
|
||||
label: "Getting Started"
|
||||
href: "https://docs.example.com/getting-started"
|
||||
|
||||
!!site.footer_item
|
||||
title: "Community"
|
||||
label: "Discord"
|
||||
href: "https://discord.gg/example"
|
||||
```
|
||||
|
||||
## Page Organization
|
||||
|
||||
### Example 1: Simple Pages Without Categories
|
||||
|
||||
When you don't need categories, pages are added sequentially. The collection only needs to be specified once, then it's reused for subsequent pages.
|
||||
|
||||
```heroscript
|
||||
!!site.page src: "mycelium_tech:introduction"
|
||||
description: "Introduction to ThreeFold Technology"
|
||||
slug: "/"
|
||||
|
||||
!!site.page src: "vision"
|
||||
description: "Our Vision for the Future Internet"
|
||||
|
||||
!!site.page src: "what"
|
||||
description: "What ThreeFold is Building"
|
||||
|
||||
!!site.page src: "presentation"
|
||||
description: "ThreeFold Technology Presentation"
|
||||
|
||||
!!site.page src: "status"
|
||||
description: "Current Development Status"
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
|
||||
- First page specifies collection as `tech:introduction` (collection:page_name format)
|
||||
- Subsequent pages only need the page name (e.g., `vision`) - the `tech` collection is reused
|
||||
- If `title` is not specified, it will be extracted from the markdown file itself
|
||||
- Pages are ordered by their appearance in the HeroScript file
|
||||
- `slug` can be used to customize the URL path (e.g., `"/"` for homepage)
|
||||
|
||||
### Example 2: Pages with Categories
|
||||
|
||||
Categories (sections) help organize pages into logical groups with their own navigation structure.
|
||||
|
||||
```heroscript
|
||||
!!site.page_category
|
||||
name: "first_principle_thinking"
|
||||
label: "First Principle Thinking"
|
||||
|
||||
!!site.page src: "first_principle_thinking:hardware_badly_used"
|
||||
description: "Hardware is not used properly, why it is important to understand hardware"
|
||||
|
||||
!!site.page src: "internet_risk"
|
||||
description: "Internet risk, how to mitigate it, and why it is important"
|
||||
|
||||
!!site.page src: "onion_analogy"
|
||||
description: "Compare onion with a computer, layers of abstraction"
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
|
||||
- `!!site.page_category` creates a new section/category
|
||||
- `name` is the internal identifier (snake_case)
|
||||
- `label` is the display name (automatically derived from `name` if not specified)
|
||||
- Category name is converted to title case: `first_principle_thinking` → "First Principle Thinking"
|
||||
- Once a category is defined, all subsequent pages belong to it until a new category is declared
|
||||
- Collection persistence works the same: specify once (e.g., `first_principle_thinking:hardware_badly_used`), then reuse
|
||||
|
||||
### Example 3: Advanced Page Configuration
|
||||
|
||||
```heroscript
|
||||
!!site.page_category
|
||||
name: "components"
|
||||
label: "System Components"
|
||||
position: 100
|
||||
|
||||
!!site.page src: "mycelium_tech:mycelium"
|
||||
title: "Mycelium Network"
|
||||
description: "Peer-to-peer overlay network"
|
||||
slug: "mycelium-network"
|
||||
position: 1
|
||||
draft: false
|
||||
hide_title: false
|
||||
|
||||
!!site.page src: "fungistor"
|
||||
title: "Fungistor Storage"
|
||||
description: "Distributed storage system"
|
||||
position: 2
|
||||
```
|
||||
|
||||
**Available Page Parameters:**
|
||||
|
||||
- `src`: Source reference as `collection:page_name` (required for first page in collection)
|
||||
- `title`: Page title (optional, extracted from markdown if not provided)
|
||||
- `description`: Page description for metadata
|
||||
- `slug`: Custom URL slug
|
||||
- `position`: Manual ordering (auto-incremented if not specified)
|
||||
- `draft`: Mark page as draft (default: false)
|
||||
- `hide_title`: Hide the page title in rendering (default: false)
|
||||
- `path`: Custom path for the page (defaults to category name)
|
||||
- `category`: Override the current category for this page
|
||||
|
||||
## File Organization
|
||||
|
||||
HeroScript files should be organized with numeric prefixes to control execution order:
|
||||
|
||||
```
|
||||
docs/
|
||||
├── 0_config.heroscript # Site configuration
|
||||
├── 1_menu.heroscript # Navigation and footer
|
||||
├── 2_intro_pages.heroscript # Introduction pages
|
||||
├── 3_tech_pages.heroscript # Technical documentation
|
||||
└── 4_api_pages.heroscript # API reference
|
||||
```
|
||||
|
||||
**Important:** Files are processed in alphabetical order, so use numeric prefixes (0_, 1_, 2_, etc.) to ensure correct execution sequence.
|
||||
|
||||
## Import External Content
|
||||
|
||||
```heroscript
|
||||
!!site.import
|
||||
url: "https://github.com/example/external-docs"
|
||||
dest: "external"
|
||||
replace: "PROJECT_NAME:My Project,VERSION:1.0.0"
|
||||
visible: true
|
||||
```
|
||||
|
||||
## Publish Destinations
|
||||
|
||||
```heroscript
|
||||
!!site.publish
|
||||
path: "/var/www/html/docs"
|
||||
ssh_name: "production_server"
|
||||
|
||||
!!site.publish_dev
|
||||
path: "/tmp/docs-preview"
|
||||
```
|
||||
|
||||
## Factory Methods
|
||||
|
||||
### Create or Get a Site
|
||||
|
||||
```v
|
||||
import incubaid.herolib.web.site
|
||||
|
||||
// Create a new site
|
||||
mut mysite := site.new(name: 'my_docs')!
|
||||
|
||||
// Get an existing site
|
||||
mut mysite := site.get(name: 'my_docs')!
|
||||
|
||||
// Get default site
|
||||
mut mysite := site.default()!
|
||||
|
||||
// Check if site exists
|
||||
if site.exists(name: 'my_docs') {
|
||||
println('Site exists')
|
||||
}
|
||||
|
||||
// List all sites
|
||||
sites := site.list()
|
||||
println(sites)
|
||||
```
|
||||
|
||||
### Using with PlayBook
|
||||
|
||||
```v
|
||||
import incubaid.herolib.core.playbook
|
||||
import incubaid.herolib.web.site
|
||||
|
||||
// Create playbook from path
|
||||
mut plbook := playbook.new(path: '/path/to/heroscripts')!
|
||||
|
||||
// Process site configuration
|
||||
site.play(mut plbook)!
|
||||
|
||||
// Access the configured site
|
||||
mut mysite := site.get(name: 'my_site')!
|
||||
```
|
||||
|
||||
## Data Structures
|
||||
|
||||
### Site
|
||||
|
||||
```v
|
||||
pub struct Site {
|
||||
pub mut:
|
||||
pages []Page
|
||||
sections []Section
|
||||
siteconfig SiteConfig
|
||||
}
|
||||
```
|
||||
|
||||
### Page
|
||||
|
||||
```v
|
||||
pub struct Page {
|
||||
pub mut:
|
||||
name string // Page identifier
|
||||
title string // Display title
|
||||
description string // Page description
|
||||
draft bool // Draft status
|
||||
position int // Sort order
|
||||
hide_title bool // Hide title in rendering
|
||||
src string // Source as collection:page_name
|
||||
path string // URL path (without page name)
|
||||
section_name string // Category/section name
|
||||
title_nr int // Title numbering level
|
||||
slug string // Custom URL slug
|
||||
}
|
||||
```
|
||||
|
||||
### Section
|
||||
|
||||
```v
|
||||
pub struct Section {
|
||||
pub mut:
|
||||
name string // Internal identifier
|
||||
position int // Sort order
|
||||
path string // URL path
|
||||
label string // Display name
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **File Naming**: Use numeric prefixes (0_, 1_, 2_) to control execution order
|
||||
2. **Collection Reuse**: Specify collection once, then reuse for subsequent pages
|
||||
3. **Category Organization**: Group related pages under categories for better navigation
|
||||
4. **Title Extraction**: Let titles be extracted from markdown files when possible
|
||||
5. **Position Management**: Use automatic positioning unless you need specific ordering
|
||||
6. **Description**: Always provide descriptions for better SEO and navigation
|
||||
7. **Draft Status**: Use `draft: true` for work-in-progress pages
|
||||
|
||||
## Complete Example
|
||||
|
||||
See `examples/web/site/site_example.vsh` for a complete working example.
|
||||
|
||||
For a real-world example, check: <https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech>
|
||||
Reference in New Issue
Block a user